Compare commits
57
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b723ecccc | ||
|
|
71073a06ff | ||
|
|
168938500f | ||
|
|
39d20b197d | ||
|
|
fb99ba7b40 | ||
|
|
79aa5a5d7f | ||
|
|
2bb7d1e4b2 | ||
|
|
10ee773aac | ||
|
|
c5c3b74728 | ||
|
|
81d7c51a84 | ||
|
|
79381b08cc | ||
|
|
84c380c8c4 | ||
|
|
cb0642564c | ||
|
|
23e30fcb1a | ||
|
|
ae8619142f | ||
|
|
499408ea6f | ||
|
|
dc245814fe | ||
|
|
6c52a0638d | ||
|
|
64a9a46b28 | ||
|
|
30d4f6c2f5 | ||
|
|
5909c0f8b7 | ||
|
|
554388bff3 | ||
|
|
ebf3b46f0d | ||
|
|
5cfac31f93 | ||
|
|
787ac3134d | ||
|
|
2a06c85f1a | ||
|
|
acefd82378 | ||
|
|
abf376940d | ||
|
|
9de8c27368 | ||
|
|
9498d1f003 | ||
|
|
a2cb932d54 | ||
|
|
fe246698b6 | ||
|
|
31bcdaa3dd | ||
|
|
6ae606c981 | ||
|
|
aaf47b4c1f | ||
|
|
cc4851b19d | ||
|
|
5520bf68f3 | ||
|
|
46621b0983 | ||
|
|
2c94c89077 | ||
|
|
97de283103 | ||
|
|
7ed7c1b618 | ||
|
|
8f3234cf4b | ||
|
|
b2de6843f6 | ||
|
|
73c4180e64 | ||
|
|
96f4f50d2d | ||
|
|
f7f687ce53 | ||
|
|
5716bb29b8 | ||
|
|
e72a343612 | ||
|
|
ee2844877c | ||
|
|
66c6b8cd4f | ||
|
|
ef606204a2 | ||
|
|
3dee50d2b1 | ||
|
|
1f09af270c | ||
|
|
6b0a5bea43 | ||
|
|
c5979c4575 | ||
|
|
9046730206 | ||
|
|
7968e98529 |
@@ -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: |
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ SMTP_OPENSSL_VERIFY_MODE=peer
|
|||||||
# Comment out the following environment variables if required by your SMTP server
|
# Comment out the following environment variables if required by your SMTP server
|
||||||
# SMTP_TLS=
|
# SMTP_TLS=
|
||||||
# SMTP_SSL=
|
# SMTP_SSL=
|
||||||
|
# SMTP_OPEN_TIMEOUT
|
||||||
|
# SMTP_READ_TIMEOUT
|
||||||
|
|
||||||
# Mail Incoming
|
# Mail Incoming
|
||||||
# This is the domain set for the reply emails when conversation continuity is enabled
|
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||||
|
|||||||
+1
-1
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.2.2
|
3.3.3
|
||||||
|
|||||||
@@ -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.14.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.15.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'
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ group :development, :test do
|
|||||||
gem 'mock_redis'
|
gem 'mock_redis'
|
||||||
gem 'pry-rails'
|
gem 'pry-rails'
|
||||||
gem 'rspec_junit_formatter'
|
gem 'rspec_junit_formatter'
|
||||||
gem 'rspec-rails', '>= 6.0.3'
|
gem 'rspec-rails', '>= 6.1.3'
|
||||||
gem 'rubocop', require: false
|
gem 'rubocop', require: false
|
||||||
gem 'rubocop-performance', require: false
|
gem 'rubocop-performance', require: false
|
||||||
gem 'rubocop-rails', require: false
|
gem 'rubocop-rails', require: false
|
||||||
|
|||||||
+53
-45
@@ -103,8 +103,8 @@ GEM
|
|||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
acts-as-taggable-on (9.0.1)
|
acts-as-taggable-on (9.0.1)
|
||||||
activerecord (>= 6.0, < 7.1)
|
activerecord (>= 6.0, < 7.1)
|
||||||
addressable (2.8.4)
|
addressable (2.8.7)
|
||||||
public_suffix (>= 2.0.2, < 6.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
administrate (0.20.1)
|
administrate (0.20.1)
|
||||||
actionpack (>= 6.0, < 8.0)
|
actionpack (>= 6.0, < 8.0)
|
||||||
actionview (>= 6.0, < 8.0)
|
actionview (>= 6.0, < 8.0)
|
||||||
@@ -150,7 +150,7 @@ GEM
|
|||||||
statsd-ruby (~> 1.1)
|
statsd-ruby (~> 1.1)
|
||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
bigdecimal (3.1.7)
|
bigdecimal (3.1.8)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
bootsnap (1.16.0)
|
bootsnap (1.16.0)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
@@ -171,10 +171,13 @@ GEM
|
|||||||
commonmarker (0.23.10)
|
commonmarker (0.23.10)
|
||||||
concurrent-ruby (1.3.3)
|
concurrent-ruby (1.3.3)
|
||||||
connection_pool (2.4.1)
|
connection_pool (2.4.1)
|
||||||
crack (0.4.5)
|
crack (1.0.0)
|
||||||
|
bigdecimal
|
||||||
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 +186,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)
|
||||||
@@ -223,7 +229,7 @@ GEM
|
|||||||
http (>= 3.0)
|
http (>= 3.0)
|
||||||
ruby2_keywords
|
ruby2_keywords
|
||||||
email_reply_trimmer (0.1.13)
|
email_reply_trimmer (0.1.13)
|
||||||
erubi (1.12.0)
|
erubi (1.13.0)
|
||||||
et-orbi (1.2.7)
|
et-orbi (1.2.7)
|
||||||
tzinfo
|
tzinfo
|
||||||
execjs (2.8.1)
|
execjs (2.8.1)
|
||||||
@@ -353,7 +359,7 @@ GEM
|
|||||||
ruby2ruby (~> 2.4)
|
ruby2ruby (~> 2.4)
|
||||||
ruby_parser (~> 3.10)
|
ruby_parser (~> 3.10)
|
||||||
hana (1.3.7)
|
hana (1.3.7)
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.1.0)
|
||||||
hashie (5.0.0)
|
hashie (5.0.0)
|
||||||
http (5.1.1)
|
http (5.1.1)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
@@ -416,15 +422,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 +440,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 +467,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.23.1)
|
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)
|
||||||
@@ -489,14 +496,14 @@ GEM
|
|||||||
newrelic_rpm (9.6.0)
|
newrelic_rpm (9.6.0)
|
||||||
base64
|
base64
|
||||||
nio4r (2.7.3)
|
nio4r (2.7.3)
|
||||||
nokogiri (1.16.5)
|
nokogiri (1.16.6)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.5-arm64-darwin)
|
nokogiri (1.16.6-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.5-x86_64-darwin)
|
nokogiri (1.16.6-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.5-x86_64-linux)
|
nokogiri (1.16.6-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth (1.1.0)
|
oauth (1.1.0)
|
||||||
oauth-tty (~> 1.0, >= 1.0.1)
|
oauth-tty (~> 1.0, >= 1.0.1)
|
||||||
@@ -544,7 +551,7 @@ GEM
|
|||||||
method_source (~> 1.0)
|
method_source (~> 1.0)
|
||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (5.0.1)
|
public_suffix (6.0.0)
|
||||||
puma (6.4.2)
|
puma (6.4.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.3.0)
|
pundit (2.3.0)
|
||||||
@@ -603,7 +610,7 @@ GEM
|
|||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
redis (5.0.6)
|
redis (5.0.6)
|
||||||
redis-client (>= 0.9.0)
|
redis-client (>= 0.9.0)
|
||||||
redis-client (0.22.1)
|
redis-client (0.22.2)
|
||||||
connection_pool
|
connection_pool
|
||||||
redis-namespace (1.10.0)
|
redis-namespace (1.10.0)
|
||||||
redis (>= 4)
|
redis (>= 4)
|
||||||
@@ -627,17 +634,17 @@ GEM
|
|||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
reverse_markdown (2.1.1)
|
reverse_markdown (2.1.1)
|
||||||
nokogiri
|
nokogiri
|
||||||
rexml (3.2.8)
|
rexml (3.3.2)
|
||||||
strscan (>= 3.0.9)
|
strscan
|
||||||
rspec-core (3.13.0)
|
rspec-core (3.13.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-expectations (3.13.0)
|
rspec-expectations (3.13.1)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-mocks (3.13.1)
|
rspec-mocks (3.13.1)
|
||||||
diff-lcs (>= 1.2.0, < 2.0)
|
diff-lcs (>= 1.2.0, < 2.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
rspec-rails (6.1.2)
|
rspec-rails (6.1.3)
|
||||||
actionpack (>= 6.1)
|
actionpack (>= 6.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
railties (>= 6.1)
|
railties (>= 6.1)
|
||||||
@@ -703,23 +710,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.17.3)
|
sentry-rails (5.18.1)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
sentry-ruby (~> 5.17.3)
|
sentry-ruby (~> 5.18.1)
|
||||||
sentry-ruby (5.17.3)
|
sentry-ruby (5.18.1)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sentry-sidekiq (5.17.3)
|
sentry-sidekiq (5.18.1)
|
||||||
sentry-ruby (~> 5.17.3)
|
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)
|
||||||
@@ -802,7 +810,7 @@ GEM
|
|||||||
web-push (3.0.1)
|
web-push (3.0.1)
|
||||||
jwt (~> 2.0)
|
jwt (~> 2.0)
|
||||||
openssl (~> 3.0)
|
openssl (~> 3.0)
|
||||||
webmock (3.18.1)
|
webmock (3.23.1)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
@@ -819,7 +827,7 @@ GEM
|
|||||||
working_hours (1.4.1)
|
working_hours (1.4.1)
|
||||||
activesupport (>= 3.2)
|
activesupport (>= 3.2)
|
||||||
tzinfo
|
tzinfo
|
||||||
zeitwerk (2.6.15)
|
zeitwerk (2.6.16)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-20
|
arm64-darwin-20
|
||||||
@@ -922,7 +930,7 @@ DEPENDENCIES
|
|||||||
responders (>= 3.1.1)
|
responders (>= 3.1.1)
|
||||||
rest-client
|
rest-client
|
||||||
reverse_markdown
|
reverse_markdown
|
||||||
rspec-rails (>= 6.0.3)
|
rspec-rails (>= 6.1.3)
|
||||||
rspec_junit_formatter
|
rspec_junit_formatter
|
||||||
rubocop
|
rubocop
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
@@ -931,11 +939,11 @@ DEPENDENCIES
|
|||||||
scout_apm
|
scout_apm
|
||||||
scss_lint
|
scss_lint
|
||||||
seed_dump
|
seed_dump
|
||||||
sentry-rails (>= 5.14.0)
|
sentry-rails (>= 5.18.1)
|
||||||
sentry-ruby
|
sentry-ruby
|
||||||
sentry-sidekiq (>= 5.15.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 +968,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.16
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
|||||||
when 'email'
|
when 'email'
|
||||||
['MAILER_INBOUND_EMAIL_DOMAIN']
|
['MAILER_INBOUND_EMAIL_DOMAIN']
|
||||||
else
|
else
|
||||||
%w[ENABLE_ACCOUNT_SIGNUP]
|
%w[ENABLE_ACCOUNT_SIGNUP FIREBASE_PROJECT_ID FIREBASE_CREDENTIALS]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -111,27 +111,19 @@
|
|||||||
@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>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
import VirtualList from 'vue-virtual-scroll-list';
|
import VirtualList from 'vue-virtual-scroll-list';
|
||||||
|
|
||||||
import ChatListHeader from './ChatListHeader.vue';
|
import ChatListHeader from './ChatListHeader.vue';
|
||||||
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
|
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
|
||||||
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
||||||
import ConversationItem from './ConversationItem.vue';
|
import ConversationItem from './ConversationItem.vue';
|
||||||
import timeMixin from '../mixins/time';
|
|
||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import conversationMixin from '../mixins/conversations';
|
import conversationMixin from '../mixins/conversations';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
@@ -140,9 +132,7 @@ import filterQueryGenerator from '../helper/filterQueryGenerator.js';
|
|||||||
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
|
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
|
||||||
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
||||||
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
import filterMixin from 'shared/mixins/filterMixin';
|
import filterMixin from 'shared/mixins/filterMixin';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
|
||||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||||
import countries from 'shared/constants/countries';
|
import countries from 'shared/constants/countries';
|
||||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||||
@@ -152,10 +142,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,16 +156,8 @@ export default {
|
|||||||
ConversationBulkActions,
|
ConversationBulkActions,
|
||||||
IntersectionObserver,
|
IntersectionObserver,
|
||||||
VirtualList,
|
VirtualList,
|
||||||
CustomSnoozeModal,
|
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [conversationMixin, keyboardEventListenerMixins, filterMixin],
|
||||||
timeMixin,
|
|
||||||
conversationMixin,
|
|
||||||
keyboardEventListenerMixins,
|
|
||||||
alertMixin,
|
|
||||||
filterMixin,
|
|
||||||
uiSettingsMixin,
|
|
||||||
],
|
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
// Actions to be performed on virtual list item and context menu.
|
// Actions to be performed on virtual list item and context menu.
|
||||||
@@ -224,6 +202,13 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const { uiSettings } = useUISettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
uiSettings,
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
|
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
|
||||||
@@ -247,7 +232,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 +267,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 +500,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) {
|
||||||
@@ -833,7 +811,7 @@ export default {
|
|||||||
});
|
});
|
||||||
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
||||||
if (conversationId) {
|
if (conversationId) {
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t(
|
this.$t(
|
||||||
'CONVERSATION.CARD_CONTEXT_MENU.API.AGENT_ASSIGNMENT.SUCCESFUL',
|
'CONVERSATION.CARD_CONTEXT_MENU.API.AGENT_ASSIGNMENT.SUCCESFUL',
|
||||||
{
|
{
|
||||||
@@ -843,10 +821,10 @@ export default {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.showAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
|
useAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
|
useAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async assignPriority(priority, conversationId = null) {
|
async assignPriority(priority, conversationId = null) {
|
||||||
@@ -861,7 +839,7 @@ export default {
|
|||||||
newValue: priority,
|
newValue: priority,
|
||||||
from: 'Context menu',
|
from: 'Context menu',
|
||||||
});
|
});
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
|
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
|
||||||
priority,
|
priority,
|
||||||
conversationId,
|
conversationId,
|
||||||
@@ -904,7 +882,7 @@ export default {
|
|||||||
conversationId,
|
conversationId,
|
||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
});
|
});
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t(
|
this.$t(
|
||||||
'CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.SUCCESFUL',
|
'CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.SUCCESFUL',
|
||||||
{
|
{
|
||||||
@@ -914,7 +892,7 @@ export default {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t('CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.FAILED')
|
this.$t('CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.FAILED')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -931,7 +909,7 @@ export default {
|
|||||||
});
|
});
|
||||||
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
||||||
if (conversationId) {
|
if (conversationId) {
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t(
|
this.$t(
|
||||||
'CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_ASSIGNMENT.SUCCESFUL',
|
'CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_ASSIGNMENT.SUCCESFUL',
|
||||||
{
|
{
|
||||||
@@ -941,10 +919,10 @@ export default {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
|
useAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
|
useAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onAssignTeamsForBulk(team) {
|
async onAssignTeamsForBulk(team) {
|
||||||
@@ -957,9 +935,9 @@ export default {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
||||||
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
|
useAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
|
useAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onUpdateConversations(status, snoozedUntil) {
|
async onUpdateConversations(status, snoozedUntil) {
|
||||||
@@ -973,9 +951,9 @@ export default {
|
|||||||
snoozed_until: snoozedUntil,
|
snoozed_until: snoozedUntil,
|
||||||
});
|
});
|
||||||
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
||||||
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
|
useAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
|
useAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleConversationStatus(conversationId, status, snoozedUntil) {
|
toggleConversationStatus(conversationId, status, snoozedUntil) {
|
||||||
@@ -986,7 +964,7 @@ export default {
|
|||||||
snoozedUntil,
|
snoozedUntil,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -999,43 +977,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>
|
||||||
|
|||||||
@@ -25,10 +25,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import 'highlight.js/styles/default.css';
|
import 'highlight.js/styles/default.css';
|
||||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
script: {
|
script: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -61,7 +60,7 @@ export default {
|
|||||||
async onCopy(e) {
|
async onCopy(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await copyTextToClipboard(this.script);
|
await copyTextToClipboard(this.script);
|
||||||
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,10 +18,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import 'highlight.js/styles/default.css';
|
import 'highlight.js/styles/default.css';
|
||||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -37,7 +36,7 @@ export default {
|
|||||||
async onCopy(e) {
|
async onCopy(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await copyTextToClipboard(this.value);
|
await copyTextToClipboard(this.value);
|
||||||
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||||
},
|
},
|
||||||
toggleMasked() {
|
toggleMasked() {
|
||||||
this.masked = !this.masked;
|
this.masked = !this.masked;
|
||||||
|
|||||||
@@ -15,13 +15,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import WootSnackbar from './Snackbar.vue';
|
import WootSnackbar from './Snackbar.vue';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootSnackbar,
|
WootSnackbar,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
duration: {
|
duration: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -42,7 +40,7 @@ export default {
|
|||||||
this.$emitter.off('newToastMessage', this.onNewToastMessage);
|
this.$emitter.off('newToastMessage', this.onNewToastMessage);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onNewToastMessage(message, action) {
|
onNewToastMessage({ message, action }) {
|
||||||
this.snackMessages.push({
|
this.snackMessages.push({
|
||||||
key: new Date().getTime(),
|
key: new Date().getTime(),
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||||
|
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||||
import accountMixin from 'dashboard/mixins/account';
|
import accountMixin from 'dashboard/mixins/account';
|
||||||
|
|
||||||
const EMPTY_SUBSCRIPTION_INFO = {
|
const EMPTY_SUBSCRIPTION_INFO = {
|
||||||
@@ -22,7 +22,13 @@ const EMPTY_SUBSCRIPTION_INFO = {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { Banner },
|
components: { Banner },
|
||||||
mixins: [adminMixin, accountMixin],
|
mixins: [accountMixin],
|
||||||
|
setup() {
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
|
return {
|
||||||
|
isAdmin,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
||||||
|
|||||||
@@ -14,11 +14,11 @@
|
|||||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import accountMixin from 'dashboard/mixins/account';
|
import accountMixin from 'dashboard/mixins/account';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { Banner },
|
components: { Banner },
|
||||||
mixins: [accountMixin, alertMixin],
|
mixins: [accountMixin],
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
@@ -36,7 +36,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
resendVerificationEmail() {
|
resendVerificationEmail() {
|
||||||
this.$store.dispatch('resendConfirmation');
|
this.$store.dispatch('resendConfirmation');
|
||||||
this.showAlert(this.$t('APP_GLOBAL.EMAIL_VERIFICATION_SENT'));
|
useAlert(this.$t('APP_GLOBAL.EMAIL_VERIFICATION_SENT'));
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,15 +14,20 @@ import Banner from 'dashboard/components/ui/Banner.vue';
|
|||||||
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 { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||||
import { hasAnUpdateAvailable } from './versionCheckHelper';
|
import { hasAnUpdateAvailable } from './versionCheckHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { Banner },
|
components: { Banner },
|
||||||
mixins: [adminMixin],
|
|
||||||
props: {
|
props: {
|
||||||
latestChatwootVersion: { type: String, default: '' },
|
latestChatwootVersion: { type: String, default: '' },
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
|
return {
|
||||||
|
isAdmin,
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return { userDismissedBanner: false };
|
return { userDismissedBanner: false };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,13 +12,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
|
||||||
import accountMixin from 'dashboard/mixins/account';
|
import accountMixin from 'dashboard/mixins/account';
|
||||||
import { differenceInDays } from 'date-fns';
|
import { differenceInDays } from 'date-fns';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { Banner },
|
components: { Banner },
|
||||||
mixins: [adminMixin, accountMixin],
|
mixins: [accountMixin],
|
||||||
data() {
|
data() {
|
||||||
return { conversationMeta: {} };
|
return { conversationMeta: {} };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||||
@@ -94,7 +94,7 @@ export default {
|
|||||||
WootDropdownItem,
|
WootDropdownItem,
|
||||||
WootDropdownMenu,
|
WootDropdownMenu,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, keyboardEventListenerMixins],
|
mixins: [keyboardEventListenerMixins],
|
||||||
props: { conversationId: { type: [String, Number], required: true } },
|
props: { conversationId: { type: [String, Number], required: true } },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -209,7 +209,7 @@ export default {
|
|||||||
snoozedUntil,
|
snoozedUntil,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</woot-button>
|
</woot-button>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
<woot-dropdown-divider />
|
<woot-dropdown-divider />
|
||||||
<woot-dropdown-item class="m-0 flex items-center justify-between p-2">
|
<woot-dropdown-item class="flex items-center justify-between p-2 m-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
|
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="my-0 mx-1 text-xs font-medium text-slate-600 dark:text-slate-100"
|
class="mx-1 my-0 text-xs font-medium text-slate-600 dark:text-slate-100"
|
||||||
>
|
>
|
||||||
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
|
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
|
||||||
</span>
|
</span>
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
<woot-switch
|
<woot-switch
|
||||||
size="small"
|
size="small"
|
||||||
class="mt-px mx-1 mb-0"
|
class="mx-1 mt-px mb-0"
|
||||||
:value="currentUserAutoOffline"
|
:value="currentUserAutoOffline"
|
||||||
@input="updateAutoOffline"
|
@input="updateAutoOffline"
|
||||||
/>
|
/>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
|
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
|
||||||
@@ -65,9 +65,6 @@ export default {
|
|||||||
WootDropdownItem,
|
WootDropdownItem,
|
||||||
AvailabilityStatusBadge,
|
AvailabilityStatusBadge,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [alertMixin],
|
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isStatusMenuOpened: false,
|
isStatusMenuOpened: false,
|
||||||
@@ -129,7 +126,7 @@ export default {
|
|||||||
account_id: this.currentAccountId,
|
account_id: this.currentAccountId,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
|
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -30,21 +30,20 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import adminMixin from '../../mixins/isAdmin';
|
|
||||||
import { getSidebarItems } from './config/default-sidebar';
|
import { getSidebarItems } from './config/default-sidebar';
|
||||||
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: {
|
||||||
PrimarySidebar,
|
PrimarySidebar,
|
||||||
SecondarySidebar,
|
SecondarySidebar,
|
||||||
},
|
},
|
||||||
mixins: [adminMixin, alertMixin, keyboardEventListenerMixins],
|
mixins: [keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
showSecondarySidebar: {
|
showSecondarySidebar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -98,9 +97,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;
|
||||||
|
|||||||
@@ -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'],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -51,10 +51,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import { required, minLength } from 'vuelidate/lib/validators';
|
import { required, minLength } from 'vuelidate/lib/validators';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -88,13 +87,13 @@ export default {
|
|||||||
account_name: this.accountName,
|
account_name: this.accountName,
|
||||||
});
|
});
|
||||||
this.$emit('close-account-create-modal');
|
this.$emit('close-account-create-modal');
|
||||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
|
useAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
|
||||||
window.location = `/app/accounts/${account_id}/dashboard`;
|
window.location = `/app/accounts/${account_id}/dashboard`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.response.status === 422) {
|
if (error.response.status === 422) {
|
||||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
|
useAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
|
||||||
} else {
|
} else {
|
||||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
|
useAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,35 +65,36 @@
|
|||||||
: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>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||||
import adminMixin from '../../../mixins/isAdmin';
|
|
||||||
import configMixin from 'shared/mixins/configMixin';
|
import configMixin from 'shared/mixins/configMixin';
|
||||||
import {
|
import {
|
||||||
getInboxClassByType,
|
getInboxClassByType,
|
||||||
@@ -105,16 +106,23 @@ 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: [configMixin],
|
||||||
props: {
|
props: {
|
||||||
menuItem: {
|
menuItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
|
return {
|
||||||
|
isAdmin,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
activeInbox: 'getSelectedInbox',
|
activeInbox: 'getSelectedInbox',
|
||||||
|
|||||||
+4
-4
@@ -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);
|
||||||
|
|||||||
+3
-3
@@ -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);
|
||||||
|
|||||||
+4
-2
@@ -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: {
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+7
-7
@@ -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"', () => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
delay: { show: 1500, hide: 0 },
|
delay: { show: 1500, hide: 0 },
|
||||||
hideOnClick: true,
|
hideOnClick: true,
|
||||||
}"
|
}"
|
||||||
class="text-xxs text-slate-500 dark:text-slate-500 leading-4 ml-auto hover:text-slate-900 dark:hover:text-slate-100"
|
class="ml-auto leading-4 text-xxs text-slate-500 dark:text-slate-500 hover:text-slate-900 dark:hover:text-slate-100"
|
||||||
>
|
>
|
||||||
<span>{{ `${createdAtTime} • ${lastActivityTime}` }}</span>
|
<span>{{ `${createdAtTime} • ${lastActivityTime}` }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,11 +16,14 @@ const MINUTE_IN_MILLI_SECONDS = 60000;
|
|||||||
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
|
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
|
||||||
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
|
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
|
||||||
|
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import {
|
||||||
|
dynamicTime,
|
||||||
|
dateFormat,
|
||||||
|
shortTimestamp,
|
||||||
|
} from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TimeAgo',
|
name: 'TimeAgo',
|
||||||
mixins: [timeMixin],
|
|
||||||
props: {
|
props: {
|
||||||
isAutoRefreshEnabled: {
|
isAutoRefreshEnabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -37,17 +40,17 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
lastActivityAtTimeAgo: this.dynamicTime(this.lastActivityTimestamp),
|
lastActivityAtTimeAgo: dynamicTime(this.lastActivityTimestamp),
|
||||||
createdAtTimeAgo: this.dynamicTime(this.createdAtTimestamp),
|
createdAtTimeAgo: dynamicTime(this.createdAtTimestamp),
|
||||||
timer: null,
|
timer: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
lastActivityTime() {
|
lastActivityTime() {
|
||||||
return this.shortTimestamp(this.lastActivityAtTimeAgo);
|
return shortTimestamp(this.lastActivityAtTimeAgo);
|
||||||
},
|
},
|
||||||
createdAtTime() {
|
createdAtTime() {
|
||||||
return this.shortTimestamp(this.createdAtTimeAgo);
|
return shortTimestamp(this.createdAtTimeAgo);
|
||||||
},
|
},
|
||||||
createdAt() {
|
createdAt() {
|
||||||
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
|
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
|
||||||
@@ -56,9 +59,9 @@ export default {
|
|||||||
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
|
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
|
||||||
this.createdAtTimeAgo
|
this.createdAtTimeAgo
|
||||||
}`
|
}`
|
||||||
: `${this.$t(
|
: `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST')} ${dateFormat(
|
||||||
'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST'
|
this.createdAtTimestamp
|
||||||
)} ${this.dateFormat(this.createdAtTimestamp)}`;
|
)}`;
|
||||||
},
|
},
|
||||||
lastActivity() {
|
lastActivity() {
|
||||||
const lastActivityTimeDiff =
|
const lastActivityTimeDiff =
|
||||||
@@ -70,7 +73,7 @@ export default {
|
|||||||
}`
|
}`
|
||||||
: `${this.$t(
|
: `${this.$t(
|
||||||
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
|
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
|
||||||
)} ${this.dateFormat(this.lastActivityTimestamp)}`;
|
)} ${dateFormat(this.lastActivityTimestamp)}`;
|
||||||
},
|
},
|
||||||
tooltipText() {
|
tooltipText() {
|
||||||
return `${this.createdAt}
|
return `${this.createdAt}
|
||||||
@@ -79,10 +82,10 @@ export default {
|
|||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
lastActivityTimestamp() {
|
lastActivityTimestamp() {
|
||||||
this.lastActivityAtTimeAgo = this.dynamicTime(this.lastActivityTimestamp);
|
this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
|
||||||
},
|
},
|
||||||
createdAtTimestamp() {
|
createdAtTimestamp() {
|
||||||
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp);
|
this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -96,10 +99,8 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
createTimer() {
|
createTimer() {
|
||||||
this.timer = setTimeout(() => {
|
this.timer = setTimeout(() => {
|
||||||
this.lastActivityAtTimeAgo = this.dynamicTime(
|
this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
|
||||||
this.lastActivityTimestamp
|
this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
|
||||||
);
|
|
||||||
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp);
|
|
||||||
this.createTimer();
|
this.createTimer();
|
||||||
}, this.refreshTime());
|
}, this.refreshTime());
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,13 +35,13 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||||
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import AICTAModal from './AICTAModal.vue';
|
import AICTAModal from './AICTAModal.vue';
|
||||||
import AIAssistanceModal from './AIAssistanceModal.vue';
|
import AIAssistanceModal from './AIAssistanceModal.vue';
|
||||||
import adminMixin from 'dashboard/mixins/aiMixin';
|
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||||
import aiMixin from 'dashboard/mixins/isAdmin';
|
|
||||||
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
|
||||||
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
|
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -50,7 +50,17 @@ export default {
|
|||||||
AICTAModal,
|
AICTAModal,
|
||||||
AIAssistanceCTAButton,
|
AIAssistanceCTAButton,
|
||||||
},
|
},
|
||||||
mixins: [aiMixin, keyboardEventListenerMixins, adminMixin, uiSettingsMixin],
|
mixins: [aiMixin, keyboardEventListenerMixins],
|
||||||
|
setup() {
|
||||||
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
|
|
||||||
|
return {
|
||||||
|
uiSettings,
|
||||||
|
updateUISettings,
|
||||||
|
isAdmin,
|
||||||
|
};
|
||||||
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
showAIAssistanceModal: false,
|
showAIAssistanceModal: false,
|
||||||
showAICtaModal: false,
|
showAICtaModal: false,
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="px-0 min-w-0 flex-1">
|
<div class="flex-1 min-w-0 px-0">
|
||||||
<woot-modal-header
|
<woot-modal-header
|
||||||
:header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')"
|
:header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')"
|
||||||
:header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')"
|
:header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')"
|
||||||
/>
|
/>
|
||||||
<form
|
<form
|
||||||
class="flex flex-wrap flex-col modal-content"
|
class="flex flex-col flex-wrap modal-content"
|
||||||
@submit.prevent="finishOpenAI"
|
@submit.prevent="finishOpenAI"
|
||||||
>
|
>
|
||||||
<div class="mt-2 w-full">
|
<div class="w-full mt-2">
|
||||||
<woot-input
|
<woot-input
|
||||||
v-model="value"
|
v-model="value"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
@blur="$v.value.$touch"
|
@blur="$v.value.$touch"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row justify-between gap-2 py-2 px-0 w-full">
|
<div class="flex flex-row justify-between w-full gap-2 px-0 py-2">
|
||||||
<woot-button variant="link" @click.prevent="openOpenAIDoc">
|
<woot-button variant="link" @click.prevent="openOpenAIDoc">
|
||||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
|
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
@@ -39,13 +39,20 @@
|
|||||||
<script>
|
<script>
|
||||||
import { required } from 'vuelidate/lib/validators';
|
import { required } from 'vuelidate/lib/validators';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
|
||||||
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [aiMixin, alertMixin, uiSettingsMixin],
|
mixins: [aiMixin],
|
||||||
|
setup() {
|
||||||
|
const { updateUISettings } = useUISettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateUISettings,
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
value: '',
|
value: '',
|
||||||
@@ -67,7 +74,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onDismiss() {
|
onDismiss() {
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DISMISS_MESSAGE')
|
this.$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DISMISS_MESSAGE')
|
||||||
);
|
);
|
||||||
this.updateUISettings({
|
this.updateUISettings({
|
||||||
@@ -97,7 +104,7 @@ export default {
|
|||||||
this.alertMessage =
|
this.alertMessage =
|
||||||
errorMessage || this.$t('INTEGRATION_APPS.ADD.API.ERROR_MESSAGE');
|
errorMessage || this.$t('INTEGRATION_APPS.ADD.API.ERROR_MESSAGE');
|
||||||
} finally {
|
} finally {
|
||||||
this.showAlert(this.alertMessage);
|
useAlert(this.alertMessage);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openOpenAIDoc() {
|
openOpenAIDoc() {
|
||||||
|
|||||||
@@ -26,13 +26,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
import Spinner from 'shared/components/Spinner.vue';
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Spinner,
|
Spinner,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -71,7 +70,7 @@ export default {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.uploadState = 'failed';
|
this.uploadState = 'failed';
|
||||||
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOAD_FAILED');
|
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOAD_FAILED');
|
||||||
this.showAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
|
useAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
|
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
|
||||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -41,7 +41,7 @@ export default {
|
|||||||
LabelDropdown,
|
LabelDropdown,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [adminMixin, keyboardEventListenerMixins],
|
mixins: [keyboardEventListenerMixins],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
allLabels: {
|
allLabels: {
|
||||||
@@ -54,6 +54,13 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
|
return {
|
||||||
|
isAdmin,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showSearchDropdownLabel: false,
|
showSearchDropdownLabel: false,
|
||||||
|
|||||||
@@ -15,10 +15,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import DyteAPI from 'dashboard/api/integrations/dyte';
|
import DyteAPI from 'dashboard/api/integrations/dyte';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
conversationId: {
|
conversationId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -47,7 +46,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
await DyteAPI.createAMeeting(this.conversationId);
|
await DyteAPI.createAMeeting(this.conversationId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR'));
|
useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR'));
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import 'videojs-record/dist/css/videojs.record.css';
|
|||||||
|
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
|
|
||||||
import alertMixin from '../../../../shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
import Recorder from 'opus-recorder';
|
import Recorder from 'opus-recorder';
|
||||||
|
|
||||||
@@ -50,7 +50,6 @@ const RECORDER_CONFIG = {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WootAudioRecorder',
|
name: 'WootAudioRecorder',
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
audioRecordFormat: {
|
audioRecordFormat: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -188,14 +187,10 @@ export default {
|
|||||||
deviceErrorName?.includes('notallowederror') ||
|
deviceErrorName?.includes('notallowederror') ||
|
||||||
deviceErrorName?.includes('permissiondeniederror')
|
deviceErrorName?.includes('permissiondeniederror')
|
||||||
) {
|
) {
|
||||||
this.showAlert(
|
useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION'));
|
||||||
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION')
|
|
||||||
);
|
|
||||||
this.fireStateRecorderChanged('notallowederror');
|
this.fireStateRecorderChanged('notallowederror');
|
||||||
} else {
|
} else {
|
||||||
this.showAlert(
|
useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR'));
|
||||||
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formatTimeProgress() {
|
formatTimeProgress() {
|
||||||
|
|||||||
@@ -80,8 +80,7 @@ import {
|
|||||||
hasPressedCommandAndEnter,
|
hasPressedCommandAndEnter,
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
|
||||||
import {
|
import {
|
||||||
replaceVariablesInMessage,
|
replaceVariablesInMessage,
|
||||||
createTypingIndicator,
|
createTypingIndicator,
|
||||||
@@ -89,7 +88,7 @@ import {
|
|||||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import {
|
import {
|
||||||
MESSAGE_EDITOR_MENU_OPTIONS,
|
MESSAGE_EDITOR_MENU_OPTIONS,
|
||||||
MESSAGE_EDITOR_IMAGE_RESIZES,
|
MESSAGE_EDITOR_IMAGE_RESIZES,
|
||||||
@@ -119,7 +118,7 @@ const createState = (
|
|||||||
export default {
|
export default {
|
||||||
name: 'WootMessageEditor',
|
name: 'WootMessageEditor',
|
||||||
components: { TagAgents, CannedResponse, VariableList },
|
components: { TagAgents, CannedResponse, VariableList },
|
||||||
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
|
mixins: [keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
value: { type: String, default: '' },
|
value: { type: String, default: '' },
|
||||||
editorId: { type: String, default: '' },
|
editorId: { type: String, default: '' },
|
||||||
@@ -139,6 +138,19 @@ export default {
|
|||||||
channelType: { type: String, default: '' },
|
channelType: { type: String, default: '' },
|
||||||
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const {
|
||||||
|
uiSettings,
|
||||||
|
isEditorHotKeyEnabled,
|
||||||
|
fetchSignatureFlagFromUISettings,
|
||||||
|
} = useUISettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
uiSettings,
|
||||||
|
isEditorHotKeyEnabled,
|
||||||
|
fetchSignatureFlagFromUISettings,
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
typingIndicator: createTypingIndicator(
|
typingIndicator: createTypingIndicator(
|
||||||
@@ -278,7 +290,7 @@ export default {
|
|||||||
// this is considered the source of truth, we watch this property
|
// this is considered the source of truth, we watch this property
|
||||||
// on change, we toggle the signature in the editor
|
// on change, we toggle the signature in the editor
|
||||||
if (this.allowSignature && !this.isPrivate && this.channelType) {
|
if (this.allowSignature && !this.isPrivate && this.channelType) {
|
||||||
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
return this.fetchSignatureFlagFromUISettings(this.channelType);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -521,10 +533,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
isEnterToSendEnabled() {
|
isEnterToSendEnabled() {
|
||||||
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
|
return this.isEditorHotKeyEnabled('enter');
|
||||||
},
|
},
|
||||||
isCmdPlusEnterToSendEnabled() {
|
isCmdPlusEnterToSendEnabled() {
|
||||||
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter');
|
return this.isEditorHotKeyEnabled('cmd_enter');
|
||||||
},
|
},
|
||||||
getKeyboardEvents() {
|
getKeyboardEvents() {
|
||||||
return {
|
return {
|
||||||
@@ -611,7 +623,7 @@ export default {
|
|||||||
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
||||||
this.uploadImageToStorage(file);
|
this.uploadImageToStorage(file);
|
||||||
} else {
|
} else {
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t(
|
this.$t(
|
||||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR',
|
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR',
|
||||||
{
|
{
|
||||||
@@ -629,13 +641,13 @@ export default {
|
|||||||
if (fileUrl) {
|
if (fileUrl) {
|
||||||
this.onImageInsertInEditor(fileUrl);
|
this.onImageInsertInEditor(fileUrl);
|
||||||
}
|
}
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t(
|
this.$t(
|
||||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SUCCESS'
|
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SUCCESS'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t(
|
this.$t(
|
||||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_ERROR'
|
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_ERROR'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ import {
|
|||||||
Selection,
|
Selection,
|
||||||
} from '@chatwoot/prosemirror-schema';
|
} from '@chatwoot/prosemirror-schema';
|
||||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
|
||||||
|
|
||||||
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||||
const createState = (
|
const createState = (
|
||||||
@@ -51,13 +51,21 @@ const createState = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
|
mixins: [keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
value: { type: String, default: '' },
|
value: { type: String, default: '' },
|
||||||
editorId: { type: String, default: '' },
|
editorId: { type: String, default: '' },
|
||||||
placeholder: { type: String, default: '' },
|
placeholder: { type: String, default: '' },
|
||||||
enabledMenuOptions: { type: Array, default: () => [] },
|
enabledMenuOptions: { type: Array, default: () => [] },
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
uiSettings,
|
||||||
|
updateUISettings,
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
editorView: null,
|
editorView: null,
|
||||||
@@ -108,7 +116,7 @@ export default {
|
|||||||
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
||||||
this.uploadImageToStorage(file);
|
this.uploadImageToStorage(file);
|
||||||
} else {
|
} else {
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR_FILE_SIZE', {
|
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR_FILE_SIZE', {
|
||||||
size: MAXIMUM_FILE_UPLOAD_SIZE,
|
size: MAXIMUM_FILE_UPLOAD_SIZE,
|
||||||
})
|
})
|
||||||
@@ -127,13 +135,9 @@ export default {
|
|||||||
if (fileUrl) {
|
if (fileUrl) {
|
||||||
this.onImageUploadStart(fileUrl);
|
this.onImageUploadStart(fileUrl);
|
||||||
}
|
}
|
||||||
this.showAlert(
|
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS'));
|
||||||
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS')
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(
|
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR'));
|
||||||
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onImageUploadStart(fileUrl) {
|
onImageUploadStart(fileUrl) {
|
||||||
|
|||||||
@@ -134,10 +134,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import FileUpload from 'vue-upload-component';
|
import FileUpload from 'vue-upload-component';
|
||||||
import * as ActiveStorage from 'activestorage';
|
import * as ActiveStorage from 'activestorage';
|
||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
import {
|
import {
|
||||||
@@ -153,7 +153,7 @@ import { mapGetters } from 'vuex';
|
|||||||
export default {
|
export default {
|
||||||
name: 'ReplyBottomPanel',
|
name: 'ReplyBottomPanel',
|
||||||
components: { FileUpload, VideoCallButton, AIAssistanceButton },
|
components: { FileUpload, VideoCallButton, AIAssistanceButton },
|
||||||
mixins: [keyboardEventListenerMixins, uiSettingsMixin, inboxMixin],
|
mixins: [keyboardEventListenerMixins, inboxMixin],
|
||||||
props: {
|
props: {
|
||||||
mode: {
|
mode: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -248,6 +248,15 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
|
||||||
|
useUISettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
setSignatureFlagForInbox,
|
||||||
|
fetchSignatureFlagFromUISettings,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
@@ -320,7 +329,7 @@ export default {
|
|||||||
},
|
},
|
||||||
sendWithSignature() {
|
sendWithSignature() {
|
||||||
// channelType is sourced from inboxMixin
|
// channelType is sourced from inboxMixin
|
||||||
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
return this.fetchSignatureFlagFromUISettings(this.channelType);
|
||||||
},
|
},
|
||||||
signatureToggleTooltip() {
|
signatureToggleTooltip() {
|
||||||
return this.sendWithSignature
|
return this.sendWithSignature
|
||||||
|
|||||||
+5
-4
@@ -79,7 +79,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { required, requiredIf } from 'vuelidate/lib/validators';
|
import { required, requiredIf } from 'vuelidate/lib/validators';
|
||||||
import FilterInputBox from '../FilterInput/Index.vue';
|
import FilterInputBox from '../FilterInput/Index.vue';
|
||||||
import languages from './advancedFilterItems/languages';
|
import languages from './advancedFilterItems/languages';
|
||||||
@@ -89,12 +89,13 @@ import { filterAttributeGroups } from './advancedFilterItems';
|
|||||||
import filterMixin from 'shared/mixins/filterMixin';
|
import filterMixin from 'shared/mixins/filterMixin';
|
||||||
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
|
||||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
|
import cloneObject from 'dashboard/helpers/clone';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FilterInputBox,
|
FilterInputBox,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, filterMixin],
|
mixins: [filterMixin],
|
||||||
props: {
|
props: {
|
||||||
onClose: {
|
onClose: {
|
||||||
type: Function,
|
type: Function,
|
||||||
@@ -341,7 +342,7 @@ export default {
|
|||||||
},
|
},
|
||||||
removeFilter(index) {
|
removeFilter(index) {
|
||||||
if (this.appliedFilters.length <= 1) {
|
if (this.appliedFilters.length <= 1) {
|
||||||
this.showAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
|
useAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
|
||||||
} else {
|
} else {
|
||||||
this.appliedFilters.splice(index, 1);
|
this.appliedFilters.splice(index, 1);
|
||||||
}
|
}
|
||||||
@@ -351,7 +352,7 @@ export default {
|
|||||||
if (this.$v.$invalid) return;
|
if (this.$v.$invalid) return;
|
||||||
this.$store.dispatch(
|
this.$store.dispatch(
|
||||||
'setConversationFilters',
|
'setConversationFilters',
|
||||||
JSON.parse(JSON.stringify(this.appliedFilters))
|
cloneObject(this.appliedFilters)
|
||||||
);
|
);
|
||||||
this.$emit('applyFilter', this.appliedFilters);
|
this.$emit('applyFilter', this.appliedFilters);
|
||||||
this.$track(CONVERSATION_EVENTS.APPLY_FILTER, {
|
this.$track(CONVERSATION_EVENTS.APPLY_FILTER, {
|
||||||
|
|||||||
+13
-7
@@ -12,10 +12,10 @@
|
|||||||
<div
|
<div
|
||||||
v-if="showActionsDropdown"
|
v-if="showActionsDropdown"
|
||||||
v-on-clickaway="closeDropdown"
|
v-on-clickaway="closeDropdown"
|
||||||
class="dropdown-pane dropdown-pane--open mt-1 right-0 basic-filter"
|
class="right-0 mt-1 dropdown-pane dropdown-pane--open basic-filter"
|
||||||
>
|
>
|
||||||
<div class="items-center flex justify-between last:mt-4">
|
<div class="flex items-center justify-between last:mt-4">
|
||||||
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
|
<span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
|
||||||
$t('CHAT_LIST.CHAT_SORT.STATUS')
|
$t('CHAT_LIST.CHAT_SORT.STATUS')
|
||||||
}}</span>
|
}}</span>
|
||||||
<filter-item
|
<filter-item
|
||||||
@@ -26,8 +26,8 @@
|
|||||||
@onChangeFilter="onChangeFilter"
|
@onChangeFilter="onChangeFilter"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="items-center flex justify-between last:mt-4">
|
<div class="flex items-center justify-between last:mt-4">
|
||||||
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
|
<span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
|
||||||
$t('CHAT_LIST.CHAT_SORT.ORDER_BY')
|
$t('CHAT_LIST.CHAT_SORT.ORDER_BY')
|
||||||
}}</span>
|
}}</span>
|
||||||
<filter-item
|
<filter-item
|
||||||
@@ -46,13 +46,19 @@
|
|||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import FilterItem from './FilterItem.vue';
|
import FilterItem from './FilterItem.vue';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FilterItem,
|
FilterItem,
|
||||||
},
|
},
|
||||||
mixins: [uiSettingsMixin],
|
setup() {
|
||||||
|
const { updateUISettings } = useUISettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateUISettings,
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showActionsDropdown: false,
|
showActionsDropdown: false,
|
||||||
|
|||||||
@@ -120,13 +120,11 @@ import { mapGetters } from 'vuex';
|
|||||||
import Thumbnail from '../Thumbnail.vue';
|
import Thumbnail from '../Thumbnail.vue';
|
||||||
import MessagePreview from './MessagePreview.vue';
|
import MessagePreview from './MessagePreview.vue';
|
||||||
import conversationMixin from '../../../mixins/conversations';
|
import conversationMixin from '../../../mixins/conversations';
|
||||||
import timeMixin from '../../../mixins/time';
|
|
||||||
import router from '../../../routes';
|
import router from '../../../routes';
|
||||||
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
|
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
|
||||||
import InboxName from '../InboxName.vue';
|
import InboxName from '../InboxName.vue';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
import ConversationContextMenu from './contextMenu/Index.vue';
|
import ConversationContextMenu from './contextMenu/Index.vue';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
|
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
|
||||||
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
||||||
import PriorityMark from './PriorityMark.vue';
|
import PriorityMark from './PriorityMark.vue';
|
||||||
@@ -143,8 +141,7 @@ export default {
|
|||||||
PriorityMark,
|
PriorityMark,
|
||||||
SLACardLabel,
|
SLACardLabel,
|
||||||
},
|
},
|
||||||
|
mixins: [inboxMixin, conversationMixin],
|
||||||
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
activeLabel: {
|
activeLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<!-- eslint-disable vue/no-mutating-props -->
|
<!-- eslint-disable vue/no-mutating-props -->
|
||||||
<template>
|
<template>
|
||||||
<woot-modal :show.sync="show" :on-close="onCancel">
|
<woot-modal :show.sync="show" :on-close="onCancel">
|
||||||
<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('EMAIL_TRANSCRIPT.TITLE')"
|
:header-title="$t('EMAIL_TRANSCRIPT.TITLE')"
|
||||||
:header-content="$t('EMAIL_TRANSCRIPT.DESC')"
|
:header-content="$t('EMAIL_TRANSCRIPT.DESC')"
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
||||||
<woot-submit-button
|
<woot-submit-button
|
||||||
:button-text="$t('EMAIL_TRANSCRIPT.SUBMIT')"
|
:button-text="$t('EMAIL_TRANSCRIPT.SUBMIT')"
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid"
|
||||||
@@ -77,9 +77,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
export default {
|
export default {
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -142,10 +141,10 @@ export default {
|
|||||||
email: this.selectedEmailAddress,
|
email: this.selectedEmailAddress,
|
||||||
conversationId: this.currentChat.id,
|
conversationId: this.currentChat.id,
|
||||||
});
|
});
|
||||||
this.showAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'));
|
useAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'));
|
||||||
this.onCancel();
|
this.onCancel();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'));
|
useAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'));
|
||||||
} finally {
|
} finally {
|
||||||
this.isSubmitting = false;
|
this.isSubmitting = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||||
import accountMixin from 'dashboard/mixins/account';
|
import accountMixin from 'dashboard/mixins/account';
|
||||||
import OnboardingView from '../OnboardingView.vue';
|
import OnboardingView from '../OnboardingView.vue';
|
||||||
import EmptyStateMessage from './EmptyStateMessage.vue';
|
import EmptyStateMessage from './EmptyStateMessage.vue';
|
||||||
@@ -45,13 +45,19 @@ export default {
|
|||||||
OnboardingView,
|
OnboardingView,
|
||||||
EmptyStateMessage,
|
EmptyStateMessage,
|
||||||
},
|
},
|
||||||
mixins: [accountMixin, adminMixin],
|
mixins: [accountMixin],
|
||||||
props: {
|
props: {
|
||||||
isOnExpandedLayout: {
|
isOnExpandedLayout: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
|
return {
|
||||||
|
isAdmin,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentChat: 'getSelectedChat',
|
currentChat: 'getSelectedChat',
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -157,8 +157,7 @@ import ContextMenu from 'dashboard/modules/conversations/components/MessageConte
|
|||||||
import InstagramStory from './bubble/InstagramStory.vue';
|
import InstagramStory from './bubble/InstagramStory.vue';
|
||||||
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
|
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
|
||||||
import Spinner from 'shared/components/Spinner.vue';
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { CONTENT_TYPES } from 'shared/constants/contentType';
|
||||||
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
|
||||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||||
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
@@ -166,6 +165,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: {
|
||||||
@@ -183,7 +183,7 @@ export default {
|
|||||||
InstagramStoryReply,
|
InstagramStoryReply,
|
||||||
Spinner,
|
Spinner,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
|
mixins: [messageFormatterMixin],
|
||||||
props: {
|
props: {
|
||||||
data: {
|
data: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -257,7 +257,16 @@ export default {
|
|||||||
html_content: { full: fullHTMLContent } = {},
|
html_content: { full: fullHTMLContent } = {},
|
||||||
text_content: { full: fullTextContent } = {},
|
text_content: { full: fullTextContent } = {},
|
||||||
} = this.contentAttributes.email || {};
|
} = this.contentAttributes.email || {};
|
||||||
return fullHTMLContent || fullTextContent || '';
|
|
||||||
|
if (fullHTMLContent) {
|
||||||
|
return fullHTMLContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullTextContent) {
|
||||||
|
return fullTextContent.replace(/\n/g, '<br>');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
},
|
},
|
||||||
displayQuotedButton() {
|
displayQuotedButton() {
|
||||||
if (this.emailMessageContent.includes('<blockquote')) {
|
if (this.emailMessageContent.includes('<blockquote')) {
|
||||||
@@ -472,6 +481,9 @@ export default {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
isEmailContentType() {
|
||||||
|
return this.contentType === CONTENT_TYPES.INCOMING_EMAIL;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
data() {
|
data() {
|
||||||
@@ -493,15 +505,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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex actions--container relative items-center gap-2">
|
<div class="relative flex items-center gap-2 actions--container">
|
||||||
<woot-button
|
<woot-button
|
||||||
v-if="!currentChat.muted"
|
v-if="!currentChat.muted"
|
||||||
v-tooltip="$t('CONTACT_PANEL.MUTE_CONTACT')"
|
v-tooltip="$t('CONTACT_PANEL.MUTE_CONTACT')"
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import EmailTranscriptModal from './EmailTranscriptModal.vue';
|
import EmailTranscriptModal from './EmailTranscriptModal.vue';
|
||||||
import ResolveAction from '../../buttons/ResolveAction.vue';
|
import ResolveAction from '../../buttons/ResolveAction.vue';
|
||||||
import {
|
import {
|
||||||
@@ -51,7 +51,6 @@ export default {
|
|||||||
EmailTranscriptModal,
|
EmailTranscriptModal,
|
||||||
ResolveAction,
|
ResolveAction,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin],
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showEmailActionsModal: false,
|
showEmailActionsModal: false,
|
||||||
@@ -73,11 +72,11 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
mute() {
|
mute() {
|
||||||
this.$store.dispatch('muteConversation', this.currentChat.id);
|
this.$store.dispatch('muteConversation', this.currentChat.id);
|
||||||
this.showAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS'));
|
useAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS'));
|
||||||
},
|
},
|
||||||
unmute() {
|
unmute() {
|
||||||
this.$store.dispatch('unmuteConversation', this.currentChat.id);
|
this.$store.dispatch('unmuteConversation', this.currentChat.id);
|
||||||
this.showAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS'));
|
useAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS'));
|
||||||
},
|
},
|
||||||
toggleEmailActionsModal() {
|
toggleEmailActionsModal() {
|
||||||
this.showEmailActionsModal = !this.showEmailActionsModal;
|
this.showEmailActionsModal = !this.showEmailActionsModal;
|
||||||
|
|||||||
@@ -153,7 +153,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
|
|
||||||
import CannedResponse from './CannedResponse.vue';
|
import CannedResponse from './CannedResponse.vue';
|
||||||
@@ -180,10 +181,8 @@ import {
|
|||||||
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||||
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
|
||||||
import { trimContent, debounce } from '@chatwoot/utils';
|
import { trimContent, debounce } from '@chatwoot/utils';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
|
||||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||||
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
||||||
@@ -197,7 +196,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: {
|
||||||
@@ -218,8 +217,6 @@ export default {
|
|||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
inboxMixin,
|
inboxMixin,
|
||||||
uiSettingsMixin,
|
|
||||||
alertMixin,
|
|
||||||
messageFormatterMixin,
|
messageFormatterMixin,
|
||||||
rtlMixin,
|
rtlMixin,
|
||||||
fileUploadMixin,
|
fileUploadMixin,
|
||||||
@@ -231,6 +228,21 @@ export default {
|
|||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const {
|
||||||
|
uiSettings,
|
||||||
|
updateUISettings,
|
||||||
|
isEditorHotKeyEnabled,
|
||||||
|
fetchSignatureFlagFromUISettings,
|
||||||
|
} = useUISettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
uiSettings,
|
||||||
|
updateUISettings,
|
||||||
|
isEditorHotKeyEnabled,
|
||||||
|
fetchSignatureFlagFromUISettings,
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
message: '',
|
message: '',
|
||||||
@@ -310,7 +322,7 @@ export default {
|
|||||||
agentId,
|
agentId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.showAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
|
useAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -406,7 +418,7 @@ export default {
|
|||||||
if (this.isPrivate) {
|
if (this.isPrivate) {
|
||||||
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
|
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
|
||||||
}
|
}
|
||||||
const keyLabel = isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter')
|
const keyLabel = this.isEditorHotKeyEnabled('cmd_enter')
|
||||||
? '(⌘ + ↵)'
|
? '(⌘ + ↵)'
|
||||||
: '(↵)';
|
: '(↵)';
|
||||||
return `${sendMessageText} ${keyLabel}`;
|
return `${sendMessageText} ${keyLabel}`;
|
||||||
@@ -479,7 +491,7 @@ export default {
|
|||||||
return !!this.signatureToApply;
|
return !!this.signatureToApply;
|
||||||
},
|
},
|
||||||
sendWithSignature() {
|
sendWithSignature() {
|
||||||
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
return this.fetchSignatureFlagFromUISettings(this.channelType);
|
||||||
},
|
},
|
||||||
editorMessageKey() {
|
editorMessageKey() {
|
||||||
const { editor_message_key: isEnabled } = this.uiSettings;
|
const { editor_message_key: isEnabled } = this.uiSettings;
|
||||||
@@ -748,7 +760,7 @@ export default {
|
|||||||
!this.showCannedMenu &&
|
!this.showCannedMenu &&
|
||||||
!this.showVariablesMenu &&
|
!this.showVariablesMenu &&
|
||||||
this.isFocused &&
|
this.isFocused &&
|
||||||
isEditorHotKeyEnabled(this.uiSettings, selectedKey)
|
this.isEditorHotKeyEnabled(selectedKey)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onPaste(e) {
|
onPaste(e) {
|
||||||
@@ -880,7 +892,7 @@ export default {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
|
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
|
||||||
this.showAlert(errorMessage);
|
useAlert(errorMessage);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onSendWhatsAppReply(messagePayload) {
|
async onSendWhatsAppReply(messagePayload) {
|
||||||
|
|||||||
@@ -77,10 +77,10 @@
|
|||||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import timeMixin from '../../../../mixins/time';
|
import { messageTimestamp } from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [inboxMixin, timeMixin],
|
mixins: [inboxMixin],
|
||||||
props: {
|
props: {
|
||||||
sender: {
|
sender: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -159,7 +159,7 @@ export default {
|
|||||||
return MESSAGE_STATUS.SENT === this.messageStatus;
|
return MESSAGE_STATUS.SENT === this.messageStatus;
|
||||||
},
|
},
|
||||||
readableTime() {
|
readableTime() {
|
||||||
return this.messageTimestamp(this.createdAt, 'LLL d, h:mm a');
|
return messageTimestamp(this.createdAt, 'LLL d, h:mm a');
|
||||||
},
|
},
|
||||||
screenName() {
|
screenName() {
|
||||||
const { additional_attributes: additionalAttributes = {} } =
|
const { additional_attributes: additionalAttributes = {} } =
|
||||||
|
|||||||
@@ -17,14 +17,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
import {
|
import {
|
||||||
DuplicateContactException,
|
DuplicateContactException,
|
||||||
ExceptionWithMessage,
|
ExceptionWithMessage,
|
||||||
} from 'shared/helpers/CustomErrors';
|
} from 'shared/helpers/CustomErrors';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -52,18 +51,18 @@ export default {
|
|||||||
'contacts/create',
|
'contacts/create',
|
||||||
this.getContactObject()
|
this.getContactObject()
|
||||||
);
|
);
|
||||||
this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||||
}
|
}
|
||||||
this.openContactNewTab(contact.id);
|
this.openContactNewTab(contact.id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DuplicateContactException) {
|
if (error instanceof DuplicateContactException) {
|
||||||
if (error.data.includes('phone_number')) {
|
if (error.data.includes('phone_number')) {
|
||||||
this.showAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
|
useAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
|
||||||
}
|
}
|
||||||
} else if (error instanceof ExceptionWithMessage) {
|
} else if (error instanceof ExceptionWithMessage) {
|
||||||
this.showAlert(error.data);
|
useAlert(error.data);
|
||||||
} else {
|
} else {
|
||||||
this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-3
@@ -31,10 +31,9 @@
|
|||||||
<script>
|
<script>
|
||||||
import DyteAPI from 'dashboard/api/integrations/dyte';
|
import DyteAPI from 'dashboard/api/integrations/dyte';
|
||||||
import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
|
import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
messageId: {
|
messageId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -61,7 +60,7 @@ export default {
|
|||||||
await DyteAPI.addParticipantToMeeting(this.messageId);
|
await DyteAPI.addParticipantToMeeting(this.messageId);
|
||||||
this.dyteAuthToken = authToken;
|
this.dyteAuthToken = authToken;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
|
useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
@click="onClose"
|
@click="onClose"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-slate-900 z-10 flex items-center justify-between w-full h-16 px-6 py-2"
|
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-white dark:bg-slate-900"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -167,7 +167,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center w-full h-16 px-6 py-2 z-10">
|
<div class="z-10 flex items-center justify-center w-full h-16 px-6 py-2">
|
||||||
<div
|
<div
|
||||||
class="items-center rounded-sm flex font-semibold justify-center min-w-[5rem] p-1 bg-slate-25 dark:bg-slate-800 text-slate-600 dark:text-slate-200 text-sm"
|
class="items-center rounded-sm flex font-semibold justify-center min-w-[5rem] p-1 bg-slate-25 dark:bg-slate-800 text-slate-600 dark:text-slate-200 text-sm"
|
||||||
>
|
>
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { messageTimestamp } from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
},
|
},
|
||||||
mixins: [keyboardEventListenerMixins, timeMixin],
|
mixins: [keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -236,7 +236,7 @@ export default {
|
|||||||
readableTime() {
|
readableTime() {
|
||||||
const { created_at: createdAt } = this.activeAttachment;
|
const { created_at: createdAt } = this.activeAttachment;
|
||||||
if (!createdAt) return '';
|
if (!createdAt) return '';
|
||||||
return this.messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || '';
|
return messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || '';
|
||||||
},
|
},
|
||||||
isImage() {
|
isImage() {
|
||||||
return this.activeFileType === ALLOWED_FILE_TYPES.IMAGE;
|
return this.activeFileType === ALLOWED_FILE_TYPES.IMAGE;
|
||||||
|
|||||||
+21
-22
@@ -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 } },
|
||||||
@@ -85,11 +85,11 @@ describe('MoveActions', () => {
|
|||||||
it('shows alert', async () => {
|
it('shows alert', async () => {
|
||||||
await moreActions.find('button:first-child').trigger('click');
|
await moreActions.find('button:first-child').trigger('click');
|
||||||
|
|
||||||
expect(emitter.emit).toBeCalledWith(
|
expect(emitter.emit).toBeCalledWith('newToastMessage', {
|
||||||
'newToastMessage',
|
message:
|
||||||
'This contact is blocked successfully. You will not be notified of any future conversations.',
|
'This contact is blocked successfully. You will not be notified of any future conversations.',
|
||||||
undefined
|
action: null,
|
||||||
);
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,11 +111,10 @@ describe('MoveActions', () => {
|
|||||||
it('shows alert', async () => {
|
it('shows alert', async () => {
|
||||||
await moreActions.find('button:first-child').trigger('click');
|
await moreActions.find('button:first-child').trigger('click');
|
||||||
|
|
||||||
expect(emitter.emit).toBeCalledWith(
|
expect(emitter.emit).toBeCalledWith('newToastMessage', {
|
||||||
'newToastMessage',
|
message: 'This contact is unblocked successfully.',
|
||||||
'This contact is unblocked successfully.',
|
action: null,
|
||||||
undefined
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+4
-4
@@ -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) {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { getCurrentInstance } from 'vue';
|
import { getCurrentInstance } from 'vue';
|
||||||
import { emitter } from 'shared/helpers/mitt';
|
import { emitter } from 'shared/helpers/mitt';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to track events
|
||||||
|
* @returns {Function} The track function
|
||||||
|
*/
|
||||||
export const useTrack = () => {
|
export const useTrack = () => {
|
||||||
const vm = getCurrentInstance();
|
const vm = getCurrentInstance();
|
||||||
if (!vm) throw new Error('must be called in setup');
|
if (!vm) throw new Error('must be called in setup');
|
||||||
@@ -8,6 +12,11 @@ export const useTrack = () => {
|
|||||||
return vm.proxy.$track;
|
return vm.proxy.$track;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useAlert(message, action) {
|
/**
|
||||||
emitter.emit('newToastMessage', message, action);
|
* Emits a toast message event using a global emitter.
|
||||||
}
|
* @param {string} message - The message to be displayed in the toast.
|
||||||
|
* @param {Object|null} action - Optional callback function or object to execute.
|
||||||
|
*/
|
||||||
|
export const useAlert = (message, action = null) => {
|
||||||
|
emitter.emit('newToastMessage', { message, action });
|
||||||
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { getCurrentInstance } from 'vue';
|
||||||
|
import { emitter } from 'shared/helpers/mitt';
|
||||||
|
import { useTrack, useAlert } from '../index';
|
||||||
|
|
||||||
|
vi.mock('vue', () => ({
|
||||||
|
getCurrentInstance: vi.fn(),
|
||||||
|
}));
|
||||||
|
vi.mock('shared/helpers/mitt', () => ({
|
||||||
|
emitter: {
|
||||||
|
emit: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useTrack', () => {
|
||||||
|
it('should return $track from the current instance proxy', () => {
|
||||||
|
const mockProxy = { $track: vi.fn() };
|
||||||
|
getCurrentInstance.mockReturnValue({ proxy: mockProxy });
|
||||||
|
const track = useTrack();
|
||||||
|
expect(track).toBe(mockProxy.$track);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if called outside of setup', () => {
|
||||||
|
getCurrentInstance.mockReturnValue(null);
|
||||||
|
expect(useTrack).toThrowError('must be called in setup');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('useAlert', () => {
|
||||||
|
it('should emit a newToastMessage event with the provided message and action', () => {
|
||||||
|
const message = 'Toast message';
|
||||||
|
const action = {
|
||||||
|
type: 'link',
|
||||||
|
to: '/app/accounts/1/conversations/1',
|
||||||
|
message: 'Navigate',
|
||||||
|
};
|
||||||
|
useAlert(message, action);
|
||||||
|
expect(emitter.emit).toHaveBeenCalledWith('newToastMessage', {
|
||||||
|
message,
|
||||||
|
action,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit a newToastMessage event with the provided message and no action if action is null', () => {
|
||||||
|
const message = 'Toast message';
|
||||||
|
useAlert(message);
|
||||||
|
expect(emitter.emit).toHaveBeenCalledWith('newToastMessage', {
|
||||||
|
message,
|
||||||
|
action: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
import { useAdmin } from '../useAdmin';
|
||||||
|
import { useStoreGetters } from 'dashboard/composables/store';
|
||||||
|
|
||||||
|
vi.mock('dashboard/composables/store');
|
||||||
|
|
||||||
|
describe('useAdmin', () => {
|
||||||
|
it('returns true if the current user is an administrator', () => {
|
||||||
|
useStoreGetters.mockReturnValue({
|
||||||
|
getCurrentRole: ref('administrator'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
|
expect(isAdmin.value).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the current user is not an administrator', () => {
|
||||||
|
useStoreGetters.mockReturnValue({
|
||||||
|
getCurrentRole: ref('user'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
|
expect(isAdmin.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the current user role is null', () => {
|
||||||
|
useStoreGetters.mockReturnValue({
|
||||||
|
getCurrentRole: ref(null),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
|
expect(isAdmin.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the current user role is undefined', () => {
|
||||||
|
useStoreGetters.mockReturnValue({
|
||||||
|
getCurrentRole: ref(undefined),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
|
expect(isAdmin.value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false if the current user role is an empty string', () => {
|
||||||
|
useStoreGetters.mockReturnValue({
|
||||||
|
getCurrentRole: ref(''),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
|
expect(isAdmin.value).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { ref } from 'vue';
|
||||||
|
import {
|
||||||
|
useUISettings,
|
||||||
|
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
|
DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||||
|
} from 'dashboard/composables/useUISettings';
|
||||||
|
|
||||||
|
// Mocking the store composables
|
||||||
|
const mockDispatch = vi.fn();
|
||||||
|
|
||||||
|
const getUISettingsMock = ref({
|
||||||
|
is_ct_labels_open: true,
|
||||||
|
conversation_sidebar_items_order: DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
|
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||||
|
editor_message_key: 'enter',
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('dashboard/composables/store', () => ({
|
||||||
|
useStoreGetters: () => ({
|
||||||
|
getUISettings: getUISettingsMock,
|
||||||
|
}),
|
||||||
|
useStore: () => ({
|
||||||
|
dispatch: mockDispatch,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useUISettings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockDispatch.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns uiSettings', () => {
|
||||||
|
const { uiSettings } = useUISettings();
|
||||||
|
expect(uiSettings.value).toEqual({
|
||||||
|
is_ct_labels_open: true,
|
||||||
|
conversation_sidebar_items_order:
|
||||||
|
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
|
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||||
|
editor_message_key: 'enter',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates UI settings correctly', () => {
|
||||||
|
const { updateUISettings } = useUISettings();
|
||||||
|
updateUISettings({ enter_to_send_enabled: true });
|
||||||
|
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
|
||||||
|
uiSettings: {
|
||||||
|
enter_to_send_enabled: true,
|
||||||
|
is_ct_labels_open: true,
|
||||||
|
conversation_sidebar_items_order:
|
||||||
|
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
|
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||||
|
editor_message_key: 'enter',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggles sidebar UI state correctly', () => {
|
||||||
|
const { toggleSidebarUIState } = useUISettings();
|
||||||
|
toggleSidebarUIState('is_ct_labels_open');
|
||||||
|
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
|
||||||
|
uiSettings: {
|
||||||
|
is_ct_labels_open: false,
|
||||||
|
conversation_sidebar_items_order:
|
||||||
|
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
|
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||||
|
editor_message_key: 'enter',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct conversation sidebar items order', () => {
|
||||||
|
const { conversationSidebarItemsOrder } = useUISettings();
|
||||||
|
expect(conversationSidebarItemsOrder.value).toEqual(
|
||||||
|
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct contact sidebar items order', () => {
|
||||||
|
const { contactSidebarItemsOrder } = useUISettings();
|
||||||
|
expect(contactSidebarItemsOrder.value).toEqual(
|
||||||
|
DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct value for isContactSidebarItemOpen', () => {
|
||||||
|
const { isContactSidebarItemOpen } = useUISettings();
|
||||||
|
expect(isContactSidebarItemOpen('is_ct_labels_open')).toBe(true);
|
||||||
|
expect(isContactSidebarItemOpen('non_existent_key')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets signature flag for inbox correctly', () => {
|
||||||
|
const { setSignatureFlagForInbox } = useUISettings();
|
||||||
|
setSignatureFlagForInbox('email', true);
|
||||||
|
expect(mockDispatch).toHaveBeenCalledWith('updateUISettings', {
|
||||||
|
uiSettings: {
|
||||||
|
is_ct_labels_open: true,
|
||||||
|
conversation_sidebar_items_order:
|
||||||
|
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
||||||
|
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
||||||
|
email_signature_enabled: true,
|
||||||
|
editor_message_key: 'enter',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fetches signature flag from UI settings correctly', () => {
|
||||||
|
const { fetchSignatureFlagFromUISettings } = useUISettings();
|
||||||
|
expect(fetchSignatureFlagFromUISettings('email')).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is configured', () => {
|
||||||
|
getUISettingsMock.value.enter_to_send_enabled = false;
|
||||||
|
const { isEditorHotKeyEnabled } = useUISettings();
|
||||||
|
expect(isEditorHotKeyEnabled('enter')).toBe(true);
|
||||||
|
expect(isEditorHotKeyEnabled('cmd_enter')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns correct value for isEditorHotKeyEnabled when editor_message_key is not configured', () => {
|
||||||
|
getUISettingsMock.value.editor_message_key = undefined;
|
||||||
|
const { isEditorHotKeyEnabled } = useUISettings();
|
||||||
|
expect(isEditorHotKeyEnabled('enter')).toBe(false);
|
||||||
|
expect(isEditorHotKeyEnabled('cmd_enter')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles non-existent keys', () => {
|
||||||
|
const {
|
||||||
|
isContactSidebarItemOpen,
|
||||||
|
fetchSignatureFlagFromUISettings,
|
||||||
|
isEditorHotKeyEnabled,
|
||||||
|
} = useUISettings();
|
||||||
|
expect(isContactSidebarItemOpen('non_existent_key')).toBe(false);
|
||||||
|
expect(fetchSignatureFlagFromUISettings('non_existent_key')).toBe(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
expect(isEditorHotKeyEnabled('non_existent_key')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
import { useStoreGetters } from 'dashboard/composables/store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to determine if the current user is an administrator.
|
||||||
|
* @returns {Boolean} - True if the current user is an administrator, false otherwise.
|
||||||
|
*/
|
||||||
|
export function useAdmin() {
|
||||||
|
const getters = useStoreGetters();
|
||||||
|
|
||||||
|
const currentUserRole = computed(() => getters.getCurrentRole.value);
|
||||||
|
const isAdmin = computed(() => currentUserRole.value === 'administrator');
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAdmin,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
import { computed } from 'vue';
|
||||||
|
import { useStore, useStoreGetters } from 'dashboard/composables/store';
|
||||||
|
|
||||||
|
export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = Object.freeze([
|
||||||
|
{ name: 'conversation_actions' },
|
||||||
|
{ name: 'macros' },
|
||||||
|
{ name: 'conversation_info' },
|
||||||
|
{ name: 'contact_attributes' },
|
||||||
|
{ name: 'previous_conversation' },
|
||||||
|
{ name: 'conversation_participants' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER = Object.freeze([
|
||||||
|
{ name: 'contact_attributes' },
|
||||||
|
{ name: 'contact_labels' },
|
||||||
|
{ name: 'previous_conversation' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slugifies the channel name.
|
||||||
|
* Replaces spaces, hyphens, and double colons with underscores.
|
||||||
|
* @param {string} name - The channel name to slugify.
|
||||||
|
* @returns {string} The slugified channel name.
|
||||||
|
*/
|
||||||
|
const slugifyChannel = name =>
|
||||||
|
name?.toLowerCase().replace(' ', '_').replace('-', '_').replace('::', '_');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the order of items in the conversation sidebar, using defaults if not present.
|
||||||
|
* @param {Object} uiSettings - Reactive UI settings object.
|
||||||
|
* @returns {Array} Ordered list of sidebar items.
|
||||||
|
*/
|
||||||
|
const useConversationSidebarItemsOrder = uiSettings => {
|
||||||
|
return computed(() => {
|
||||||
|
const { conversation_sidebar_items_order: itemsOrder } = uiSettings.value;
|
||||||
|
// If the sidebar order is not set, use the default order.
|
||||||
|
if (!itemsOrder) {
|
||||||
|
return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
|
||||||
|
}
|
||||||
|
// Create a copy of itemsOrder to avoid mutating the original store object.
|
||||||
|
const itemsOrderCopy = [...itemsOrder];
|
||||||
|
// If the sidebar order doesn't have the new elements, then add them to the list.
|
||||||
|
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER.forEach(item => {
|
||||||
|
if (!itemsOrderCopy.find(i => i.name === item.name)) {
|
||||||
|
itemsOrderCopy.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return itemsOrderCopy;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the order of items in the contact sidebar,using defaults if not present.
|
||||||
|
* @param {Object} uiSettings - Reactive UI settings object.
|
||||||
|
* @returns {Array} Ordered list of sidebar items.
|
||||||
|
*/
|
||||||
|
const useContactSidebarItemsOrder = uiSettings => {
|
||||||
|
return computed(() => {
|
||||||
|
const { contact_sidebar_items_order: itemsOrder } = uiSettings.value;
|
||||||
|
return itemsOrder || DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the open state of a sidebar item.
|
||||||
|
* @param {string} key - The key of the sidebar item to toggle.
|
||||||
|
* @param {Object} uiSettings - Reactive UI settings object.
|
||||||
|
* @param {Function} updateUISettings - Function to update UI settings.
|
||||||
|
*/
|
||||||
|
const toggleSidebarUIState = (key, uiSettings, updateUISettings) => {
|
||||||
|
updateUISettings({ [key]: !uiSettings.value[key] });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the signature flag for a specific channel type in the inbox settings.
|
||||||
|
* @param {string} channelType - The type of the channel.
|
||||||
|
* @param {boolean} value - The value to set for the signature enabled flag.
|
||||||
|
* @param {Function} updateUISettings - Function to update UI settings.
|
||||||
|
*/
|
||||||
|
const setSignatureFlagForInbox = (channelType, value, updateUISettings) => {
|
||||||
|
if (!channelType) return;
|
||||||
|
|
||||||
|
const slugifiedChannel = slugifyChannel(channelType);
|
||||||
|
updateUISettings({ [`${slugifiedChannel}_signature_enabled`]: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the signature flag for a specific channel type from UI settings.
|
||||||
|
* @param {string} channelType - The type of the channel.
|
||||||
|
* @param {Object} uiSettings - Reactive UI settings object.
|
||||||
|
* @returns {boolean} The value of the signature enabled flag.
|
||||||
|
*/
|
||||||
|
const fetchSignatureFlagFromUISettings = (channelType, uiSettings) => {
|
||||||
|
if (!channelType) return false;
|
||||||
|
|
||||||
|
const slugifiedChannel = slugifyChannel(channelType);
|
||||||
|
return uiSettings.value[`${slugifiedChannel}_signature_enabled`];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a specific editor hotkey is enabled.
|
||||||
|
* @param {string} key - The key to check.
|
||||||
|
* @param {Object} uiSettings - Reactive UI settings object.
|
||||||
|
* @returns {boolean} True if the hotkey is enabled, otherwise false.
|
||||||
|
*/
|
||||||
|
const isEditorHotKeyEnabled = (key, uiSettings) => {
|
||||||
|
const {
|
||||||
|
editor_message_key: editorMessageKey,
|
||||||
|
enter_to_send_enabled: enterToSendEnabled,
|
||||||
|
} = uiSettings.value || {};
|
||||||
|
if (!editorMessageKey) {
|
||||||
|
return key === (enterToSendEnabled ? 'enter' : 'cmd_enter');
|
||||||
|
}
|
||||||
|
return editorMessageKey === key;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main composable function for managing UI settings.
|
||||||
|
* @returns {Object} An object containing reactive properties and methods for UI settings management.
|
||||||
|
*/
|
||||||
|
export function useUISettings() {
|
||||||
|
const getters = useStoreGetters();
|
||||||
|
const store = useStore();
|
||||||
|
const uiSettings = computed(() => getters.getUISettings.value);
|
||||||
|
|
||||||
|
const updateUISettings = (settings = {}) => {
|
||||||
|
store.dispatch('updateUISettings', {
|
||||||
|
uiSettings: {
|
||||||
|
...uiSettings.value,
|
||||||
|
...settings,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
uiSettings,
|
||||||
|
updateUISettings,
|
||||||
|
conversationSidebarItemsOrder: useConversationSidebarItemsOrder(uiSettings),
|
||||||
|
contactSidebarItemsOrder: useContactSidebarItemsOrder(uiSettings),
|
||||||
|
isContactSidebarItemOpen: key => !!uiSettings.value[key],
|
||||||
|
toggleSidebarUIState: key =>
|
||||||
|
toggleSidebarUIState(key, uiSettings, updateUISettings),
|
||||||
|
setSignatureFlagForInbox: (channelType, value) =>
|
||||||
|
setSignatureFlagForInbox(channelType, value, updateUISettings),
|
||||||
|
fetchSignatureFlagFromUISettings: channelType =>
|
||||||
|
fetchSignatureFlagFromUISettings(channelType, uiSettings),
|
||||||
|
isEditorHotKeyEnabled: key => isEditorHotKeyEnabled(key, uiSettings),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
const SCRIPT_TYPE = 'text/javascript';
|
||||||
|
const DATA_LOADED_ATTR = 'data-loaded';
|
||||||
|
const SCRIPT_PROPERTIES = [
|
||||||
|
'defer',
|
||||||
|
'crossOrigin',
|
||||||
|
'noModule',
|
||||||
|
'referrerPolicy',
|
||||||
|
'id',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom error class for script loading failures.
|
||||||
|
* @extends Error
|
||||||
|
*/
|
||||||
|
class ScriptLoaderError extends Error {
|
||||||
|
/**
|
||||||
|
* Creates a new ScriptLoaderError.
|
||||||
|
* @param {string} src - The source URL of the script that failed to load.
|
||||||
|
* @param {string} message - The error message.
|
||||||
|
*/
|
||||||
|
constructor(src, message = 'Failed to load script') {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ScriptLoaderError';
|
||||||
|
this.src = src;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets detailed error information.
|
||||||
|
* @returns {string} A string containing the error details.
|
||||||
|
*/
|
||||||
|
getErrorDetails() {
|
||||||
|
return `Failed to load script from source: ${this.src}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new script element with the specified attributes.
|
||||||
|
* @param {string} src - The source URL of the script.
|
||||||
|
* @param {Object} options - Options for configuring the script element.
|
||||||
|
* @param {string} [options.type='text/javascript'] - The type of the script.
|
||||||
|
* @param {boolean} [options.async=true] - Whether the script should load asynchronously.
|
||||||
|
* @param {boolean} [options.defer] - Whether the script execution should be deferred.
|
||||||
|
* @param {string} [options.crossOrigin] - The CORS setting for the script.
|
||||||
|
* @param {boolean} [options.noModule] - Whether the script should not be treated as a JavaScript module.
|
||||||
|
* @param {string} [options.referrerPolicy] - The referrer policy for the script.
|
||||||
|
* @param {string} [options.id] - The id attribute for the script element.
|
||||||
|
* @param {Object} [options.attrs] - Additional attributes to set on the script element.
|
||||||
|
* @returns {HTMLScriptElement} The created script element.
|
||||||
|
*/
|
||||||
|
const createScriptElement = (src, options) => {
|
||||||
|
const el = document.createElement('script');
|
||||||
|
el.type = options.type || SCRIPT_TYPE;
|
||||||
|
el.async = options.async !== false;
|
||||||
|
el.src = src;
|
||||||
|
|
||||||
|
SCRIPT_PROPERTIES.forEach(property => {
|
||||||
|
if (property in options) {
|
||||||
|
el[property] = options[property];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.entries(options.attrs || {}).forEach(([name, value]) =>
|
||||||
|
el.setAttribute(name, value)
|
||||||
|
);
|
||||||
|
|
||||||
|
return el;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an existing script element with the specified source URL.
|
||||||
|
* @param {string} src - The source URL to search for.
|
||||||
|
* @returns {HTMLScriptElement|null} The found script element, or null if not found.
|
||||||
|
*/
|
||||||
|
const findExistingScript = src => {
|
||||||
|
return document.querySelector(`script[src="${src}"]`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a script asynchronously and returns a promise.
|
||||||
|
* @param {string} src - The source URL of the script to load.
|
||||||
|
* @param {Object} options - Options for configuring the script element.
|
||||||
|
* @param {string} [options.type='text/javascript'] - The type of the script.
|
||||||
|
* @param {boolean} [options.async=true] - Whether the script should load asynchronously.
|
||||||
|
* @param {boolean} [options.defer] - Whether the script execution should be deferred.
|
||||||
|
* @param {string} [options.crossOrigin] - The CORS setting for the script.
|
||||||
|
* @param {boolean} [options.noModule] - Whether the script should not be treated as a JavaScript module.
|
||||||
|
* @param {string} [options.referrerPolicy] - The referrer policy for the script.
|
||||||
|
* @param {string} [options.id] - The id attribute for the script element.
|
||||||
|
* @param {Object} [options.attrs] - Additional attributes to set on the script element.
|
||||||
|
* @returns {Promise<HTMLScriptElement|boolean>} A promise that resolves with the loaded script element,
|
||||||
|
* or false if the script couldn't be loaded.
|
||||||
|
* @throws {ScriptLoaderError} If the script fails to load.
|
||||||
|
*/
|
||||||
|
export async function loadScript(src, options) {
|
||||||
|
if (typeof window === 'undefined' || !window.document) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof src !== 'string' || src.trim() === '') {
|
||||||
|
reject(new Error('Invalid source URL provided'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let el = findExistingScript(src);
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
el = createScriptElement(src, options);
|
||||||
|
document.head.appendChild(el);
|
||||||
|
} else if (el.hasAttribute(DATA_LOADED_ATTR)) {
|
||||||
|
resolve(el);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleError = () => reject(new ScriptLoaderError(src));
|
||||||
|
|
||||||
|
el.addEventListener('error', handleError);
|
||||||
|
el.addEventListener('abort', handleError);
|
||||||
|
el.addEventListener('load', () => {
|
||||||
|
el.setAttribute(DATA_LOADED_ATTR, 'true');
|
||||||
|
resolve(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import cloneObject from 'dashboard/helpers/clone';
|
||||||
|
|
||||||
const allElementsString = arr => {
|
const allElementsString = arr => {
|
||||||
return arr.every(elem => typeof elem === 'string');
|
return arr.every(elem => typeof elem === 'string');
|
||||||
};
|
};
|
||||||
@@ -27,7 +29,7 @@ const generatePayloadForObject = item => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const generatePayload = data => {
|
const generatePayload = data => {
|
||||||
const actions = JSON.parse(JSON.stringify(data));
|
const actions = cloneObject(data);
|
||||||
let payload = actions.map(item => {
|
let payload = actions.map(item => {
|
||||||
if (Array.isArray(item.action_params)) {
|
if (Array.isArray(item.action_params)) {
|
||||||
item.action_params = formatArray(item.action_params);
|
item.action_params = formatArray(item.action_params);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
} from 'dashboard/routes/dashboard/settings/automation/operators';
|
} from 'dashboard/routes/dashboard/settings/automation/operators';
|
||||||
import filterQueryGenerator from './filterQueryGenerator';
|
import filterQueryGenerator from './filterQueryGenerator';
|
||||||
import actionQueryGenerator from './actionQueryGenerator';
|
import actionQueryGenerator from './actionQueryGenerator';
|
||||||
|
import cloneObject from 'dashboard/helpers/clone';
|
||||||
|
|
||||||
const MESSAGE_CONDITION_VALUES = [
|
const MESSAGE_CONDITION_VALUES = [
|
||||||
{
|
{
|
||||||
id: 'incoming',
|
id: 'incoming',
|
||||||
@@ -121,7 +123,7 @@ export const generateConditionOptions = (options, key = 'id') => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add the "None" option to the agent list
|
// Add the "None" option to the agent list
|
||||||
export const agentList = agents => [
|
export const addNoneToList = agents => [
|
||||||
{
|
{
|
||||||
id: 'nil',
|
id: 'nil',
|
||||||
name: 'None',
|
name: 'None',
|
||||||
@@ -137,8 +139,8 @@ export const getActionOptions = ({
|
|||||||
type,
|
type,
|
||||||
}) => {
|
}) => {
|
||||||
const actionsMap = {
|
const actionsMap = {
|
||||||
assign_agent: agentList(agents),
|
assign_agent: addNoneToList(agents),
|
||||||
assign_team: teams,
|
assign_team: addNoneToList(teams),
|
||||||
send_email_to_team: teams,
|
send_email_to_team: teams,
|
||||||
add_label: generateConditionOptions(labels, 'title'),
|
add_label: generateConditionOptions(labels, 'title'),
|
||||||
remove_label: generateConditionOptions(labels, 'title'),
|
remove_label: generateConditionOptions(labels, 'title'),
|
||||||
@@ -255,7 +257,7 @@ export const getStandardAttributeInputType = (automationTypes, event, key) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const generateAutomationPayload = payload => {
|
export const generateAutomationPayload = payload => {
|
||||||
const automation = JSON.parse(JSON.stringify(payload));
|
const automation = cloneObject(payload);
|
||||||
automation.conditions[automation.conditions.length - 1].query_operator = null;
|
automation.conditions[automation.conditions.length - 1].query_operator = null;
|
||||||
automation.conditions = filterQueryGenerator(automation.conditions).payload;
|
automation.conditions = filterQueryGenerator(automation.conditions).payload;
|
||||||
automation.actions = actionQueryGenerator(automation.actions);
|
automation.actions = actionQueryGenerator(automation.actions);
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Creates a deep clone of the provided object.
|
||||||
|
*
|
||||||
|
* This function attempts to use the `structuredClone` method if available.
|
||||||
|
* If `structuredClone` is not supported, it falls back to using
|
||||||
|
* `JSON.parse(JSON.stringify())`.
|
||||||
|
*
|
||||||
|
* @param {*} obj - The object to be cloned. Can be of any type.
|
||||||
|
* @returns {*} A deep clone of the input object.
|
||||||
|
*
|
||||||
|
* @throws {TypeError} If the object contains values that JSON cannot serialize
|
||||||
|
* (e.g., functions, undefined) when falling back to
|
||||||
|
* JSON methods.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const original = { a: 1, b: { c: 2 } };
|
||||||
|
* const clone = cloneObject(original);
|
||||||
|
* console.log(clone); // { a: 1, b: { c: 2 } }
|
||||||
|
* console.log(original === clone); // false
|
||||||
|
*/
|
||||||
|
export function cloneObject(obj) {
|
||||||
|
if (typeof structuredClone === 'function') {
|
||||||
|
return structuredClone(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The JSON method doesn't handle all JavaScript types correctly and may cause unexpected behavior.
|
||||||
|
// At the moment structuredClone has good adoption across browsers https://caniuse.com/mdn-api_structuredclone
|
||||||
|
// and is the preferred method for cloning objects.
|
||||||
|
//
|
||||||
|
// We can consider implementing a more robust fallback method in the future if we find users are running into issues.
|
||||||
|
// Ref: https://github.com/lukeed/klona
|
||||||
|
return JSON.parse(JSON.stringify(obj));
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import cloneObject from 'dashboard/helpers/clone';
|
||||||
|
|
||||||
const setArrayValues = item => {
|
const setArrayValues = item => {
|
||||||
return item.values[0]?.id ? item.values.map(val => val.id) : item.values;
|
return item.values[0]?.id ? item.values.map(val => val.id) : item.values;
|
||||||
};
|
};
|
||||||
@@ -21,7 +23,7 @@ const generateValues = item => {
|
|||||||
|
|
||||||
const generatePayload = data => {
|
const generatePayload = data => {
|
||||||
// Make a copy of data to avoid vue data reactivity issues
|
// Make a copy of data to avoid vue data reactivity issues
|
||||||
const filters = JSON.parse(JSON.stringify(data));
|
const filters = cloneObject(data);
|
||||||
let payload = filters.map(item => {
|
let payload = filters.map(item => {
|
||||||
// If item key is content, we will split it using comma and return as array
|
// If item key is content, we will split it using comma and return as array
|
||||||
// FIX ME: Make this generic option instead of using the key directly here
|
// FIX ME: Make this generic option instead of using the key directly here
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}, {});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
import NotificationSubscriptions from '../api/notificationSubscription';
|
import NotificationSubscriptions from '../api/notificationSubscription';
|
||||||
import auth from '../api/auth';
|
import auth from '../api/auth';
|
||||||
import { emitter } from 'shared/helpers/mitt';
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
export const verifyServiceWorkerExistence = (callback = () => {}) => {
|
export const verifyServiceWorkerExistence = (callback = () => {}) => {
|
||||||
if (!('serviceWorker' in navigator)) {
|
if (!('serviceWorker' in navigator)) {
|
||||||
@@ -69,19 +69,13 @@ export const registerSubscription = (onSuccess = () => {}) => {
|
|||||||
onSuccess();
|
onSuccess();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
emitter.emit(
|
useAlert('This browser does not support desktop notification');
|
||||||
'newToastMessage',
|
|
||||||
'This browser does not support desktop notification'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestPushPermissions = ({ onSuccess }) => {
|
export const requestPushPermissions = ({ onSuccess }) => {
|
||||||
if (!('Notification' in window)) {
|
if (!('Notification' in window)) {
|
||||||
emitter.emit(
|
useAlert('This browser does not support desktop notification');
|
||||||
'newToastMessage',
|
|
||||||
'This browser does not support desktop notification'
|
|
||||||
);
|
|
||||||
} else if (Notification.permission === 'granted') {
|
} else if (Notification.permission === 'granted') {
|
||||||
registerSubscription(onSuccess);
|
registerSubscription(onSuccess);
|
||||||
} else if (Notification.permission !== 'denied') {
|
} else if (Notification.permission !== 'denied') {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import { loadScript } from '../DOMHelpers';
|
||||||
|
import { JSDOM } from 'jsdom';
|
||||||
|
|
||||||
|
describe('loadScript', () => {
|
||||||
|
let dom;
|
||||||
|
let window;
|
||||||
|
let document;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dom = new JSDOM('<!DOCTYPE html><html><head></head><body></body></html>', {
|
||||||
|
url: 'http://localhost',
|
||||||
|
});
|
||||||
|
window = dom.window;
|
||||||
|
document = window.document;
|
||||||
|
global.document = document;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
delete global.document;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load a script successfully', async () => {
|
||||||
|
const src = 'https://example.com/script.js';
|
||||||
|
const loadPromise = loadScript(src, {});
|
||||||
|
|
||||||
|
// Simulate successful script load
|
||||||
|
setTimeout(() => {
|
||||||
|
const script = document.querySelector(`script[src="${src}"]`);
|
||||||
|
if (script) {
|
||||||
|
script.dispatchEvent(new window.Event('load'));
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const script = await loadPromise;
|
||||||
|
|
||||||
|
expect(script).toBeTruthy();
|
||||||
|
expect(script.getAttribute('src')).toBe(src);
|
||||||
|
expect(script.getAttribute('data-loaded')).toBe('true');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not load a script if document is not available', async () => {
|
||||||
|
delete global.document;
|
||||||
|
const result = await loadScript('https://example.com/script.js', {});
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use an existing script if already present', async () => {
|
||||||
|
const src = 'https://example.com/existing-script.js';
|
||||||
|
const existingScript = document.createElement('script');
|
||||||
|
existingScript.src = src;
|
||||||
|
existingScript.setAttribute('data-loaded', 'true');
|
||||||
|
document.head.appendChild(existingScript);
|
||||||
|
|
||||||
|
const script = await loadScript(src, {});
|
||||||
|
|
||||||
|
expect(script).toBe(existingScript);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set custom attributes on the script element', async () => {
|
||||||
|
const src = 'https://example.com/custom-script.js';
|
||||||
|
const options = {
|
||||||
|
type: 'module',
|
||||||
|
async: false,
|
||||||
|
defer: true,
|
||||||
|
crossOrigin: 'anonymous',
|
||||||
|
noModule: true,
|
||||||
|
referrerPolicy: 'origin',
|
||||||
|
id: 'custom-script',
|
||||||
|
attrs: { 'data-custom': 'value' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPromise = loadScript(src, options);
|
||||||
|
|
||||||
|
// Simulate successful script load
|
||||||
|
setTimeout(() => {
|
||||||
|
const script = document.querySelector(`script[src="${src}"]`);
|
||||||
|
if (script) {
|
||||||
|
script.dispatchEvent(new window.Event('load'));
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const script = await loadPromise;
|
||||||
|
|
||||||
|
expect(script.type).toBe('module');
|
||||||
|
expect(script.async).toBe(false);
|
||||||
|
expect(script.defer).toBe(true);
|
||||||
|
expect(script.crossOrigin).toBe('anonymous');
|
||||||
|
expect(script.noModule).toBe(true);
|
||||||
|
expect(script.referrerPolicy).toBe('origin');
|
||||||
|
expect(script.id).toBe('custom-script');
|
||||||
|
expect(script.getAttribute('data-custom')).toBe('value');
|
||||||
|
});
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user