Merge branch 'release/3.11.0'

This commit is contained in:
Sojan 2024-07-16 19:23:03 -07:00
commit 8ea412bc85
203 changed files with 2502 additions and 3186 deletions

View File

@ -7,7 +7,7 @@ defaults: &defaults
working_directory: ~/build working_directory: ~/build
docker: docker:
# specify the version you desire here # 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 # Specify service dependencies here if necessary
# CircleCI maintains a library of pre-built images # CircleCI maintains a library of pre-built images
@ -130,10 +130,7 @@ jobs:
command: | command: |
mkdir -p ~/tmp/test-results/frontend_specs mkdir -p ~/tmp/test-results/frontend_specs
~/tmp/cc-test-reporter before-build ~/tmp/cc-test-reporter before-build
TESTFILES=$(circleci tests glob **/specs/*.spec.js | circleci tests split --split-by=timings) yarn test:coverage
yarn test:coverage --profile 10 \
--out ~/tmp/test-results/yarn.xml \
-- ${TESTFILES}
- run: - run:
name: Code Climate Test Coverage name: Code Climate Test Coverage
command: | command: |

View File

@ -12,7 +12,7 @@ services:
args: args:
VARIANT: "ubuntu-22.04" VARIANT: "ubuntu-22.04"
NODE_VERSION: "20.9.0" 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. # 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_UID: "1000"
USER_GID: "1000" USER_GID: "1000"
@ -25,7 +25,7 @@ services:
args: args:
VARIANT: "ubuntu-22.04" VARIANT: "ubuntu-22.04"
NODE_VERSION: "20.9.0" 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. # 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_UID: "1000"
USER_GID: "1000" USER_GID: "1000"

View File

@ -65,10 +65,10 @@ module.exports = {
}, },
env: { env: {
browser: true, browser: true,
jest: true,
node: true, node: true,
}, },
globals: { globals: {
bus: true, bus: true,
vi: true,
}, },
}; };

View File

@ -1 +1 @@
3.2.2 3.3.3

View File

@ -1,6 +1,6 @@
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '3.2.2' ruby '3.3.3'
##-- base gems for rails --## ##-- base gems for rails --##
gem 'rack-cors', '2.0.0', require: 'rack/cors' gem 'rack-cors', '2.0.0', require: 'rack/cors'
@ -111,12 +111,12 @@ gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
gem 'scout_apm', 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-ruby', require: false
gem 'sentry-sidekiq', '>= 5.18.0', require: false gem 'sentry-sidekiq', '>= 5.18.1', require: false
##-- background job processing --## ##-- background job processing --##
gem 'sidekiq', '>= 7.2.4' gem 'sidekiq', '>= 7.3.0'
# We want cron jobs # We want cron jobs
gem 'sidekiq-cron', '>= 1.12.0' gem 'sidekiq-cron', '>= 1.12.0'

View File

@ -174,7 +174,9 @@ GEM
crack (0.4.5) crack (0.4.5)
rexml rexml
crass (1.0.6) 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) cypress-on-rails (1.16.0)
rack rack
database_cleaner (2.0.2) database_cleaner (2.0.2)
@ -183,13 +185,16 @@ GEM
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
date (3.3.4) datadog-ci (0.8.3)
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)
msgpack 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) debug (1.8.0)
irb (>= 1.5.0) irb (>= 1.5.0)
reline (>= 0.3.1) reline (>= 0.3.1)
@ -416,15 +421,15 @@ GEM
addressable (~> 2.8) addressable (~> 2.8)
letter_opener (1.8.1) letter_opener (1.8.1)
launchy (>= 2.2, < 3) launchy (>= 2.2, < 3)
libdatadog (2.0.0.1.0) libdatadog (7.0.0.1.0)
libdatadog (2.0.0.1.0-x86_64-linux) libdatadog (7.0.0.1.0-x86_64-linux)
libddwaf (1.8.2.0.0) libddwaf (1.14.0.0.0)
ffi (~> 1.0) ffi (~> 1.0)
libddwaf (1.8.2.0.0-arm64-darwin) libddwaf (1.14.0.0.0-arm64-darwin)
ffi (~> 1.0) ffi (~> 1.0)
libddwaf (1.8.2.0.0-x86_64-darwin) libddwaf (1.14.0.0.0-x86_64-darwin)
ffi (~> 1.0) ffi (~> 1.0)
libddwaf (1.8.2.0.0-x86_64-linux) libddwaf (1.14.0.0.0-x86_64-linux)
ffi (~> 1.0) ffi (~> 1.0)
line-bot-api (1.28.0) line-bot-api (1.28.0)
liquid (5.4.0) liquid (5.4.0)
@ -434,6 +439,7 @@ GEM
llhttp-ffi (0.4.0) llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0) ffi-compiler (~> 1.0)
rake (~> 13.0) rake (~> 13.0)
logger (1.6.0)
lograge (0.14.0) lograge (0.14.0)
actionpack (>= 4) actionpack (>= 4)
activesupport (>= 4) activesupport (>= 4)
@ -460,7 +466,7 @@ GEM
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.7) mini_portile2 (2.8.7)
minitest (5.24.0) minitest (5.24.1)
mock_redis (0.36.0) mock_redis (0.36.0)
ruby2_keywords ruby2_keywords
msgpack (1.7.0) msgpack (1.7.0)
@ -703,23 +709,24 @@ GEM
activesupport (>= 4) activesupport (>= 4)
selectize-rails (0.12.6) selectize-rails (0.12.6)
semantic_range (3.0.0) semantic_range (3.0.0)
sentry-rails (5.18.0) sentry-rails (5.18.1)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.18.0) sentry-ruby (~> 5.18.1)
sentry-ruby (5.18.0) sentry-ruby (5.18.1)
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.18.0) sentry-sidekiq (5.18.1)
sentry-ruby (~> 5.18.0) sentry-ruby (~> 5.18.1)
sidekiq (>= 3.0) sidekiq (>= 3.0)
sexp_processor (4.17.0) sexp_processor (4.17.0)
shoulda-matchers (5.3.0) shoulda-matchers (5.3.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (7.2.4) sidekiq (7.3.0)
concurrent-ruby (< 2) concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
logger
rack (>= 2.2.4) rack (>= 2.2.4)
redis-client (>= 0.19.0) redis-client (>= 0.22.2)
sidekiq-cron (1.12.0) sidekiq-cron (1.12.0)
fugit (~> 1.8) fugit (~> 1.8)
globalid (>= 1.0.1) globalid (>= 1.0.1)
@ -931,11 +938,11 @@ DEPENDENCIES
scout_apm scout_apm
scss_lint scss_lint
seed_dump seed_dump
sentry-rails (>= 5.18.0) sentry-rails (>= 5.18.1)
sentry-ruby sentry-ruby
sentry-sidekiq (>= 5.18.0) sentry-sidekiq (>= 5.18.1)
shoulda-matchers shoulda-matchers
sidekiq (>= 7.2.4) sidekiq (>= 7.3.0)
sidekiq-cron (>= 1.12.0) sidekiq-cron (>= 1.12.0)
simplecov (= 0.17.1) simplecov (= 0.17.1)
slack-ruby-client (~> 2.2.0) slack-ruby-client (~> 2.2.0)
@ -960,7 +967,7 @@ DEPENDENCIES
working_hours working_hours
RUBY VERSION RUBY VERSION
ruby 3.2.2p185 ruby 3.3.3p89
BUNDLED WITH BUNDLED WITH
2.4.6 2.5.14

View File

@ -37,7 +37,7 @@ class DashboardController < ActionController::Base
end end
def set_dashboard_scripts def set_dashboard_scripts
@dashboard_scripts = GlobalConfig.get_value('DASHBOARD_SCRIPTS') @dashboard_scripts = sensitive_path? ? nil : GlobalConfig.get_value('DASHBOARD_SCRIPTS')
end end
def ensure_installation_onboarding def ensure_installation_onboarding
@ -75,4 +75,14 @@ class DashboardController < ActionController::Base
'application' 'application'
end end
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 end

View File

@ -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
});
});

View File

@ -15,10 +15,10 @@ describe('#enterpriseAccountAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -15,10 +15,10 @@ describe('#accountAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -10,10 +10,10 @@ describe('#ContactsAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -14,7 +14,7 @@ describe('#AgentAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -14,10 +14,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {
@ -44,10 +44,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {
@ -71,10 +71,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {
@ -98,10 +98,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {
@ -129,10 +129,10 @@ describe('#PortalAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -4,10 +4,10 @@ describe('#AssignableAgentsAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -13,10 +13,10 @@ describe('#FBChannel', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -17,10 +17,10 @@ describe('#ContactsAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -16,10 +16,10 @@ describe('#ConversationApi', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -11,10 +11,10 @@ describe('#Reports API', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -24,10 +24,10 @@ describe('#ConversationAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -15,10 +15,10 @@ describe('#ConversationAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -17,10 +17,10 @@ describe('#InboxesAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -18,10 +18,10 @@ describe('#integrationAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -11,10 +11,10 @@ describe('#accountAPI', () => {
describe('createAMeeting', () => { describe('createAMeeting', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {
@ -39,10 +39,10 @@ describe('#accountAPI', () => {
describe('addParticipantToMeeting', () => { describe('addParticipantToMeeting', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -16,10 +16,10 @@ describe('#linearAPI', () => {
describe('getTeams', () => { describe('getTeams', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {
@ -41,10 +41,10 @@ describe('#linearAPI', () => {
describe('getTeamEntities', () => { describe('getTeamEntities', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {
@ -66,10 +66,10 @@ describe('#linearAPI', () => {
describe('createIssue', () => { describe('createIssue', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {
@ -96,10 +96,10 @@ describe('#linearAPI', () => {
describe('link_issue', () => { describe('link_issue', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {
@ -125,10 +125,10 @@ describe('#linearAPI', () => {
describe('getLinkedIssue', () => { describe('getLinkedIssue', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {
@ -150,10 +150,10 @@ describe('#linearAPI', () => {
describe('unlinkIssue', () => { describe('unlinkIssue', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {
@ -178,10 +178,10 @@ describe('#linearAPI', () => {
describe('searchIssues', () => { describe('searchIssues', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -13,10 +13,10 @@ describe('#NotificationAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -20,10 +20,10 @@ describe('#Reports API', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -12,10 +12,10 @@ describe('#SLAReports API', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -16,10 +16,10 @@ describe('#TeamsAPI', () => {
describe('API calls', () => { describe('API calls', () => {
const originalAxios = window.axios; const originalAxios = window.axios;
const axiosMock = { const axiosMock = {
post: jest.fn(() => Promise.resolve()), post: vi.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()), get: vi.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()), patch: vi.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()), delete: vi.fn(() => Promise.resolve()),
}; };
beforeEach(() => { beforeEach(() => {

View File

@ -111,15 +111,6 @@
@updateFolder="onUpdateSavedFilter" @updateFolder="onUpdateSavedFilter"
/> />
</woot-modal> </woot-modal>
<woot-modal
:show.sync="showCustomSnoozeModal"
:on-close="hideCustomSnoozeModal"
>
<custom-snooze-modal
@close="hideCustomSnoozeModal"
@choose-time="chooseSnoozeTime"
/>
</woot-modal>
</div> </div>
</template> </template>
@ -152,10 +143,6 @@ import {
isOnUnattendedView, isOnUnattendedView,
} from '../store/modules/conversations/helpers/actionHelpers'; } from '../store/modules/conversations/helpers/actionHelpers';
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events'; 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'; import IntersectionObserver from './IntersectionObserver.vue';
export default { export default {
@ -170,7 +157,6 @@ export default {
ConversationBulkActions, ConversationBulkActions,
IntersectionObserver, IntersectionObserver,
VirtualList, VirtualList,
CustomSnoozeModal,
}, },
mixins: [ mixins: [
timeMixin, timeMixin,
@ -247,7 +233,6 @@ export default {
root: this.$refs.conversationList, root: this.$refs.conversationList,
rootMargin: '100px 0px 100px 0px', rootMargin: '100px 0px 100px 0px',
}, },
showCustomSnoozeModal: false,
itemComponent: ConversationItem, itemComponent: ConversationItem,
// virtualListExtraProps is to pass the props to the conversationItem component. // virtualListExtraProps is to pass the props to the conversationItem component.
@ -283,7 +268,6 @@ export default {
campaigns: 'campaigns/getAllCampaigns', campaigns: 'campaigns/getAllCampaigns',
labels: 'labels/getLabels', labels: 'labels/getLabels',
selectedConversations: 'bulkActions/getSelectedConversationIds', selectedConversations: 'bulkActions/getSelectedConversationIds',
contextMenuChatId: 'getContextMenuChatId',
}), }),
hasAppliedFilters() { hasAppliedFilters() {
return this.appliedFilters.length !== 0; return this.appliedFilters.length !== 0;
@ -517,11 +501,6 @@ export default {
this.$emitter.on('fetch_conversation_stats', () => { this.$emitter.on('fetch_conversation_stats', () => {
this.$store.dispatch('conversationStats/get', this.conversationFilters); 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: { methods: {
updateVirtualListProps(key, value) { updateVirtualListProps(key, value) {
@ -999,43 +978,6 @@ export default {
onContextMenuToggle(state) { onContextMenuToggle(state) {
this.isContextMenuOpen = 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;
},
}, },
}; };
</script> </script>

View File

@ -20,7 +20,7 @@
:teams="teams" :teams="teams"
:custom-views="customViews" :custom-views="customViews"
:menu-config="activeSecondaryMenu" :menu-config="activeSecondaryMenu"
:current-role="currentRole" :current-user="currentUser"
:is-on-chatwoot-cloud="isOnChatwootCloud" :is-on-chatwoot-cloud="isOnChatwootCloud"
@add-label="showAddLabelPopup" @add-label="showAddLabelPopup"
@toggle-accounts="toggleAccountModal" @toggle-accounts="toggleAccountModal"
@ -37,7 +37,8 @@ import alertMixin from 'shared/mixins/alertMixin';
import PrimarySidebar from './sidebarComponents/Primary.vue'; import PrimarySidebar from './sidebarComponents/Primary.vue';
import SecondarySidebar from './sidebarComponents/Secondary.vue'; import SecondarySidebar from './sidebarComponents/Secondary.vue';
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins'; import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
import router from '../../routes'; import router, { routesWithPermissions } from '../../routes';
import { hasPermissions } from '../../helper/permissionsHelper';
export default { export default {
components: { components: {
@ -98,9 +99,13 @@ export default {
return getSidebarItems(this.accountId); return getSidebarItems(this.accountId);
}, },
primaryMenuItems() { primaryMenuItems() {
const userPermissions = this.currentUser.permissions;
const menuItems = this.sideMenuConfig.primaryMenu; const menuItems = this.sideMenuConfig.primaryMenu;
return menuItems.filter(menuItem => { return menuItems.filter(menuItem => {
const isAvailableForTheUser = menuItem.roles.includes(this.currentRole); const isAvailableForTheUser = hasPermissions(
routesWithPermissions[menuItem.toStateName],
userPermissions
);
if (!isAvailableForTheUser) { if (!isAvailableForTheUser) {
return false; return false;

View File

@ -9,7 +9,6 @@ const primaryMenuItems = accountId => [
featureFlag: FEATURE_FLAGS.INBOX_VIEW, featureFlag: FEATURE_FLAGS.INBOX_VIEW,
toState: frontendURL(`accounts/${accountId}/inbox-view`), toState: frontendURL(`accounts/${accountId}/inbox-view`),
toStateName: 'inbox_view', toStateName: 'inbox_view',
roles: ['administrator', 'agent'],
}, },
{ {
icon: 'chat', icon: 'chat',
@ -17,7 +16,6 @@ const primaryMenuItems = accountId => [
label: 'CONVERSATIONS', label: 'CONVERSATIONS',
toState: frontendURL(`accounts/${accountId}/dashboard`), toState: frontendURL(`accounts/${accountId}/dashboard`),
toStateName: 'home', toStateName: 'home',
roles: ['administrator', 'agent'],
}, },
{ {
icon: 'book-contacts', icon: 'book-contacts',
@ -26,7 +24,6 @@ const primaryMenuItems = accountId => [
featureFlag: FEATURE_FLAGS.CRM, featureFlag: FEATURE_FLAGS.CRM,
toState: frontendURL(`accounts/${accountId}/contacts`), toState: frontendURL(`accounts/${accountId}/contacts`),
toStateName: 'contacts_dashboard', toStateName: 'contacts_dashboard',
roles: ['administrator', 'agent'],
}, },
{ {
icon: 'arrow-trending-lines', icon: 'arrow-trending-lines',
@ -34,8 +31,7 @@ const primaryMenuItems = accountId => [
label: 'REPORTS', label: 'REPORTS',
featureFlag: FEATURE_FLAGS.REPORTS, featureFlag: FEATURE_FLAGS.REPORTS,
toState: frontendURL(`accounts/${accountId}/reports`), toState: frontendURL(`accounts/${accountId}/reports`),
toStateName: 'settings_account_reports', toStateName: 'account_overview_reports',
roles: ['administrator'],
}, },
{ {
icon: 'megaphone', icon: 'megaphone',
@ -44,7 +40,6 @@ const primaryMenuItems = accountId => [
featureFlag: FEATURE_FLAGS.CAMPAIGNS, featureFlag: FEATURE_FLAGS.CAMPAIGNS,
toState: frontendURL(`accounts/${accountId}/campaigns`), toState: frontendURL(`accounts/${accountId}/campaigns`),
toStateName: 'ongoing_campaigns', toStateName: 'ongoing_campaigns',
roles: ['administrator'],
}, },
{ {
icon: 'library', icon: 'library',
@ -54,7 +49,6 @@ const primaryMenuItems = accountId => [
alwaysVisibleOnChatwootInstances: true, alwaysVisibleOnChatwootInstances: true,
toState: frontendURL(`accounts/${accountId}/portals`), toState: frontendURL(`accounts/${accountId}/portals`),
toStateName: 'default_portal_articles', toStateName: 'default_portal_articles',
roles: ['administrator'],
}, },
{ {
icon: 'settings', icon: 'settings',
@ -62,7 +56,6 @@ const primaryMenuItems = accountId => [
label: 'SETTINGS', label: 'SETTINGS',
toState: frontendURL(`accounts/${accountId}/settings`), toState: frontendURL(`accounts/${accountId}/settings`),
toStateName: 'settings_home', toStateName: 'settings_home',
roles: ['administrator', 'agent'],
}, },
]; ];

View File

@ -24,7 +24,6 @@ const settings = accountId => ({
'settings_inbox_list', 'settings_inbox_list',
'settings_inbox_new', 'settings_inbox_new',
'settings_inbox_show', 'settings_inbox_show',
'settings_inbox',
'settings_inboxes_add_agents', 'settings_inboxes_add_agents',
'settings_inboxes_page_channel', 'settings_inboxes_page_channel',
'settings_integrations_dashboard_apps', 'settings_integrations_dashboard_apps',
@ -46,6 +45,9 @@ const settings = accountId => ({
icon: 'briefcase', icon: 'briefcase',
label: 'ACCOUNT_SETTINGS', label: 'ACCOUNT_SETTINGS',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/general`), toState: frontendURL(`accounts/${accountId}/settings/general`),
toStateName: 'general_settings_index', toStateName: 'general_settings_index',
}, },
@ -53,6 +55,9 @@ const settings = accountId => ({
icon: 'people', icon: 'people',
label: 'AGENTS', label: 'AGENTS',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/agents/list`), toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
toStateName: 'agent_list', toStateName: 'agent_list',
featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT, featureFlag: FEATURE_FLAGS.AGENT_MANAGEMENT,
@ -61,6 +66,9 @@ const settings = accountId => ({
icon: 'people-team', icon: 'people-team',
label: 'TEAMS', label: 'TEAMS',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/teams/list`), toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
toStateName: 'settings_teams_list', toStateName: 'settings_teams_list',
featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT, featureFlag: FEATURE_FLAGS.TEAM_MANAGEMENT,
@ -69,6 +77,9 @@ const settings = accountId => ({
icon: 'mail-inbox-all', icon: 'mail-inbox-all',
label: 'INBOXES', label: 'INBOXES',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`), toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
toStateName: 'settings_inbox_list', toStateName: 'settings_inbox_list',
featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT, featureFlag: FEATURE_FLAGS.INBOX_MANAGEMENT,
@ -77,6 +88,9 @@ const settings = accountId => ({
icon: 'tag', icon: 'tag',
label: 'LABELS', label: 'LABELS',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/labels/list`), toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
toStateName: 'labels_list', toStateName: 'labels_list',
featureFlag: FEATURE_FLAGS.LABELS, featureFlag: FEATURE_FLAGS.LABELS,
@ -85,6 +99,9 @@ const settings = accountId => ({
icon: 'code', icon: 'code',
label: 'CUSTOM_ATTRIBUTES', label: 'CUSTOM_ATTRIBUTES',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL( toState: frontendURL(
`accounts/${accountId}/settings/custom-attributes/list` `accounts/${accountId}/settings/custom-attributes/list`
), ),
@ -95,6 +112,9 @@ const settings = accountId => ({
icon: 'automation', icon: 'automation',
label: 'AUTOMATION', label: 'AUTOMATION',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/automation/list`), toState: frontendURL(`accounts/${accountId}/settings/automation/list`),
toStateName: 'automation_list', toStateName: 'automation_list',
featureFlag: FEATURE_FLAGS.AUTOMATIONS, featureFlag: FEATURE_FLAGS.AUTOMATIONS,
@ -103,6 +123,9 @@ const settings = accountId => ({
icon: 'bot', icon: 'bot',
label: 'AGENT_BOTS', label: 'AGENT_BOTS',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
globalConfigFlag: 'csmlEditorHost', globalConfigFlag: 'csmlEditorHost',
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`), toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
toStateName: 'agent_bots', toStateName: 'agent_bots',
@ -112,6 +135,9 @@ const settings = accountId => ({
icon: 'flash-settings', icon: 'flash-settings',
label: 'MACROS', label: 'MACROS',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator', 'agent'],
},
toState: frontendURL(`accounts/${accountId}/settings/macros`), toState: frontendURL(`accounts/${accountId}/settings/macros`),
toStateName: 'macros_wrapper', toStateName: 'macros_wrapper',
featureFlag: FEATURE_FLAGS.MACROS, featureFlag: FEATURE_FLAGS.MACROS,
@ -120,6 +146,9 @@ const settings = accountId => ({
icon: 'chat-multiple', icon: 'chat-multiple',
label: 'CANNED_RESPONSES', label: 'CANNED_RESPONSES',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator', 'agent'],
},
toState: frontendURL( toState: frontendURL(
`accounts/${accountId}/settings/canned-response/list` `accounts/${accountId}/settings/canned-response/list`
), ),
@ -130,6 +159,9 @@ const settings = accountId => ({
icon: 'flash-on', icon: 'flash-on',
label: 'INTEGRATIONS', label: 'INTEGRATIONS',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/integrations`), toState: frontendURL(`accounts/${accountId}/settings/integrations`),
toStateName: 'settings_integrations', toStateName: 'settings_integrations',
featureFlag: FEATURE_FLAGS.INTEGRATIONS, featureFlag: FEATURE_FLAGS.INTEGRATIONS,
@ -138,6 +170,9 @@ const settings = accountId => ({
icon: 'star-emphasis', icon: 'star-emphasis',
label: 'APPLICATIONS', label: 'APPLICATIONS',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/applications`), toState: frontendURL(`accounts/${accountId}/settings/applications`),
toStateName: 'settings_applications', toStateName: 'settings_applications',
featureFlag: FEATURE_FLAGS.INTEGRATIONS, featureFlag: FEATURE_FLAGS.INTEGRATIONS,
@ -146,6 +181,9 @@ const settings = accountId => ({
icon: 'key', icon: 'key',
label: 'AUDIT_LOGS', label: 'AUDIT_LOGS',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/audit-log/list`), toState: frontendURL(`accounts/${accountId}/settings/audit-log/list`),
toStateName: 'auditlogs_list', toStateName: 'auditlogs_list',
isEnterpriseOnly: true, isEnterpriseOnly: true,
@ -156,6 +194,9 @@ const settings = accountId => ({
icon: 'document-list-clock', icon: 'document-list-clock',
label: 'SLA', label: 'SLA',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/sla/list`), toState: frontendURL(`accounts/${accountId}/settings/sla/list`),
toStateName: 'sla_list', toStateName: 'sla_list',
isEnterpriseOnly: true, isEnterpriseOnly: true,
@ -166,6 +207,9 @@ const settings = accountId => ({
icon: 'credit-card-person', icon: 'credit-card-person',
label: 'BILLING', label: 'BILLING',
hasSubMenu: false, hasSubMenu: false,
meta: {
permissions: ['administrator'],
},
toState: frontendURL(`accounts/${accountId}/settings/billing`), toState: frontendURL(`accounts/${accountId}/settings/billing`),
toStateName: 'billing_settings_index', toStateName: 'billing_settings_index',
showOnlyOnCloud: true, showOnlyOnCloud: true,

View File

@ -29,6 +29,8 @@ import SecondaryNavItem from './SecondaryNavItem.vue';
import AccountContext from './AccountContext.vue'; import AccountContext from './AccountContext.vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { FEATURE_FLAGS } from '../../../featureFlags'; import { FEATURE_FLAGS } from '../../../featureFlags';
import { hasPermissions } from '../../../helper/permissionsHelper';
import { routesWithPermissions } from '../../../routes';
export default { export default {
components: { components: {
@ -60,9 +62,9 @@ export default {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
currentRole: { currentUser: {
type: String, type: Object,
default: '', default: () => {},
}, },
isOnChatwootCloud: { isOnChatwootCloud: {
type: Boolean, type: Boolean,
@ -80,16 +82,16 @@ export default {
return this.customViews.filter(view => view.filter_type === 'contact'); return this.customViews.filter(view => view.filter_type === 'contact');
}, },
accessibleMenuItems() { accessibleMenuItems() {
if (!this.currentRole) { const menuItemsFilteredByPermissions = this.menuConfig.menuItems.filter(
return []; menuItem => {
} const { permissions: userPermissions = [] } = this.currentUser;
const menuItemsFilteredByRole = this.menuConfig.menuItems.filter( return hasPermissions(
menuItem => routesWithPermissions[menuItem.toStateName],
window.roleWiseRoutes[this.currentRole].indexOf( userPermissions
menuItem.toStateName );
) > -1 }
); );
return menuItemsFilteredByRole.filter(item => { return menuItemsFilteredByPermissions.filter(item => {
if (item.showOnlyOnCloud) { if (item.showOnlyOnCloud) {
return this.isOnChatwootCloud; return this.isOnChatwootCloud;
} }

View File

@ -65,27 +65,29 @@
:show-child-count="showChildCount(child.count)" :show-child-count="showChildCount(child.count)"
:child-item-count="child.count" :child-item-count="child.count"
/> />
<router-link <Policy :permissions="['administrator']">
v-if="showItem(menuItem)" <router-link
v-slot="{ href, navigate }" v-if="menuItem.newLink"
:to="menuItem.toState" v-slot="{ href, navigate }"
custom :to="menuItem.toState"
> custom
<li class="pl-1"> >
<a :href="href"> <li class="pl-1">
<woot-button <a :href="href">
size="tiny" <woot-button
variant="clear" size="tiny"
color-scheme="secondary" variant="clear"
icon="add" color-scheme="secondary"
:data-testid="menuItem.dataTestid" icon="add"
@click="e => newLinkClick(e, navigate)" :data-testid="menuItem.dataTestid"
> @click="e => newLinkClick(e, navigate)"
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }} >
</woot-button> {{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
</a> </woot-button>
</li> </a>
</router-link> </li>
</router-link>
</Policy>
</ul> </ul>
</li> </li>
</template> </template>
@ -105,9 +107,10 @@ import {
isOnMentionsView, isOnMentionsView,
isOnUnattendedView, isOnUnattendedView,
} from '../../../store/modules/conversations/helpers/actionHelpers'; } from '../../../store/modules/conversations/helpers/actionHelpers';
import Policy from '../../policy.vue';
export default { export default {
components: { SecondaryChildNavItem }, components: { SecondaryChildNavItem, Policy },
mixins: [adminMixin, configMixin], mixins: [adminMixin, configMixin],
props: { props: {
menuItem: { menuItem: {

View File

@ -1,12 +1,12 @@
import AccountSelector from '../AccountSelector'; import AccountSelector from '../AccountSelector.vue';
import { createLocalVue, mount } from '@vue/test-utils'; import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import VueI18n from 'vue-i18n'; import VueI18n from 'vue-i18n';
import i18n from 'dashboard/i18n'; import i18n from 'dashboard/i18n';
import WootModal from 'dashboard/components/Modal'; import WootModal from 'dashboard/components/Modal.vue';
import WootModalHeader from 'dashboard/components/ModalHeader'; import WootModalHeader from 'dashboard/components/ModalHeader.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon'; import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.component('woot-modal', WootModal); localVue.component('woot-modal', WootModal);

View File

@ -1,12 +1,12 @@
import AgentDetails from '../AgentDetails'; import AgentDetails from '../AgentDetails.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import VueI18n from 'vue-i18n'; import VueI18n from 'vue-i18n';
import VTooltip from 'v-tooltip'; import VTooltip from 'v-tooltip';
import i18n from 'dashboard/i18n'; import i18n from 'dashboard/i18n';
import Thumbnail from 'dashboard/components/widgets/Thumbnail'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import WootButton from 'dashboard/components/ui/WootButton'; import WootButton from 'dashboard/components/ui/WootButton.vue';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
localVue.use(VueI18n); localVue.use(VueI18n);

View File

@ -1,13 +1,15 @@
import NotificationBell from '../NotificationBell'; import NotificationBell from '../NotificationBell.vue';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import VueI18n from 'vue-i18n'; import VueI18n from 'vue-i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import i18n from 'dashboard/i18n'; import i18n from 'dashboard/i18n';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
localVue.use(VueI18n); localVue.use(VueI18n);
localVue.component('fluent-icon', FluentIcon);
const i18nConfig = new VueI18n({ const i18nConfig = new VueI18n({
locale: 'en', locale: 'en',
@ -27,7 +29,7 @@ describe('notificationBell', () => {
beforeEach(() => { beforeEach(() => {
actions = { actions = {
showNotification: jest.fn(), showNotification: vi.fn(),
}; };
modules = { modules = {
auth: { auth: {

View File

@ -2,15 +2,21 @@ import AvailabilityStatus from '../AvailabilityStatus.vue';
import { createLocalVue, mount } from '@vue/test-utils'; import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import VueI18n from 'vue-i18n'; 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'; import i18n from 'dashboard/i18n';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VTooltip, {
defaultHtml: false,
});
localVue.use(Vuex); localVue.use(Vuex);
localVue.use(VueI18n); localVue.use(VueI18n);
localVue.component('woot-button', WootButton); 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-menu', WootDropdownMenu);
localVue.component('woot-dropdown-divider', WootDropdownDivider); localVue.component('woot-dropdown-divider', WootDropdownDivider);
localVue.component('woot-dropdown-item', WootDropdownItem); localVue.component('woot-dropdown-item', WootDropdownItem);
localVue.component('fluent-icon', FluentIcon);
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n }); const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
describe('AvailabilityStatus', () => { describe('AvailabilityStatus', () => {
const currentAvailability = 'online'; const currentAvailability = 'online';
const currentAccountId = '1'; const currentAccountId = '1';
const currentUserAutoOffline = false;
let store = null; let store = null;
let actions = null; let actions = null;
let modules = null; let modules = null;
@ -31,7 +39,7 @@ describe('AvailabilityStatus', () => {
beforeEach(() => { beforeEach(() => {
actions = { actions = {
updateAvailability: jest.fn(() => { updateAvailability: vi.fn(() => {
return Promise.resolve(); return Promise.resolve();
}), }),
}; };
@ -41,6 +49,7 @@ describe('AvailabilityStatus', () => {
getters: { getters: {
getCurrentUserAvailability: () => currentAvailability, getCurrentUserAvailability: () => currentAvailability,
getCurrentAccountId: () => currentAccountId, getCurrentAccountId: () => currentAccountId,
getCurrentUserAutoOffline: () => currentUserAutoOffline,
}, },
}, },
}; };

View File

@ -0,0 +1,23 @@
<script setup>
import { useStoreGetters } from 'dashboard/composables/store';
import { computed } from 'vue';
import { hasPermissions } from '../helper/permissionsHelper';
const props = defineProps({
permissions: {
type: Array,
required: true,
},
});
const getters = useStoreGetters();
const user = getters.getCurrentUser.value;
const hasPermission = computed(() =>
hasPermissions(props.permissions, user.permissions)
);
</script>
<template>
<div v-if="hasPermission">
<slot />
</div>
</template>

View File

@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import SidemenuIcon from '../SidemenuIcon'; import SidemenuIcon from '../SidemenuIcon.vue';
describe('SidemenuIcon', () => { describe('SidemenuIcon', () => {
test('matches snapshot', () => { test('matches snapshot', () => {

View File

@ -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`] = `
<button <button
class="-ml-3 text-black-900 dark:text-slate-300" class="-ml-3 text-black-900 dark:text-slate-300"
color-scheme="secondary" color-scheme="secondary"

View File

@ -18,11 +18,11 @@ describe('Date formatting functions', () => {
const testDate = new Date(2020, 4, 15); // May 15, 2020 const testDate = new Date(2020, 4, 15); // May 15, 2020
beforeEach(() => { beforeEach(() => {
jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-US'); vi.spyOn(navigator, 'language', 'get').mockReturnValue('en-US');
}); });
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); vi.restoreAllMocks();
}); });
it('returns the correct month name from a date', () => { it('returns the correct month name from a date', () => {
@ -39,7 +39,7 @@ describe('Date formatting functions', () => {
}); });
it('returns the correct date format for the current locale en-IN', () => { it('returns the correct date format for the current locale en-IN', () => {
jest.spyOn(navigator, 'language', 'get').mockReturnValue('en-IN'); vi.spyOn(navigator, 'language', 'get').mockReturnValue('en-IN');
const expected = 'dd/MM/yyyy'; const expected = 'dd/MM/yyyy';
expect(getIntlDateFormatForLocale()).toBe(expected); expect(getIntlDateFormatForLocale()).toBe(expected);
}); });
@ -231,13 +231,13 @@ describe('isHoveringNextDayInRange', () => {
describe('getActiveDateRange', () => { describe('getActiveDateRange', () => {
const currentDate = new Date(2020, 5, 15, 12, 0); // May 15, 2020, at noon const currentDate = new Date(2020, 5, 15, 12, 0); // May 15, 2020, at noon
beforeAll(() => { beforeEach(() => {
// Mocking the current date to ensure consistency in tests // Mocking the current date to ensure consistency in tests
jest.useFakeTimers().setSystemTime(currentDate.getTime()); vi.useFakeTimers().setSystemTime(currentDate.getTime());
}); });
afterAll(() => { afterEach(() => {
jest.useRealTimers(); vi.useRealTimers();
}); });
it('returns the correct range for "last7days"', () => { it('returns the correct range for "last7days"', () => {

View File

@ -127,7 +127,7 @@
</div> </div>
<div <div
v-if="shouldShowContextMenu" v-if="shouldShowContextMenu"
class="context-menu-wrap invisible group-hover:visible" class="invisible context-menu-wrap group-hover:visible"
> >
<context-menu <context-menu
v-if="isBubble && !isMessageDeleted" v-if="isBubble && !isMessageDeleted"
@ -166,6 +166,7 @@ import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage'; import { LocalStorage } from 'shared/helpers/localStorage';
import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper'; import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper';
import * as Sentry from '@sentry/browser';
export default { export default {
components: { components: {
@ -502,15 +503,29 @@ export default {
}, },
hasMediaAttachment(type) { hasMediaAttachment(type) {
if (this.hasAttachments && this.data.attachments.length > 0) { if (this.hasAttachments && this.data.attachments.length > 0) {
const { attachments = [{}] } = this.data; return this.compareMessageFileType(this.data, type);
const { file_type: fileType } = attachments[0];
return fileType === type && !this.hasMediaLoadError;
} }
if (this.storyReply) { if (this.storyReply) {
return true; return true;
} }
return false; return false;
}, },
compareMessageFileType(messageData, type) {
try {
const { attachments = [{}] } = messageData;
const { file_type: fileType } = attachments[0];
return fileType === type && !this.hasMediaLoadError;
} catch (err) {
Sentry.setContext('attachment-parsing-error', {
messageData,
type,
hasMediaLoadError: this.hasMediaLoadError,
});
Sentry.captureException(err);
return false;
}
},
handleContextMenuClick() { handleContextMenuClick() {
this.showContextMenu = !this.showContextMenu; this.showContextMenu = !this.showContextMenu;
}, },

View File

@ -197,7 +197,7 @@ import {
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage'; import { LocalStorage } from 'shared/helpers/localStorage';
const EmojiInput = () => import('shared/components/emoji/EmojiInput'); const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
export default { export default {
components: { components: {

View File

@ -2,16 +2,16 @@ import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import VueI18n from 'vue-i18n'; import VueI18n from 'vue-i18n';
import VTooltip from 'v-tooltip'; import VTooltip from 'v-tooltip';
import Button from 'dashboard/components/buttons/Button'; import Button from 'dashboard/components/buttons/Button.vue';
import i18n from 'dashboard/i18n'; import i18n from 'dashboard/i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon'; import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
import MoreActions from '../MoreActions'; import MoreActions from '../MoreActions.vue';
jest.mock('shared/helpers/mitt', () => ({ vi.mock('shared/helpers/mitt', () => ({
emitter: { emitter: {
emit: jest.fn(), emit: vi.fn(),
on: jest.fn(), on: vi.fn(),
off: jest.fn(), off: vi.fn(),
}, },
})); }));
@ -26,9 +26,9 @@ localVue.component('fluent-icon', FluentIcon);
localVue.component('woot-button', Button); localVue.component('woot-button', Button);
localVue.prototype.$emitter = { localVue.prototype.$emitter = {
emit: jest.fn(), emit: vi.fn(),
on: jest.fn(), on: vi.fn(),
off: jest.fn(), off: vi.fn(),
}; };
const i18nConfig = new VueI18n({ locale: 'en', messages: i18n }); const i18nConfig = new VueI18n({ locale: 'en', messages: i18n });
@ -49,8 +49,8 @@ describe('MoveActions', () => {
currentChat, currentChat,
}; };
muteConversation = jest.fn(() => Promise.resolve()); muteConversation = vi.fn(() => Promise.resolve());
unmuteConversation = jest.fn(() => Promise.resolve()); unmuteConversation = vi.fn(() => Promise.resolve());
modules = { modules = {
conversations: { actions: { muteConversation, unmuteConversation } }, conversations: { actions: { muteConversation, unmuteConversation } },

View File

@ -8,7 +8,7 @@ const buildComponent = ({ data = {}, methods = {} }) => ({
data() { data() {
return { ...data, selectedIndex: 0, items: [1, 2, 3] }; return { ...data, selectedIndex: 0, items: [1, 2, 3] };
}, },
methods: { ...methods, onSelect: jest.fn(), adjustScroll: jest.fn() }, methods: { ...methods, onSelect: vi.fn(), adjustScroll: vi.fn() },
mixins: [keyboardEventListenerMixins], mixins: [keyboardEventListenerMixins],
}); });
@ -21,7 +21,7 @@ describe('mentionSelectionKeyboardMixin', () => {
}); });
it('ArrowUp and Control+KeyP update selectedIndex correctly', () => { it('ArrowUp and Control+KeyP update selectedIndex correctly', () => {
const preventDefault = jest.fn(); const preventDefault = vi.fn();
const keyboardEvents = wrapper.vm.getKeyboardEvents(); const keyboardEvents = wrapper.vm.getKeyboardEvents();
if (keyboardEvents && keyboardEvents.ArrowUp) { if (keyboardEvents && keyboardEvents.ArrowUp) {
@ -39,7 +39,7 @@ describe('mentionSelectionKeyboardMixin', () => {
}); });
it('ArrowDown and Control+KeyN update selectedIndex correctly', () => { it('ArrowDown and Control+KeyN update selectedIndex correctly', () => {
const preventDefault = jest.fn(); const preventDefault = vi.fn();
const keyboardEvents = wrapper.vm.getKeyboardEvents(); const keyboardEvents = wrapper.vm.getKeyboardEvents();
if (keyboardEvents && keyboardEvents.ArrowDown) { if (keyboardEvents && keyboardEvents.ArrowDown) {
@ -57,7 +57,7 @@ describe('mentionSelectionKeyboardMixin', () => {
}); });
it('Enter key triggers onSelect method', () => { it('Enter key triggers onSelect method', () => {
const preventDefault = jest.fn(); const preventDefault = vi.fn();
const keyboardEvents = wrapper.vm.getKeyboardEvents(); const keyboardEvents = wrapper.vm.getKeyboardEvents();
if (keyboardEvents && keyboardEvents.Enter) { if (keyboardEvents && keyboardEvents.Enter) {

View File

@ -2,17 +2,17 @@ import { shallowMount } from '@vue/test-utils';
import { emitter } from 'shared/helpers/mitt'; import { emitter } from 'shared/helpers/mitt';
import { useEmitter } from '../emitter'; import { useEmitter } from '../emitter';
jest.mock('shared/helpers/mitt', () => ({ vi.mock('shared/helpers/mitt', () => ({
emitter: { emitter: {
on: jest.fn(), on: vi.fn(),
off: jest.fn(), off: vi.fn(),
}, },
})); }));
describe('useEmitter', () => { describe('useEmitter', () => {
let wrapper; let wrapper;
const eventName = 'my-event'; const eventName = 'my-event';
const callback = jest.fn(); const callback = vi.fn();
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount({ wrapper = shallowMount({
@ -29,10 +29,6 @@ describe('useEmitter', () => {
}); });
}); });
afterEach(() => {
jest.resetAllMocks();
});
it('should add an event listener on mount', () => { it('should add an event listener on mount', () => {
expect(emitter.on).toHaveBeenCalledWith(eventName, callback); expect(emitter.on).toHaveBeenCalledWith(eventName, callback);
}); });

View File

@ -38,9 +38,10 @@ export class AnalyticsHelper {
* @param {Object} user - User object * @param {Object} user - User object
*/ */
identify(user) { identify(user) {
if (!this.analytics) { if (!this.analytics || !user) {
return; return;
} }
this.user = user; this.user = user;
this.analytics.identify(this.user.email, { this.analytics.identify(this.user.email, {
userId: this.user.id, userId: this.user.id,

View File

@ -1,13 +1,13 @@
import helperObject, { AnalyticsHelper } from '../'; import helperObject, { AnalyticsHelper } from '../';
jest.mock('@june-so/analytics-next', () => ({ vi.mock('@june-so/analytics-next', () => ({
AnalyticsBrowser: { AnalyticsBrowser: {
load: () => [ load: () => [
{ {
identify: jest.fn(), identify: vi.fn(),
track: jest.fn(), track: vi.fn(),
page: jest.fn(), page: vi.fn(),
group: jest.fn(), group: vi.fn(),
}, },
], ],
}, },
@ -40,7 +40,7 @@ describe('AnalyticsHelper', () => {
describe('identify', () => { describe('identify', () => {
beforeEach(() => { beforeEach(() => {
analyticsHelper.analytics = { identify: jest.fn(), group: jest.fn() }; analyticsHelper.analytics = { identify: vi.fn(), group: vi.fn() };
}); });
it('should call identify on analytics browser with correct arguments', () => { it('should call identify on analytics browser with correct arguments', () => {
@ -87,7 +87,7 @@ describe('AnalyticsHelper', () => {
describe('track', () => { describe('track', () => {
beforeEach(() => { beforeEach(() => {
analyticsHelper.analytics = { track: jest.fn() }; analyticsHelper.analytics = { track: vi.fn() };
analyticsHelper.user = { id: '123' }; analyticsHelper.user = { id: '123' };
}); });
@ -118,7 +118,7 @@ describe('AnalyticsHelper', () => {
describe('page', () => { describe('page', () => {
beforeEach(() => { beforeEach(() => {
analyticsHelper.analytics = { page: jest.fn() }; analyticsHelper.analytics = { page: vi.fn() };
}); });
it('should call the analytics.page method with the correct arguments', () => { it('should call the analytics.page method with the correct arguments', () => {

View File

@ -2,34 +2,35 @@ import Vue from 'vue';
import plugin from '../plugin'; import plugin from '../plugin';
import analyticsHelper from '../index'; import analyticsHelper from '../index';
vi.spyOn(analyticsHelper, 'init');
vi.spyOn(analyticsHelper, 'track');
describe('Vue Analytics Plugin', () => { describe('Vue Analytics Plugin', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(analyticsHelper, 'init');
jest.spyOn(analyticsHelper, 'track');
Vue.use(plugin); Vue.use(plugin);
}); });
afterEach(() => { it('should call the init method on analyticsHelper once during plugin installation', () => {
jest.resetModules(); expect(analyticsHelper.init).toHaveBeenCalledTimes(1);
jest.clearAllMocks();
}); });
it('should call the init method on the analyticsHelper', () => { it('should add the analyticsHelper to the Vue prototype as $analytics', () => {
expect(analyticsHelper.init).toHaveBeenCalled();
});
it('should add the analyticsHelper to the Vue prototype', () => {
expect(Vue.prototype.$analytics).toBe(analyticsHelper); expect(Vue.prototype.$analytics).toBe(analyticsHelper);
}); });
it('should add the track method to the Vue prototype', () => { it('should add a track method to the Vue prototype as $track', () => {
expect(typeof Vue.prototype.$track).toBe('function'); expect(typeof Vue.prototype.$track).toBe('function');
Vue.prototype.$track('eventName'); Vue.prototype.$track('eventName');
expect(analyticsHelper.track).toHaveBeenCalledWith('eventName'); expect(analyticsHelper.track)
.toHaveBeenCalledTimes(1)
.toHaveBeenCalledWith('eventName');
}); });
it('should call the track method on the analyticsHelper when $track is called', () => { it('should call the track method on analyticsHelper with the correct event name when $track is called', () => {
Vue.prototype.$track('eventName'); const eventName = 'testEvent';
expect(analyticsHelper.track).toHaveBeenCalledWith('eventName'); Vue.prototype.$track(eventName);
expect(analyticsHelper.track)
.toHaveBeenCalledTimes(1)
.toHaveBeenCalledWith(eventName);
}); });
}); });

View File

@ -52,6 +52,10 @@ class DashboardAudioNotificationHelper {
}; };
executeRecurringNotification = () => { executeRecurringNotification = () => {
if (!window.WOOT || !window.WOOT.$store) {
this.clearSetTimeout();
return;
}
const mineConversation = window.WOOT.$store.getters.getMineChats({ const mineConversation = window.WOOT.$store.getters.getMineChats({
assigneeType: 'me', assigneeType: 'me',
status: 'open', status: 'open',

View File

@ -9,6 +9,11 @@ import {
const MAX_DISCONNECT_SECONDS = 10800; const MAX_DISCONNECT_SECONDS = 10800;
// The disconnect delay threshold is added to account for delays in identifying
// disconnections (for example, the websocket disconnection takes up to 3 seconds)
// while fetching the latest updated conversations or messages.
const DISCONNECT_DELAY_THRESHOLD = 15;
class ReconnectService { class ReconnectService {
constructor(store, router) { constructor(store, router) {
this.store = store; this.store = store;
@ -47,7 +52,8 @@ class ReconnectService {
fetchConversations = async () => { fetchConversations = async () => {
await this.store.dispatch('updateChatListFilters', { await this.store.dispatch('updateChatListFilters', {
page: null, page: null,
updatedWithin: this.getSecondsSinceDisconnect(), updatedWithin:
this.getSecondsSinceDisconnect() + DISCONNECT_DELAY_THRESHOLD,
}); });
await this.store.dispatch('fetchAllConversations'); await this.store.dispatch('fetchAllConversations');
// Reset the updatedWithin in the store chat list filter after fetching conversations when the user is reconnected // Reset the updatedWithin in the store chat list filter after fetching conversations when the user is reconnected

View File

@ -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;
}, {});

View File

@ -1,19 +1,16 @@
import { hasPermissions } from './permissionsHelper';
// eslint-disable-next-line default-param-last // eslint-disable-next-line default-param-last
export const getCurrentAccount = ({ accounts } = {}, accountId) => { export const getCurrentAccount = ({ accounts } = {}, accountId) => {
return accounts.find(account => account.id === accountId); return accounts.find(account => account.id === accountId);
}; };
// eslint-disable-next-line default-param-last export const routeIsAccessibleFor = (route, userPermissions = []) => {
export const getUserRole = ({ accounts } = {}, accountId) => { const { meta: { permissions: routePermissions = [] } = {} } = route;
const currentAccount = getCurrentAccount({ accounts }, accountId) || {}; return hasPermissions(routePermissions, userPermissions);
return currentAccount.role || null;
}; };
export const routeIsAccessibleFor = (route, role, roleWiseRoutes) => { const validateActiveAccountRoutes = (to, user) => {
return roleWiseRoutes[role].includes(route);
};
const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => {
// If the current account is active, then check for the route permissions // If the current account is active, then check for the route permissions
const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`; const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`;
@ -22,15 +19,13 @@ const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => {
return accountDashboardURL; return accountDashboardURL;
} }
const userRole = getUserRole(user, Number(to.params.accountId)); const isAccessible = routeIsAccessibleFor(to, user.permissions);
const isAccessible = routeIsAccessibleFor(to.name, userRole, roleWiseRoutes);
// If the route is not accessible for the user, return to dashboard screen // If the route is not accessible for the user, return to dashboard screen
return isAccessible ? null : accountDashboardURL; return isAccessible ? null : accountDashboardURL;
}; };
export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => { export const validateLoggedInRoutes = (to, user) => {
const currentAccount = getCurrentAccount(user, Number(to.params.accountId)); const currentAccount = getCurrentAccount(user, Number(to.params.accountId));
// If current account is missing, either user does not have // If current account is missing, either user does not have
// access to the account or the account is deleted, return to login screen // access to the account or the account is deleted, return to login screen
if (!currentAccount) { if (!currentAccount) {
@ -40,7 +35,7 @@ export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => {
const isCurrentAccountActive = currentAccount.status === 'active'; const isCurrentAccountActive = currentAccount.status === 'active';
if (isCurrentAccountActive) { 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 // If the current account is not active, then redirect the user to the suspended screen

View File

@ -4,7 +4,7 @@ describe('DataManager', () => {
const accountId = 'test-account'; const accountId = 'test-account';
let dataManager; let dataManager;
beforeAll(async () => { beforeEach(async () => {
dataManager = new DataManager(accountId); dataManager = new DataManager(accountId);
await dataManager.initDb(); await dataManager.initDb();
}); });

View File

@ -8,26 +8,26 @@ import {
} from 'dashboard/helper/routeHelpers'; } from 'dashboard/helper/routeHelpers';
import ReconnectService from 'dashboard/helper/ReconnectService'; import ReconnectService from 'dashboard/helper/ReconnectService';
jest.mock('shared/helpers/mitt', () => ({ vi.mock('shared/helpers/mitt', () => ({
emitter: { emitter: {
on: jest.fn(), on: vi.fn(),
off: jest.fn(), off: vi.fn(),
emit: jest.fn(), emit: vi.fn(),
}, },
})); }));
jest.mock('date-fns', () => ({ vi.mock('date-fns', () => ({
differenceInSeconds: jest.fn(), differenceInSeconds: vi.fn(),
})); }));
jest.mock('dashboard/helper/routeHelpers', () => ({ vi.mock('dashboard/helper/routeHelpers', () => ({
isAConversationRoute: jest.fn(), isAConversationRoute: vi.fn(),
isAInboxViewRoute: jest.fn(), isAInboxViewRoute: vi.fn(),
isNotificationRoute: jest.fn(), isNotificationRoute: vi.fn(),
})); }));
const storeMock = { const storeMock = {
dispatch: jest.fn(), dispatch: vi.fn(),
getters: { getters: {
getAppliedConversationFiltersQuery: [], getAppliedConversationFiltersQuery: [],
'customViews/getActiveConversationFolder': { query: {} }, 'customViews/getActiveConversationFolder': { query: {} },
@ -46,17 +46,17 @@ describe('ReconnectService', () => {
let reconnectService; let reconnectService;
beforeEach(() => { beforeEach(() => {
window.addEventListener = jest.fn(); window.addEventListener = vi.fn();
window.removeEventListener = jest.fn(); window.removeEventListener = vi.fn();
Object.defineProperty(window, 'location', { Object.defineProperty(window, 'location', {
configurable: true, configurable: true,
value: { reload: jest.fn() }, value: { reload: vi.fn() },
}); });
reconnectService = new ReconnectService(storeMock, routerMock); reconnectService = new ReconnectService(storeMock, routerMock);
}); });
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); vi.clearAllMocks();
}); });
describe('constructor', () => { describe('constructor', () => {
@ -102,7 +102,7 @@ describe('ReconnectService', () => {
expect(reconnectService.getSecondsSinceDisconnect()).toBe(0); expect(reconnectService.getSecondsSinceDisconnect()).toBe(0);
}); });
it('should return the number of seconds since disconnect', () => { it('should return the number of seconds + threshold since disconnect', () => {
reconnectService.disconnectTime = new Date(); reconnectService.disconnectTime = new Date();
differenceInSeconds.mockReturnValue(100); differenceInSeconds.mockReturnValue(100);
expect(reconnectService.getSecondsSinceDisconnect()).toBe(100); expect(reconnectService.getSecondsSinceDisconnect()).toBe(100);
@ -111,7 +111,7 @@ describe('ReconnectService', () => {
describe('handleOnlineEvent', () => { describe('handleOnlineEvent', () => {
it('should reload the page if disconnected for more than 3 hours', () => { it('should reload the page if disconnected for more than 3 hours', () => {
reconnectService.getSecondsSinceDisconnect = jest reconnectService.getSecondsSinceDisconnect = vi
.fn() .fn()
.mockReturnValue(10801); .mockReturnValue(10801);
reconnectService.handleOnlineEvent(); reconnectService.handleOnlineEvent();
@ -119,7 +119,7 @@ describe('ReconnectService', () => {
}); });
it('should not reload the page if disconnected for less than 3 hours', () => { it('should not reload the page if disconnected for less than 3 hours', () => {
reconnectService.getSecondsSinceDisconnect = jest reconnectService.getSecondsSinceDisconnect = vi
.fn() .fn()
.mockReturnValue(10799); .mockReturnValue(10799);
reconnectService.handleOnlineEvent(); reconnectService.handleOnlineEvent();
@ -128,22 +128,27 @@ describe('ReconnectService', () => {
}); });
describe('fetchConversations', () => { describe('fetchConversations', () => {
it('should dispatch updateChatListFilters and fetchAllConversations', async () => { it('should update the filters with disconnected time and the threshold', async () => {
reconnectService.getSecondsSinceDisconnect = jest reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
.fn()
.mockReturnValue(100);
await reconnectService.fetchConversations(); await reconnectService.fetchConversations();
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', { expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
page: null, page: null,
updatedWithin: 100, updatedWithin: 115,
});
});
it('should dispatch updateChatListFilters and fetchAllConversations', async () => {
reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
await reconnectService.fetchConversations();
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
page: null,
updatedWithin: 115,
}); });
expect(storeMock.dispatch).toHaveBeenCalledWith('fetchAllConversations'); expect(storeMock.dispatch).toHaveBeenCalledWith('fetchAllConversations');
}); });
it('should dispatch updateChatListFilters and reset updatedWithin', async () => { it('should dispatch updateChatListFilters and reset updatedWithin', async () => {
reconnectService.getSecondsSinceDisconnect = jest reconnectService.getSecondsSinceDisconnect = vi.fn().mockReturnValue(100);
.fn()
.mockReturnValue(100);
await reconnectService.fetchConversations(); await reconnectService.fetchConversations();
expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', { expect(storeMock.dispatch).toHaveBeenCalledWith('updateChatListFilters', {
updatedWithin: null, updatedWithin: null,
@ -173,7 +178,7 @@ describe('ReconnectService', () => {
}, },
], ],
}; };
const spy = jest.spyOn( const spy = vi.spyOn(
reconnectService, reconnectService,
'fetchFilteredOrSavedConversations' 'fetchFilteredOrSavedConversations'
); );
@ -191,7 +196,7 @@ describe('ReconnectService', () => {
query: null, query: null,
}; };
const spy = jest.spyOn(reconnectService, 'fetchConversations'); const spy = vi.spyOn(reconnectService, 'fetchConversations');
await reconnectService.fetchConversationsOnReconnect(); await reconnectService.fetchConversationsOnReconnect();
@ -204,7 +209,7 @@ describe('ReconnectService', () => {
query: { test: 'activeFolderQuery' }, query: { test: 'activeFolderQuery' },
}; };
const spy = jest.spyOn( const spy = vi.spyOn(
reconnectService, reconnectService,
'fetchFilteredOrSavedConversations' 'fetchFilteredOrSavedConversations'
); );
@ -270,11 +275,11 @@ describe('ReconnectService', () => {
describe('handleRouteSpecificFetch', () => { describe('handleRouteSpecificFetch', () => {
it('should fetch conversations and messages if current route is a conversation route', async () => { it('should fetch conversations and messages if current route is a conversation route', async () => {
isAConversationRoute.mockReturnValue(true); isAConversationRoute.mockReturnValue(true);
const spyConversations = jest.spyOn( const spyConversations = vi.spyOn(
reconnectService, reconnectService,
'fetchConversationsOnReconnect' 'fetchConversationsOnReconnect'
); );
const spyMessages = jest.spyOn( const spyMessages = vi.spyOn(
reconnectService, reconnectService,
'fetchConversationMessagesOnReconnect' 'fetchConversationMessagesOnReconnect'
); );
@ -285,14 +290,14 @@ describe('ReconnectService', () => {
it('should fetch notifications if current route is an inbox view route', async () => { it('should fetch notifications if current route is an inbox view route', async () => {
isAInboxViewRoute.mockReturnValue(true); isAInboxViewRoute.mockReturnValue(true);
const spy = jest.spyOn(reconnectService, 'fetchNotificationsOnReconnect'); const spy = vi.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
await reconnectService.handleRouteSpecificFetch(); await reconnectService.handleRouteSpecificFetch();
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
it('should fetch notifications if current route is a notification route', async () => { it('should fetch notifications if current route is a notification route', async () => {
isNotificationRoute.mockReturnValue(true); isNotificationRoute.mockReturnValue(true);
const spy = jest.spyOn(reconnectService, 'fetchNotificationsOnReconnect'); const spy = vi.spyOn(reconnectService, 'fetchNotificationsOnReconnect');
await reconnectService.handleRouteSpecificFetch(); await reconnectService.handleRouteSpecificFetch();
expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalled();
}); });
@ -320,7 +325,7 @@ describe('ReconnectService', () => {
describe('onDisconnect', () => { describe('onDisconnect', () => {
it('should set disconnectTime and call setConversationLastMessageId', () => { it('should set disconnectTime and call setConversationLastMessageId', () => {
reconnectService.setConversationLastMessageId = jest.fn(); reconnectService.setConversationLastMessageId = vi.fn();
reconnectService.onDisconnect(); reconnectService.onDisconnect();
expect(reconnectService.disconnectTime).toBeInstanceOf(Date); expect(reconnectService.disconnectTime).toBeInstanceOf(Date);
expect(reconnectService.setConversationLastMessageId).toHaveBeenCalled(); expect(reconnectService.setConversationLastMessageId).toHaveBeenCalled();
@ -329,8 +334,8 @@ describe('ReconnectService', () => {
describe('onReconnect', () => { describe('onReconnect', () => {
it('should handle route-specific fetch, revalidate caches, and emit WEBSOCKET_RECONNECT_COMPLETED event', async () => { it('should handle route-specific fetch, revalidate caches, and emit WEBSOCKET_RECONNECT_COMPLETED event', async () => {
reconnectService.handleRouteSpecificFetch = jest.fn(); reconnectService.handleRouteSpecificFetch = vi.fn();
reconnectService.revalidateCaches = jest.fn(); reconnectService.revalidateCaches = vi.fn();
await reconnectService.onReconnect(); await reconnectService.onReconnect();
expect(reconnectService.handleRouteSpecificFetch).toHaveBeenCalled(); expect(reconnectService.handleRouteSpecificFetch).toHaveBeenCalled();
expect(reconnectService.revalidateCaches).toHaveBeenCalled(); expect(reconnectService.revalidateCaches).toHaveBeenCalled();

View File

@ -36,16 +36,14 @@ describe('#createPendingMessage', () => {
message: 'hi', message: 'hi',
}; };
it('returns the pending message with expected new keys', () => { it('returns the pending message with expected new keys', () => {
expect(createPendingMessage(message)).toHaveProperty( expect(createPendingMessage(message)).toMatchObject({
'content', content: expect.anything(),
'id', id: expect.anything(),
'status', status: expect.anything(),
'echo_id', echo_id: expect.anything(),
'status', created_at: expect.anything(),
'created_at', message_type: expect.anything(),
'message_type', });
'conversation_id'
);
}); });
it('returns the pending message with status progress', () => { it('returns the pending message with status progress', () => {
@ -61,23 +59,20 @@ describe('#createPendingMessage', () => {
}); });
}); });
it('returns the pending message with attachmnet key if file is passed', () => { it('returns the pending message with attachment key if file is passed', () => {
const messageWithFile = { const messageWithFile = {
message: 'hi', message: 'hi',
file: {}, file: {},
}; };
expect(createPendingMessage(messageWithFile)).toHaveProperty( expect(createPendingMessage(messageWithFile)).toMatchObject({
'content', content: expect.anything(),
'id', id: expect.anything(),
'status', status: expect.anything(),
'echo_id', echo_id: expect.anything(),
'status', created_at: expect.anything(),
'created_at', message_type: expect.anything(),
'message_type', attachments: [{ id: expect.anything() }],
'conversation_id', });
'attachments',
'private'
);
}); });
it('returns the pending message to have one attachment', () => { it('returns the pending message to have one attachment', () => {

View File

@ -19,19 +19,19 @@ describe('resize directive', () => {
beforeEach(() => { beforeEach(() => {
el = document.createElement('div'); el = document.createElement('div');
binding = { binding = {
value: jest.fn(), value: vi.fn(),
}; };
observer = { observer = {
observe: jest.fn(), observe: vi.fn(),
unobserve: jest.fn(), unobserve: vi.fn(),
disconnect: jest.fn(), disconnect: vi.fn(),
}; };
window.ResizeObserver = ResizeObserverMock; window.ResizeObserver = ResizeObserverMock;
jest.spyOn(window, 'ResizeObserver').mockImplementation(() => observer); vi.spyOn(window, 'ResizeObserver').mockImplementation(() => observer);
}); });
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); vi.clearAllMocks();
}); });
it('should create ResizeObserver on bind', () => { it('should create ResizeObserver on bind', () => {
@ -44,7 +44,7 @@ describe('resize directive', () => {
it('should call callback on observer callback', () => { it('should call callback on observer callback', () => {
el = document.createElement('div'); el = document.createElement('div');
binding = { binding = {
value: jest.fn(), value: vi.fn(),
}; };
resize.bind(el, binding); resize.bind(el, binding);

View File

@ -283,17 +283,17 @@ describe('findNodeToInsertImage', () => {
mockEditorState = { mockEditorState = {
selection: { selection: {
$from: { $from: {
node: jest.fn(() => ({})), node: vi.fn(() => ({})),
}, },
from: 0, from: 0,
}, },
schema: { schema: {
nodes: { nodes: {
image: { image: {
create: jest.fn(attrs => ({ type: { name: 'image' }, attrs })), create: vi.fn(attrs => ({ type: { name: 'image' }, attrs })),
}, },
paragraph: { paragraph: {
create: jest.fn((_, node) => ({ create: vi.fn((_, node) => ({
type: { name: 'paragraph' }, type: { name: 'paragraph' },
content: [node], content: [node],
})), })),
@ -381,11 +381,11 @@ describe('setURLWithQueryAndSize', () => {
beforeEach(() => { beforeEach(() => {
selectedNode = { selectedNode = {
setAttribute: jest.fn(), setAttribute: vi.fn(),
}; };
const tr = { const tr = {
setNodeMarkup: jest.fn().mockReturnValue({ setNodeMarkup: vi.fn().mockReturnValue({
docChanged: true, docChanged: true,
}), }),
}; };
@ -397,7 +397,7 @@ describe('setURLWithQueryAndSize', () => {
editorView = { editorView = {
state, state,
dispatch: jest.fn(), dispatch: vi.fn(),
}; };
}); });
@ -420,7 +420,7 @@ describe('setURLWithQueryAndSize', () => {
}); });
it('does not update the editor view if the document has not changed', () => { it('does not update the editor view if the document has not changed', () => {
editorView.state.tr.setNodeMarkup = jest.fn().mockReturnValue({ editorView.state.tr.setNodeMarkup = vi.fn().mockReturnValue({
docChanged: false, docChanged: false,
}); });

View File

@ -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");
});
});

View File

@ -1,7 +1,6 @@
import { import {
getConversationDashboardRoute, getConversationDashboardRoute,
getCurrentAccount, getCurrentAccount,
getUserRole,
isAConversationRoute, isAConversationRoute,
routeIsAccessibleFor, routeIsAccessibleFor,
validateLoggedInRoutes, 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', () => { describe('#routeIsAccessibleFor', () => {
it('should return the correct access', () => { it('should return the correct access', () => {
const roleWiseRoutes = { agent: ['conversations'], admin: ['billing'] }; let route = { meta: { permissions: ['administrator'] } };
expect(routeIsAccessibleFor('billing', 'agent', roleWiseRoutes)).toEqual( expect(routeIsAccessibleFor(route, ['agent'])).toEqual(false);
false expect(routeIsAccessibleFor(route, ['administrator'])).toEqual(true);
);
expect(routeIsAccessibleFor('billing', 'admin', roleWiseRoutes)).toEqual(
true
);
}); });
}); });
@ -40,11 +26,7 @@ describe('#validateLoggedInRoutes', () => {
describe('when account access is missing', () => { describe('when account access is missing', () => {
it('should return the login route', () => { it('should return the login route', () => {
expect( expect(
validateLoggedInRoutes( validateLoggedInRoutes({ params: { accountId: 1 } }, { accounts: [] })
{ params: { accountId: 1 } },
{ accounts: [] },
{}
)
).toEqual(`app/login`); ).toEqual(`app/login`);
}); });
}); });
@ -53,9 +35,12 @@ describe('#validateLoggedInRoutes', () => {
it('return suspended route', () => { it('return suspended route', () => {
expect( expect(
validateLoggedInRoutes( validateLoggedInRoutes(
{ name: 'conversations', params: { accountId: 1 } }, {
{ accounts: [{ id: 1, role: 'agent', status: 'suspended' }] }, name: 'conversations',
{ agent: ['conversations'] } params: { accountId: 1 },
meta: { permissions: ['agent'] },
},
{ accounts: [{ id: 1, role: 'agent', status: 'suspended' }] }
) )
).toEqual(`accounts/1/suspended`); ).toEqual(`accounts/1/suspended`);
}); });
@ -65,9 +50,22 @@ describe('#validateLoggedInRoutes', () => {
it('returns null (no action required)', () => { it('returns null (no action required)', () => {
expect( expect(
validateLoggedInRoutes( validateLoggedInRoutes(
{ name: 'conversations', params: { accountId: 1 } }, {
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }, name: 'conversations',
{ agent: ['conversations'] } params: { accountId: 1 },
meta: { permissions: ['agent'] },
},
{
permissions: ['agent'],
accounts: [
{
id: 1,
role: 'agent',
permissions: ['agent'],
status: 'active',
},
],
}
) )
).toEqual(null); ).toEqual(null);
}); });
@ -76,9 +74,12 @@ describe('#validateLoggedInRoutes', () => {
it('returns dashboard url', () => { it('returns dashboard url', () => {
expect( expect(
validateLoggedInRoutes( validateLoggedInRoutes(
{ name: 'conversations', params: { accountId: 1 } }, {
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }, name: 'billing',
{ admin: ['conversations'], agent: [] } params: { accountId: 1 },
meta: { permissions: ['administrator'] },
},
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }
) )
).toEqual(`accounts/1/dashboard`); ).toEqual(`accounts/1/dashboard`);
}); });
@ -88,8 +89,7 @@ describe('#validateLoggedInRoutes', () => {
expect( expect(
validateLoggedInRoutes( validateLoggedInRoutes(
{ name: 'account_suspended', params: { accountId: 1 } }, { name: 'account_suspended', params: { accountId: 1 } },
{ accounts: [{ id: 1, role: 'agent', status: 'active' }] }, { accounts: [{ id: 1, role: 'agent', status: 'active' }] }
{ agent: ['account_suspended'] }
) )
).toEqual(`accounts/1/dashboard`); ).toEqual(`accounts/1/dashboard`);
}); });

View File

@ -1,7 +1,7 @@
import { setColorTheme } from 'dashboard/helper/themeHelper.js'; import { setColorTheme } from 'dashboard/helper/themeHelper.js';
import { LocalStorage } from 'shared/helpers/localStorage'; import { LocalStorage } from 'shared/helpers/localStorage';
jest.mock('shared/helpers/localStorage'); vi.mock('shared/helpers/localStorage');
describe('setColorTheme', () => { describe('setColorTheme', () => {
it('should set body class to dark if selectedColorScheme is dark', () => { it('should set body class to dark if selectedColorScheme is dark', () => {

View File

@ -1,9 +1,8 @@
import { uploadFile } from '../uploadHelper'; import { uploadFile } from '../uploadHelper';
import axios from 'axios'; import axios from 'axios';
// Mocking axios using jest-mock-axios
global.axios = axios; global.axios = axios;
jest.mock('axios'); vi.mock('axios');
describe('#Upload Helpers', () => { describe('#Upload Helpers', () => {
afterEach(() => { afterEach(() => {

View File

@ -4,8 +4,8 @@ import Vuex from 'vuex';
import OpenAPI from '../../api/integrations/openapi'; import OpenAPI from '../../api/integrations/openapi';
import { LocalStorage } from '../../../shared/helpers/localStorage'; import { LocalStorage } from '../../../shared/helpers/localStorage';
jest.mock('../../api/integrations/openapi'); vi.mock('../../api/integrations/openapi');
jest.mock('../../../shared/helpers/localStorage'); vi.mock('../../../shared/helpers/localStorage');
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
@ -18,12 +18,12 @@ describe('aiMixin', () => {
let actions; let actions;
beforeEach(() => { beforeEach(() => {
OpenAPI.processEvent = jest.fn(); OpenAPI.processEvent = vi.fn();
LocalStorage.set = jest.fn(); LocalStorage.set = vi.fn();
LocalStorage.get = jest.fn(); LocalStorage.get = vi.fn();
actions = { actions = {
['integrations/get']: jest.fn(), ['integrations/get']: vi.fn(),
}; };
getters = { getters = {
@ -63,20 +63,20 @@ describe('aiMixin', () => {
localVue, localVue,
}); });
const dispatchSpy = jest.spyOn(wrapper.vm.$store, 'dispatch'); const dispatchSpy = vi.spyOn(wrapper.vm.$store, 'dispatch');
await wrapper.vm.fetchIntegrationsIfRequired(); await wrapper.vm.fetchIntegrationsIfRequired();
expect(dispatchSpy).toHaveBeenCalledWith('integrations/get'); expect(dispatchSpy).toHaveBeenCalledWith('integrations/get');
}); });
it('does not fetch integrations', async () => { it('does not fetch integrations', async () => {
const dispatchSpy = jest.spyOn(wrapper.vm.$store, 'dispatch'); const dispatchSpy = vi.spyOn(wrapper.vm.$store, 'dispatch');
await wrapper.vm.fetchIntegrationsIfRequired(); await wrapper.vm.fetchIntegrationsIfRequired();
expect(dispatchSpy).not.toHaveBeenCalledWith('integrations/get'); expect(dispatchSpy).not.toHaveBeenCalledWith('integrations/get');
expect(wrapper.vm.isAIIntegrationEnabled).toBeTruthy(); expect(wrapper.vm.isAIIntegrationEnabled).toBeTruthy();
}); });
it('fetches label suggestions', async () => { it('fetches label suggestions', async () => {
const processEventSpy = jest.spyOn(OpenAPI, 'processEvent'); const processEventSpy = vi.spyOn(OpenAPI, 'processEvent');
await wrapper.vm.fetchLabelSuggestions({ await wrapper.vm.fetchLabelSuggestions({
conversationId: '123', conversationId: '123',
}); });

View File

@ -11,7 +11,7 @@ describe('attributeMixin', () => {
let store; let store;
beforeEach(() => { beforeEach(() => {
actions = { updateUISettings: jest.fn(), toggleSidebarUIState: jest.fn() }; actions = { updateUISettings: vi.fn(), toggleSidebarUIState: vi.fn() };
getters = { getters = {
getSelectedChat: () => ({ getSelectedChat: () => ({
id: 7165, id: 7165,

View File

@ -1,13 +1,13 @@
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin'; import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
import Vue from 'vue'; import Vue from 'vue';
jest.mock('shared/helpers/FileHelper', () => ({ vi.mock('shared/helpers/FileHelper', () => ({
checkFileSizeLimit: jest.fn(), checkFileSizeLimit: vi.fn(),
})); }));
jest.mock('activestorage', () => ({ vi.mock('activestorage', () => ({
DirectUpload: jest.fn().mockImplementation(() => ({ DirectUpload: vi.fn().mockImplementation(() => ({
create: jest.fn(), create: vi.fn(),
})), })),
})); }));
@ -27,20 +27,20 @@ describe('FileUploadMixin', () => {
vm.currentUser = { vm.currentUser = {
access_token: 'token', access_token: 'token',
}; };
vm.$t = jest.fn(message => message); vm.$t = vi.fn(message => message);
vm.showAlert = jest.fn(); vm.showAlert = vi.fn();
vm.attachFile = jest.fn(); vm.attachFile = vi.fn();
}); });
it('should call onDirectFileUpload when direct uploads are enabled', () => { it('should call onDirectFileUpload when direct uploads are enabled', () => {
vm.onDirectFileUpload = jest.fn(); vm.onDirectFileUpload = vi.fn();
vm.onFileUpload({}); vm.onFileUpload({});
expect(vm.onDirectFileUpload).toHaveBeenCalledWith({}); expect(vm.onDirectFileUpload).toHaveBeenCalledWith({});
}); });
it('should call onIndirectFileUpload when direct uploads are disabled', () => { it('should call onIndirectFileUpload when direct uploads are disabled', () => {
vm.globalConfig.directUploadsEnabled = false; vm.globalConfig.directUploadsEnabled = false;
vm.onIndirectFileUpload = jest.fn(); vm.onIndirectFileUpload = vi.fn();
vm.onFileUpload({}); vm.onFileUpload({});
expect(vm.onIndirectFileUpload).toHaveBeenCalledWith({}); expect(vm.onIndirectFileUpload).toHaveBeenCalledWith({});
}); });
@ -53,7 +53,7 @@ describe('FileUploadMixin', () => {
it('shows an alert if the file size exceeds the maximum limit', () => { it('shows an alert if the file size exceeds the maximum limit', () => {
const fakeFile = { size: 999999999 }; const fakeFile = { size: 999999999 };
vm.showAlert = jest.fn(); vm.showAlert = vi.fn();
vm.onDirectFileUpload(fakeFile); vm.onDirectFileUpload(fakeFile);
expect(vm.showAlert).toHaveBeenCalledWith(expect.any(String)); expect(vm.showAlert).toHaveBeenCalledWith(expect.any(String));
}); });
@ -67,7 +67,7 @@ describe('FileUploadMixin', () => {
it('shows an alert if the file size exceeds the maximum limit', () => { it('shows an alert if the file size exceeds the maximum limit', () => {
const fakeFile = { size: 999999999 }; const fakeFile = { size: 999999999 };
vm.showAlert = jest.fn(); vm.showAlert = vi.fn();
vm.onIndirectFileUpload(fakeFile); vm.onIndirectFileUpload(fakeFile);
expect(vm.showAlert).toHaveBeenCalledWith(expect.any(String)); expect(vm.showAlert).toHaveBeenCalledWith(expect.any(String));
}); });

View File

@ -11,14 +11,14 @@ describe('#messageStamp', () => {
describe('#messageTimestamp', () => { describe('#messageTimestamp', () => {
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers('modern'); vi.useFakeTimers('modern');
const mockDate = new Date(2023, 4, 5); const mockDate = new Date(2023, 4, 5);
jest.setSystemTime(mockDate); vi.setSystemTime(mockDate);
}); });
afterEach(() => { afterEach(() => {
jest.useRealTimers(); vi.useRealTimers();
}); });
it('should return the message date in the specified format if the message was sent in the current year', () => { it('should return the message date in the specified format if the message was sent in the current year', () => {
@ -35,7 +35,7 @@ describe('#messageTimestamp', () => {
describe('#dynamicTime', () => { describe('#dynamicTime', () => {
it('returns correct value', () => { it('returns correct value', () => {
Date.now = jest.fn(() => new Date(Date.UTC(2023, 1, 14)).valueOf()); Date.now = vi.fn(() => new Date(Date.UTC(2023, 1, 14)).valueOf());
expect(TimeMixin.methods.dynamicTime(1612971343)).toEqual( expect(TimeMixin.methods.dynamicTime(1612971343)).toEqual(
'about 2 years ago' 'about 2 years ago'
); );

View File

@ -14,7 +14,7 @@ describe('uiSettingsMixin', () => {
let store; let store;
beforeEach(() => { beforeEach(() => {
actions = { updateUISettings: jest.fn(), toggleSidebarUIState: jest.fn() }; actions = { updateUISettings: vi.fn(), toggleSidebarUIState: vi.fn() };
getters = { getters = {
getUISettings: () => ({ getUISettings: () => ({
enter_to_send_enabled: false, enter_to_send_enabled: false,

View File

@ -7,7 +7,9 @@ export const routes = [
{ {
path: frontendURL('accounts/:accountId/search'), path: frontendURL('accounts/:accountId/search'),
name: 'search', name: 'search',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: SearchView, component: SearchView,
}, },
]; ];

View File

@ -41,7 +41,6 @@
<script> <script>
import Sidebar from '../../components/layout/Sidebar.vue'; import Sidebar from '../../components/layout/Sidebar.vue';
import CommandBar from './commands/commandbar.vue';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue'; import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal.vue'; import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal.vue';
@ -50,6 +49,7 @@ import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.v
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue'; import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
const CommandBar = () => import('./commands/commandbar.vue');
export default { export default {
components: { components: {

View File

@ -15,7 +15,7 @@ describe('AddReminder', () => {
}); });
it('tests resetValue', () => { it('tests resetValue', () => {
const resetValue = jest.spyOn(wrapper.vm, 'resetValue'); const resetValue = vi.spyOn(wrapper.vm, 'resetValue');
wrapper.vm.content = 'test'; wrapper.vm.content = 'test';
wrapper.vm.date = '08/11/2022'; wrapper.vm.date = '08/11/2022';
wrapper.vm.resetValue(); wrapper.vm.resetValue();
@ -25,7 +25,7 @@ describe('AddReminder', () => {
}); });
it('tests optionSelected', () => { it('tests optionSelected', () => {
const optionSelected = jest.spyOn(wrapper.vm, 'optionSelected'); const optionSelected = vi.spyOn(wrapper.vm, 'optionSelected');
wrapper.vm.label = ''; wrapper.vm.label = '';
wrapper.vm.optionSelected({ target: { value: 'test' } }); wrapper.vm.optionSelected({ target: { value: 'test' } });
expect(wrapper.vm.label).toEqual('test'); expect(wrapper.vm.label).toEqual('test');
@ -33,7 +33,7 @@ describe('AddReminder', () => {
}); });
it('tests onAdd', () => { it('tests onAdd', () => {
const onAdd = jest.spyOn(wrapper.vm, 'onAdd'); const onAdd = vi.spyOn(wrapper.vm, 'onAdd');
wrapper.vm.label = 'label'; wrapper.vm.label = 'label';
wrapper.vm.content = 'content'; wrapper.vm.content = 'content';
wrapper.vm.date = '08/11/2022'; wrapper.vm.date = '08/11/2022';

View File

@ -7,13 +7,17 @@ export const routes = [
{ {
path: frontendURL('accounts/:accountId/contacts'), path: frontendURL('accounts/:accountId/contacts'),
name: 'contacts_dashboard', name: 'contacts_dashboard',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ContactsView, component: ContactsView,
}, },
{ {
path: frontendURL('accounts/:accountId/contacts/custom_view/:id'), path: frontendURL('accounts/:accountId/contacts/custom_view/:id'),
name: 'contacts_segments_dashboard', name: 'contacts_segments_dashboard',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ContactsView, component: ContactsView,
props: route => { props: route => {
return { segmentsId: route.params.id }; return { segmentsId: route.params.id };
@ -22,7 +26,9 @@ export const routes = [
{ {
path: frontendURL('accounts/:accountId/labels/:label/contacts'), path: frontendURL('accounts/:accountId/labels/:label/contacts'),
name: 'contacts_labels_dashboard', name: 'contacts_labels_dashboard',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ContactsView, component: ContactsView,
props: route => { props: route => {
return { label: route.params.label }; return { label: route.params.label };
@ -31,7 +37,9 @@ export const routes = [
{ {
path: frontendURL('accounts/:accountId/contacts/:contactId'), path: frontendURL('accounts/:accountId/contacts/:contactId'),
name: 'contact_profile_dashboard', name: 'contact_profile_dashboard',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ContactManageView, component: ContactManageView,
props: route => { props: route => {
return { contactId: route.params.contactId }; return { contactId: route.params.contactId };

View File

@ -22,25 +22,40 @@
:is-on-expanded-layout="isOnExpandedLayout" :is-on-expanded-layout="isOnExpandedLayout"
@contact-panel-toggle="onToggleContactPanel" @contact-panel-toggle="onToggleContactPanel"
/> />
<woot-modal
:show.sync="showCustomSnoozeModal"
:on-close="hideCustomSnoozeModal"
>
<custom-snooze-modal
@close="hideCustomSnoozeModal"
@choose-time="chooseSnoozeTime"
/>
</woot-modal>
</section> </section>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { getUnixTime } from 'date-fns';
import ChatList from '../../../components/ChatList.vue'; import ChatList from '../../../components/ChatList.vue';
import ConversationBox from '../../../components/widgets/conversation/ConversationBox.vue'; import ConversationBox from '../../../components/widgets/conversation/ConversationBox.vue';
import PopOverSearch from './search/PopOverSearch.vue'; import PopOverSearch from './search/PopOverSearch.vue';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import alertMixin from 'shared/mixins/alertMixin';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
export default { export default {
components: { components: {
ChatList, ChatList,
ConversationBox, ConversationBox,
PopOverSearch, PopOverSearch,
CustomSnoozeModal,
}, },
mixins: [uiSettingsMixin], mixins: [uiSettingsMixin, alertMixin],
props: { props: {
inboxId: { inboxId: {
type: [String, Number], type: [String, Number],
@ -70,12 +85,14 @@ export default {
data() { data() {
return { return {
showSearchModal: false, showSearchModal: false,
showCustomSnoozeModal: false,
}; };
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
chatList: 'getAllConversations', chatList: 'getAllConversations',
currentChat: 'getSelectedChat', currentChat: 'getSelectedChat',
contextMenuChatId: 'getContextMenuChatId',
}), }),
showConversationList() { showConversationList() {
return this.isOnExpandedLayout ? !this.conversationId : true; return this.isOnExpandedLayout ? !this.conversationId : true;
@ -112,6 +129,10 @@ export default {
this.$watch('chatList.length', () => { this.$watch('chatList.length', () => {
this.setActiveChat(); this.setActiveChat();
}); });
this.$emitter.on(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
},
beforeDestroy() {
this.$emitter.off(CMD_SNOOZE_CONVERSATION, this.onCmdSnoozeConversation);
}, },
methods: { methods: {
@ -186,6 +207,43 @@ export default {
closeSearch() { closeSearch() {
this.showSearchModal = false; this.showSearchModal = false;
}, },
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 the custom snooze modal is open
// Then if the custom snooze modal is closed then set the context menu chat id to null
this.$store.dispatch('setContextMenuChatId', null);
this.showCustomSnoozeModal = false;
},
}, },
}; };
</script> </script>

View File

@ -117,7 +117,7 @@
/> />
<div class="w-full"> <div class="w-full">
<label> Social Profiles </label> <label>{{ $t('CONTACTS_PAGE.LIST.TABLE_HEADER.SOCIAL_PROFILES') }}</label>
<div <div
v-for="socialProfile in socialProfileKeys" v-for="socialProfile in socialProfileKeys"
:key="socialProfile.key" :key="socialProfile.key"
@ -130,7 +130,7 @@
</span> </span>
<input <input
v-model="socialProfileUserNames[socialProfile.key]" v-model="socialProfileUserNames[socialProfile.key]"
class="input-group-field ltr:rounded-l-none rtl:rounded-r-none !mb-0" class="input-group-field ltr:!rounded-l-none rtl:rounded-r-none !mb-0"
type="text" type="text"
/> />
</div> </div>

View File

@ -1,13 +1,15 @@
/* eslint arrow-body-style: 0 */ /* eslint arrow-body-style: 0 */
import { frontendURL } from '../../../helper/URLHelper'; import { frontendURL } from '../../../helper/URLHelper';
const ConversationView = () => import('./ConversationView'); const ConversationView = () => import('./ConversationView.vue');
export default { export default {
routes: [ routes: [
{ {
path: frontendURL('accounts/:accountId/dashboard'), path: frontendURL('accounts/:accountId/dashboard'),
name: 'home', name: 'home',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: () => { props: () => {
return { inboxId: 0 }; return { inboxId: 0 };
@ -16,7 +18,9 @@ export default {
{ {
path: frontendURL('accounts/:accountId/conversations/:conversation_id'), path: frontendURL('accounts/:accountId/conversations/:conversation_id'),
name: 'inbox_conversation', name: 'inbox_conversation',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: route => { props: route => {
return { inboxId: 0, conversationId: route.params.conversation_id }; return { inboxId: 0, conversationId: route.params.conversation_id };
@ -25,7 +29,9 @@ export default {
{ {
path: frontendURL('accounts/:accountId/inbox/:inbox_id'), path: frontendURL('accounts/:accountId/inbox/:inbox_id'),
name: 'inbox_dashboard', name: 'inbox_dashboard',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: route => { props: route => {
return { inboxId: route.params.inbox_id }; return { inboxId: route.params.inbox_id };
@ -36,7 +42,9 @@ export default {
'accounts/:accountId/inbox/:inbox_id/conversations/:conversation_id' 'accounts/:accountId/inbox/:inbox_id/conversations/:conversation_id'
), ),
name: 'conversation_through_inbox', name: 'conversation_through_inbox',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: route => { props: route => {
return { return {
@ -48,7 +56,9 @@ export default {
{ {
path: frontendURL('accounts/:accountId/label/:label'), path: frontendURL('accounts/:accountId/label/:label'),
name: 'label_conversations', name: 'label_conversations',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: route => ({ label: route.params.label }), props: route => ({ label: route.params.label }),
}, },
@ -57,7 +67,9 @@ export default {
'accounts/:accountId/label/:label/conversations/:conversation_id' 'accounts/:accountId/label/:label/conversations/:conversation_id'
), ),
name: 'conversations_through_label', name: 'conversations_through_label',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: route => ({ props: route => ({
conversationId: route.params.conversation_id, conversationId: route.params.conversation_id,
@ -67,7 +79,9 @@ export default {
{ {
path: frontendURL('accounts/:accountId/team/:teamId'), path: frontendURL('accounts/:accountId/team/:teamId'),
name: 'team_conversations', name: 'team_conversations',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: route => ({ teamId: route.params.teamId }), props: route => ({ teamId: route.params.teamId }),
}, },
@ -76,7 +90,9 @@ export default {
'accounts/:accountId/team/:teamId/conversations/:conversationId' 'accounts/:accountId/team/:teamId/conversations/:conversationId'
), ),
name: 'conversations_through_team', name: 'conversations_through_team',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: route => ({ props: route => ({
conversationId: route.params.conversationId, conversationId: route.params.conversationId,
@ -86,7 +102,9 @@ export default {
{ {
path: frontendURL('accounts/:accountId/custom_view/:id'), path: frontendURL('accounts/:accountId/custom_view/:id'),
name: 'folder_conversations', name: 'folder_conversations',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: route => ({ foldersId: route.params.id }), props: route => ({ foldersId: route.params.id }),
}, },
@ -95,7 +113,9 @@ export default {
'accounts/:accountId/custom_view/:id/conversations/:conversation_id' 'accounts/:accountId/custom_view/:id/conversations/:conversation_id'
), ),
name: 'conversations_through_folders', name: 'conversations_through_folders',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: route => ({ props: route => ({
conversationId: route.params.conversation_id, conversationId: route.params.conversation_id,
@ -105,7 +125,9 @@ export default {
{ {
path: frontendURL('accounts/:accountId/mentions/conversations'), path: frontendURL('accounts/:accountId/mentions/conversations'),
name: 'conversation_mentions', name: 'conversation_mentions',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: () => ({ conversationType: 'mention' }), props: () => ({ conversationType: 'mention' }),
}, },
@ -114,7 +136,9 @@ export default {
'accounts/:accountId/mentions/conversations/:conversationId' 'accounts/:accountId/mentions/conversations/:conversationId'
), ),
name: 'conversation_through_mentions', name: 'conversation_through_mentions',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: route => ({ props: route => ({
conversationId: route.params.conversationId, conversationId: route.params.conversationId,
@ -124,7 +148,9 @@ export default {
{ {
path: frontendURL('accounts/:accountId/unattended/conversations'), path: frontendURL('accounts/:accountId/unattended/conversations'),
name: 'conversation_unattended', name: 'conversation_unattended',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: () => ({ conversationType: 'unattended' }), props: () => ({ conversationType: 'unattended' }),
}, },
@ -133,7 +159,9 @@ export default {
'accounts/:accountId/unattended/conversations/:conversationId' 'accounts/:accountId/unattended/conversations/:conversationId'
), ),
name: 'conversation_through_unattended', name: 'conversation_through_unattended',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: route => ({ props: route => ({
conversationId: route.params.conversationId, conversationId: route.params.conversationId,
@ -143,7 +171,9 @@ export default {
{ {
path: frontendURL('accounts/:accountId/participating/conversations'), path: frontendURL('accounts/:accountId/participating/conversations'),
name: 'conversation_participating', name: 'conversation_participating',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: () => ({ conversationType: 'participating' }), props: () => ({ conversationType: 'participating' }),
}, },
@ -152,7 +182,9 @@ export default {
'accounts/:accountId/participating/conversations/:conversationId' 'accounts/:accountId/participating/conversations/:conversationId'
), ),
name: 'conversation_through_participating', name: 'conversation_through_participating',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ConversationView, component: ConversationView,
props: route => ({ props: route => ({
conversationId: route.params.conversationId, conversationId: route.params.conversationId,

View File

@ -28,7 +28,9 @@ export default {
{ {
path: frontendURL('accounts/:accountId/suspended'), path: frontendURL('accounts/:accountId/suspended'),
name: 'account_suspended', name: 'account_suspended',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: Suspended, component: Suspended,
}, },
], ],

View File

@ -59,13 +59,12 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import UpgradePage from './UpgradePage'; import UpgradePage from './UpgradePage.vue';
import { frontendURL } from '../../../../helper/URLHelper'; import { frontendURL } from '../../../../helper/URLHelper';
import Sidebar from 'dashboard/components/layout/Sidebar.vue'; import Sidebar from 'dashboard/components/layout/Sidebar.vue';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import PortalPopover from '../components/PortalPopover.vue'; import PortalPopover from '../components/PortalPopover.vue';
import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue'; import HelpCenterSidebar from '../components/Sidebar/Sidebar.vue';
import CommandBar from 'dashboard/routes/dashboard/commands/commandbar.vue';
import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue'; import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal.vue';
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue'; import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue'; import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
@ -73,6 +72,8 @@ import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import portalMixin from '../mixins/portalMixin'; import portalMixin from '../mixins/portalMixin';
import AddCategory from '../pages/categories/AddCategory.vue'; import AddCategory from '../pages/categories/AddCategory.vue';
import { FEATURE_FLAGS } from 'dashboard/featureFlags'; import { FEATURE_FLAGS } from 'dashboard/featureFlags';
const CommandBar = () =>
import('dashboard/routes/dashboard/commands/commandbar.vue');
export default { export default {
components: { components: {

View File

@ -1,42 +1,48 @@
import HelpCenterLayout from './components/HelpCenterLayout'; import HelpCenterLayout from './components/HelpCenterLayout.vue';
import { getPortalRoute } from './helpers/routeHelper'; import { getPortalRoute } from './helpers/routeHelper';
const ListAllPortals = () => import('./pages/portals/ListAllPortals'); const ListAllPortals = () => import('./pages/portals/ListAllPortals.vue');
const NewPortal = () => import('./pages/portals/NewPortal'); const NewPortal = () => import('./pages/portals/NewPortal.vue');
const EditPortal = () => import('./pages/portals/EditPortal'); const EditPortal = () => import('./pages/portals/EditPortal.vue');
const EditPortalBasic = () => import('./pages/portals/EditPortalBasic'); const EditPortalBasic = () => import('./pages/portals/EditPortalBasic.vue');
const EditPortalCustomization = () => const EditPortalCustomization = () =>
import('./pages/portals/EditPortalCustomization'); import('./pages/portals/EditPortalCustomization.vue');
const EditPortalLocales = () => import('./pages/portals/EditPortalLocales.vue'); const EditPortalLocales = () => import('./pages/portals/EditPortalLocales.vue');
const ShowPortal = () => import('./pages/portals/ShowPortal'); const ShowPortal = () => import('./pages/portals/ShowPortal.vue');
const PortalDetails = () => import('./pages/portals/PortalDetails'); const PortalDetails = () => import('./pages/portals/PortalDetails.vue');
const PortalCustomization = () => import('./pages/portals/PortalCustomization'); const PortalCustomization = () =>
import('./pages/portals/PortalCustomization.vue');
const PortalSettingsFinish = () => const PortalSettingsFinish = () =>
import('./pages/portals/PortalSettingsFinish'); import('./pages/portals/PortalSettingsFinish.vue');
const ListAllCategories = () => import('./pages/categories/ListAllCategories'); const ListAllCategories = () =>
const NewCategory = () => import('./pages/categories/NewCategory'); import('./pages/categories/ListAllCategories.vue');
const EditCategory = () => import('./pages/categories/EditCategory'); const NewCategory = () => import('./pages/categories/NewCategory.vue');
const EditCategory = () => import('./pages/categories/EditCategory.vue');
const ListCategoryArticles = () => const ListCategoryArticles = () =>
import('./pages/articles/ListCategoryArticles'); import('./pages/articles/ListCategoryArticles.vue');
const ListAllArticles = () => import('./pages/articles/ListAllArticles'); const ListAllArticles = () => import('./pages/articles/ListAllArticles.vue');
const DefaultPortalArticles = () => const DefaultPortalArticles = () =>
import('./pages/articles/DefaultPortalArticles'); import('./pages/articles/DefaultPortalArticles.vue');
const NewArticle = () => import('./pages/articles/NewArticle'); const NewArticle = () => import('./pages/articles/NewArticle.vue');
const EditArticle = () => import('./pages/articles/EditArticle'); const EditArticle = () => import('./pages/articles/EditArticle.vue');
const portalRoutes = [ const portalRoutes = [
{ {
path: getPortalRoute(''), path: getPortalRoute(''),
name: 'default_portal_articles', name: 'default_portal_articles',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator'],
},
component: DefaultPortalArticles, component: DefaultPortalArticles,
}, },
{ {
path: getPortalRoute('all'), path: getPortalRoute('all'),
name: 'list_all_portals', name: 'list_all_portals',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllPortals, component: ListAllPortals,
}, },
{ {
@ -47,55 +53,73 @@ const portalRoutes = [
path: '', path: '',
name: 'new_portal_information', name: 'new_portal_information',
component: PortalDetails, component: PortalDetails,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: ':portalSlug/customization', path: ':portalSlug/customization',
name: 'portal_customization', name: 'portal_customization',
component: PortalCustomization, component: PortalCustomization,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: ':portalSlug/finish', path: ':portalSlug/finish',
name: 'portal_finish', name: 'portal_finish',
component: PortalSettingsFinish, component: PortalSettingsFinish,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
], ],
}, },
{ {
path: getPortalRoute(':portalSlug'), path: getPortalRoute(':portalSlug'),
name: 'portalSlug', name: 'portalSlug',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ShowPortal, component: ShowPortal,
}, },
{ {
path: getPortalRoute(':portalSlug/edit'), path: getPortalRoute(':portalSlug/edit'),
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: EditPortal, component: EditPortal,
children: [ children: [
{ {
path: '', path: '',
name: 'edit_portal_information', name: 'edit_portal_information',
component: EditPortalBasic, component: EditPortalBasic,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: 'customizations', path: 'customizations',
name: 'edit_portal_customization', name: 'edit_portal_customization',
component: EditPortalCustomization, component: EditPortalCustomization,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: 'locales', path: 'locales',
name: 'edit_portal_locales', name: 'edit_portal_locales',
component: EditPortalLocales, component: EditPortalLocales,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: 'categories', path: 'categories',
name: 'list_all_locale_categories', name: 'list_all_locale_categories',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllCategories, component: ListAllCategories,
}, },
], ],
@ -106,39 +130,51 @@ const articleRoutes = [
{ {
path: getPortalRoute(':portalSlug/:locale/articles'), path: getPortalRoute(':portalSlug/:locale/articles'),
name: 'list_all_locale_articles', name: 'list_all_locale_articles',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllArticles, component: ListAllArticles,
}, },
{ {
path: getPortalRoute(':portalSlug/:locale/articles/new'), path: getPortalRoute(':portalSlug/:locale/articles/new'),
name: 'new_article', name: 'new_article',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: NewArticle, component: NewArticle,
}, },
{ {
path: getPortalRoute(':portalSlug/:locale/articles/mine'), path: getPortalRoute(':portalSlug/:locale/articles/mine'),
name: 'list_mine_articles', name: 'list_mine_articles',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllArticles, component: ListAllArticles,
}, },
{ {
path: getPortalRoute(':portalSlug/:locale/articles/archived'), path: getPortalRoute(':portalSlug/:locale/articles/archived'),
name: 'list_archived_articles', name: 'list_archived_articles',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllArticles, component: ListAllArticles,
}, },
{ {
path: getPortalRoute(':portalSlug/:locale/articles/draft'), path: getPortalRoute(':portalSlug/:locale/articles/draft'),
name: 'list_draft_articles', name: 'list_draft_articles',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllArticles, component: ListAllArticles,
}, },
{ {
path: getPortalRoute(':portalSlug/:locale/articles/:articleSlug'), path: getPortalRoute(':portalSlug/:locale/articles/:articleSlug'),
name: 'edit_article', name: 'edit_article',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: EditArticle, component: EditArticle,
}, },
]; ];
@ -147,19 +183,25 @@ const categoryRoutes = [
{ {
path: getPortalRoute(':portalSlug/:locale/categories'), path: getPortalRoute(':portalSlug/:locale/categories'),
name: 'all_locale_categories', name: 'all_locale_categories',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllCategories, component: ListAllCategories,
}, },
{ {
path: getPortalRoute(':portalSlug/:locale/categories/new'), path: getPortalRoute(':portalSlug/:locale/categories/new'),
name: 'new_category_in_locale', name: 'new_category_in_locale',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: NewCategory, component: NewCategory,
}, },
{ {
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'), path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
name: 'show_category', name: 'show_category',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ListAllArticles, component: ListAllArticles,
}, },
{ {
@ -167,13 +209,17 @@ const categoryRoutes = [
':portalSlug/:locale/categories/:categorySlug/articles' ':portalSlug/:locale/categories/:categorySlug/articles'
), ),
name: 'show_category_articles', name: 'show_category_articles',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: ListCategoryArticles, component: ListCategoryArticles,
}, },
{ {
path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'), path: getPortalRoute(':portalSlug/:locale/categories/:categorySlug'),
name: 'edit_category', name: 'edit_category',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: EditCategory, component: EditCategory,
}, },
]; ];

View File

@ -36,7 +36,7 @@
</template> </template>
<script> <script>
const EmojiInput = () => import('shared/components/emoji/EmojiInput'); const EmojiInput = () => import('shared/components/emoji/EmojiInput.vue');
export default { export default {
components: { EmojiInput }, components: { EmojiInput },

View File

@ -12,13 +12,17 @@ export const routes = [
path: '', path: '',
name: 'inbox_view', name: 'inbox_view',
component: InboxEmptyStateView, component: InboxEmptyStateView,
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
}, },
{ {
path: ':notification_id', path: ':notification_id',
name: 'inbox_view_conversation', name: 'inbox_view_conversation',
component: InboxDetailView, component: InboxDetailView,
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
}, },
], ],
}, },

View File

@ -18,7 +18,9 @@ export const routes = [
path: '', path: '',
name: 'notifications_index', name: 'notifications_index',
component: NotificationsView, component: NotificationsView,
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
}, },
], ],
}, },

View File

@ -6,7 +6,9 @@ export default {
routes: [ routes: [
{ {
path: frontendURL('accounts/:accountId/settings/general'), path: frontendURL('accounts/:accountId/settings/general'),
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
component: SettingsContent, component: SettingsContent,
props: { props: {
headerTitle: 'GENERAL_SETTINGS.TITLE', headerTitle: 'GENERAL_SETTINGS.TITLE',
@ -18,7 +20,9 @@ export default {
path: '', path: '',
name: 'general_settings_index', name: 'general_settings_index',
component: Index, component: Index,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
], ],
}, },

View File

@ -8,7 +8,9 @@ export default {
routes: [ routes: [
{ {
path: frontendURL('accounts/:accountId/settings/agent-bots'), path: frontendURL('accounts/:accountId/settings/agent-bots'),
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
component: SettingsContent, component: SettingsContent,
props: { props: {
headerTitle: 'AGENT_BOTS.HEADER', headerTitle: 'AGENT_BOTS.HEADER',
@ -20,19 +22,25 @@ export default {
path: '', path: '',
name: 'agent_bots', name: 'agent_bots',
component: Bot, component: Bot,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: 'csml/new', path: 'csml/new',
name: 'agent_bots_csml_new', name: 'agent_bots_csml_new',
component: CsmlNewBot, component: CsmlNewBot,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: 'csml/:botId', path: 'csml/:botId',
name: 'agent_bots_csml_edit', name: 'agent_bots_csml_edit',
component: CsmlEditBot, component: CsmlEditBot,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
], ],
}, },

View File

@ -1,13 +1,13 @@
<template> <template>
<woot-modal :show.sync="show" :on-close="onClose"> <woot-modal :show.sync="show" :on-close="onClose">
<div class="h-auto overflow-auto flex flex-col"> <div class="flex flex-col h-auto overflow-auto">
<woot-modal-header <woot-modal-header
:header-title="$t('AGENT_MGMT.ADD.TITLE')" :header-title="$t('AGENT_MGMT.ADD.TITLE')"
:header-content="$t('AGENT_MGMT.ADD.DESC')" :header-content="$t('AGENT_MGMT.ADD.DESC')"
/> />
<form <form
class="flex flex-col w-full items-start" class="flex flex-col items-start w-full"
@submit.prevent="addAgent()" @submit.prevent="addAgent()"
> >
<div class="w-full"> <div class="w-full">
@ -45,7 +45,7 @@
/> />
</label> </label>
</div> </div>
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full"> <div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
<div class="w-full"> <div class="w-full">
<woot-submit-button <woot-submit-button
:disabled=" :disabled="
@ -140,7 +140,7 @@ export default {
} = error; } = error;
let errorMessage = ''; let errorMessage = '';
if (error.response.status === 422 && !attributes.includes('base')) { if (error?.response?.status === 422 && !attributes.includes('base')) {
errorMessage = this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE'); errorMessage = this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE');
} else { } else {
errorMessage = this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE'); errorMessage = this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE');

View File

@ -15,14 +15,15 @@ export default {
children: [ children: [
{ {
path: '', path: '',
name: 'agents_wrapper',
redirect: 'list', redirect: 'list',
}, },
{ {
path: 'list', path: 'list',
name: 'agent_list', name: 'agent_list',
component: AgentHome, component: AgentHome,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
], ],
}, },

View File

@ -15,14 +15,15 @@ export default {
children: [ children: [
{ {
path: '', path: '',
name: 'attributes_wrapper',
redirect: 'list', redirect: 'list',
}, },
{ {
path: 'list', path: 'list',
name: 'attributes_list', name: 'attributes_list',
component: AttributesHome, component: AttributesHome,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
], ],
}, },

View File

@ -16,13 +16,14 @@ export default {
children: [ children: [
{ {
path: '', path: '',
name: 'auditlogs_wrapper',
redirect: 'list', redirect: 'list',
}, },
{ {
path: 'list', path: 'list',
name: 'auditlogs_list', name: 'auditlogs_list',
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
component: AuditLogsHome, component: AuditLogsHome,
}, },
], ],

View File

@ -15,14 +15,15 @@ export default {
children: [ children: [
{ {
path: '', path: '',
name: 'automation_wrapper',
redirect: 'list', redirect: 'list',
}, },
{ {
path: 'list', path: 'list',
name: 'automation_list', name: 'automation_list',
component: Automation, component: Automation,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
], ],
}, },

View File

@ -6,7 +6,9 @@ export default {
routes: [ routes: [
{ {
path: frontendURL('accounts/:accountId/settings/billing'), path: frontendURL('accounts/:accountId/settings/billing'),
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
component: SettingsContent, component: SettingsContent,
props: { props: {
headerTitle: 'BILLING_SETTINGS.TITLE', headerTitle: 'BILLING_SETTINGS.TITLE',
@ -18,7 +20,9 @@ export default {
path: '', path: '',
name: 'billing_settings_index', name: 'billing_settings_index',
component: Index, component: Index,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
], ],
}, },

View File

@ -19,7 +19,9 @@ export default {
{ {
path: 'ongoing', path: 'ongoing',
name: 'ongoing_campaigns', name: 'ongoing_campaigns',
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
component: Index, component: Index,
}, },
], ],
@ -35,7 +37,9 @@ export default {
{ {
path: 'one_off', path: 'one_off',
name: 'one_off', name: 'one_off',
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
component: Index, component: Index,
}, },
], ],

View File

@ -16,13 +16,14 @@ export default {
children: [ children: [
{ {
path: '', path: '',
name: 'canned_wrapper',
redirect: 'list', redirect: 'list',
}, },
{ {
path: 'list', path: 'list',
name: 'canned_list', name: 'canned_list',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: CannedHome, component: CannedHome,
}, },
], ],

View File

@ -1,12 +1,12 @@
import Facebook from './channels/Facebook'; import Facebook from './channels/Facebook.vue';
import Website from './channels/Website'; import Website from './channels/Website.vue';
import Twitter from './channels/Twitter'; import Twitter from './channels/Twitter.vue';
import Api from './channels/Api'; import Api from './channels/Api.vue';
import Email from './channels/Email'; import Email from './channels/Email.vue';
import Sms from './channels/Sms'; import Sms from './channels/Sms.vue';
import Whatsapp from './channels/Whatsapp'; import Whatsapp from './channels/Whatsapp.vue';
import Line from './channels/Line'; import Line from './channels/Line.vue';
import Telegram from './channels/Telegram'; import Telegram from './channels/Telegram.vue';
const channelViewList = { const channelViewList = {
facebook: Facebook, facebook: Facebook,

View File

@ -28,14 +28,15 @@ export default {
children: [ children: [
{ {
path: '', path: '',
name: 'settings_inbox',
redirect: 'list', redirect: 'list',
}, },
{ {
path: 'list', path: 'list',
name: 'settings_inbox_list', name: 'settings_inbox_list',
component: InboxHome, component: InboxHome,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: 'new', path: 'new',
@ -45,19 +46,25 @@ export default {
path: '', path: '',
name: 'settings_inbox_new', name: 'settings_inbox_new',
component: ChannelList, component: ChannelList,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: ':inbox_id/finish', path: ':inbox_id/finish',
name: 'settings_inbox_finish', name: 'settings_inbox_finish',
component: FinishSetup, component: FinishSetup,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: ':sub_page', path: ':sub_page',
name: 'settings_inboxes_page_channel', name: 'settings_inboxes_page_channel',
component: channelFactory.create(), component: channelFactory.create(),
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
props: route => { props: route => {
return { channel_name: route.params.sub_page }; return { channel_name: route.params.sub_page };
}, },
@ -65,7 +72,9 @@ export default {
{ {
path: ':inbox_id/agents', path: ':inbox_id/agents',
name: 'settings_inboxes_add_agents', name: 'settings_inboxes_add_agents',
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
component: AddAgents, component: AddAgents,
}, },
], ],
@ -74,7 +83,9 @@ export default {
path: ':inboxId', path: ':inboxId',
name: 'settings_inbox_show', name: 'settings_inbox_show',
component: Settings, component: Settings,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
], ],
}, },

View File

@ -26,13 +26,17 @@ export default {
path: '', path: '',
name: 'settings_applications', name: 'settings_applications',
component: Index, component: Index,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: ':integration_id', path: ':integration_id',
name: 'settings_applications_integration', name: 'settings_applications_integration',
component: IntegrationHooks, component: IntegrationHooks,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
props: route => ({ props: route => ({
integrationId: route.params.integration_id, integrationId: route.params.integration_id,
}), }),

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="h-auto overflow-auto flex flex-col"> <div class="flex flex-col h-auto overflow-auto">
<woot-modal-header <woot-modal-header
:header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.TITLE')" :header-title="$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.TITLE')"
/> />
@ -51,7 +51,7 @@ export default {
this.onClose(); this.onClose();
} catch (error) { } catch (error) {
const alertMessage = const alertMessage =
error.response.data.message || error?.response?.data?.message ||
this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE'); this.$t('INTEGRATION_SETTINGS.WEBHOOK.EDIT.API.ERROR_MESSAGE');
this.showAlert(alertMessage); this.showAlert(alertMessage);
} }

View File

@ -30,32 +30,42 @@ export default {
path: '', path: '',
name: 'settings_integrations', name: 'settings_integrations',
component: Index, component: Index,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: 'webhook', path: 'webhook',
component: Webhook, component: Webhook,
name: 'settings_integrations_webhook', name: 'settings_integrations_webhook',
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: 'dashboard-apps', path: 'dashboard-apps',
component: DashboardApps, component: DashboardApps,
name: 'settings_integrations_dashboard_apps', name: 'settings_integrations_dashboard_apps',
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
}, },
{ {
path: 'slack', path: 'slack',
name: 'settings_integrations_slack', name: 'settings_integrations_slack',
component: Slack, component: Slack,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
props: route => ({ code: route.query.code }), props: route => ({ code: route.query.code }),
}, },
{ {
path: ':integration_id', path: ':integration_id',
name: 'settings_integrations_integration', name: 'settings_integrations_integration',
component: ShowIntegration, component: ShowIntegration,
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
props: route => { props: route => {
return { return {
integrationId: route.params.integration_id, integrationId: route.params.integration_id,

View File

@ -17,13 +17,17 @@ export default {
{ {
path: '', path: '',
name: 'labels_wrapper', name: 'labels_wrapper',
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
redirect: 'list', redirect: 'list',
}, },
{ {
path: 'list', path: 'list',
name: 'labels_list', name: 'labels_list',
roles: ['administrator'], meta: {
permissions: ['administrator'],
},
component: Index, component: Index,
}, },
], ],

View File

@ -23,19 +23,25 @@ export default {
path: '', path: '',
name: 'macros_wrapper', name: 'macros_wrapper',
component: Macros, component: Macros,
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
}, },
{ {
path: 'new', path: 'new',
name: 'macros_new', name: 'macros_new',
component: MacroEditor, component: MacroEditor,
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
}, },
{ {
path: ':macroId/edit', path: ':macroId/edit',
name: 'macros_edit', name: 'macros_edit',
component: MacroEditor, component: MacroEditor,
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
}, },
], ],
}, },

View File

@ -8,14 +8,18 @@ export default {
{ {
path: frontendURL('accounts/:accountId/profile'), path: frontendURL('accounts/:accountId/profile'),
name: 'profile_settings', name: 'profile_settings',
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
component: SettingsContent, component: SettingsContent,
children: [ children: [
{ {
path: 'settings', path: 'settings',
name: 'profile_settings_index', name: 'profile_settings_index',
component: Index, component: Index,
roles: ['administrator', 'agent'], meta: {
permissions: ['administrator', 'agent'],
},
}, },
], ],
}, },

View File

@ -1,6 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'; import Vuex from 'vuex';
import ReportsFiltersAgents from '../../Filters/Agents'; import ReportsFiltersAgents from '../../Filters/Agents.vue';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
@ -16,7 +16,7 @@ const mockStore = new Vuex.Store({
getAgents: state => state.agents, getAgents: state => state.agents,
}, },
actions: { actions: {
get: jest.fn(), get: vi.fn(),
}, },
}, },
}, },
@ -50,7 +50,7 @@ describe('ReportsFiltersAgents.vue', () => {
}); });
it('dispatches the "agents/get" action when the component is mounted', () => { it('dispatches the "agents/get" action when the component is mounted', () => {
const dispatchSpy = jest.spyOn(mockStore, 'dispatch'); const dispatchSpy = vi.spyOn(mockStore, 'dispatch');
shallowMount(ReportsFiltersAgents, mountParams); shallowMount(ReportsFiltersAgents, mountParams);

Some files were not shown because too many files have changed in this diff Show More