Compare commits
26
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 |
+63
-22
@@ -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)
|
||||||
@@ -148,7 +148,7 @@ GEM
|
|||||||
barnes (0.0.9)
|
barnes (0.0.9)
|
||||||
multi_json (~> 1)
|
multi_json (~> 1)
|
||||||
statsd-ruby (~> 1.1)
|
statsd-ruby (~> 1.1)
|
||||||
base64 (0.1.1)
|
base64 (0.2.0)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
bigdecimal (3.1.8)
|
bigdecimal (3.1.8)
|
||||||
bindex (0.8.1)
|
bindex (0.8.1)
|
||||||
@@ -171,7 +171,8 @@ 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 (3.3.0)
|
csv (3.3.0)
|
||||||
@@ -242,9 +243,8 @@ GEM
|
|||||||
railties (>= 5.0.0)
|
railties (>= 5.0.0)
|
||||||
faker (3.2.0)
|
faker (3.2.0)
|
||||||
i18n (>= 1.8.11, < 2)
|
i18n (>= 1.8.11, < 2)
|
||||||
faraday (2.7.4)
|
faraday (2.9.0)
|
||||||
faraday-net_http (>= 2.0, < 3.1)
|
faraday-net_http (>= 2.0, < 3.2)
|
||||||
ruby2_keywords (>= 0.0.4)
|
|
||||||
faraday-follow_redirects (0.3.0)
|
faraday-follow_redirects (0.3.0)
|
||||||
faraday (>= 1, < 3)
|
faraday (>= 1, < 3)
|
||||||
faraday-mashify (0.1.1)
|
faraday-mashify (0.1.1)
|
||||||
@@ -252,7 +252,8 @@ GEM
|
|||||||
hashie
|
hashie
|
||||||
faraday-multipart (1.0.4)
|
faraday-multipart (1.0.4)
|
||||||
multipart-post (~> 2)
|
multipart-post (~> 2)
|
||||||
faraday-net_http (3.0.2)
|
faraday-net_http (3.1.0)
|
||||||
|
net-http
|
||||||
faraday-net_http_persistent (2.1.0)
|
faraday-net_http_persistent (2.1.0)
|
||||||
faraday (~> 2.5)
|
faraday (~> 2.5)
|
||||||
net-http-persistent (~> 4.0)
|
net-http-persistent (~> 4.0)
|
||||||
@@ -321,7 +322,10 @@ GEM
|
|||||||
google-cloud-translate-v3 (0.6.0)
|
google-cloud-translate-v3 (0.6.0)
|
||||||
gapic-common (>= 0.17.1, < 2.a)
|
gapic-common (>= 0.17.1, < 2.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
|
google-protobuf (3.25.3)
|
||||||
google-protobuf (3.25.3-arm64-darwin)
|
google-protobuf (3.25.3-arm64-darwin)
|
||||||
|
google-protobuf (3.25.3-x86_64-darwin)
|
||||||
|
google-protobuf (3.25.3-x86_64-linux)
|
||||||
googleapis-common-protos (1.4.0)
|
googleapis-common-protos (1.4.0)
|
||||||
google-protobuf (~> 3.14)
|
google-protobuf (~> 3.14)
|
||||||
googleapis-common-protos-types (~> 1.2)
|
googleapis-common-protos-types (~> 1.2)
|
||||||
@@ -337,7 +341,16 @@ GEM
|
|||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
groupdate (6.2.1)
|
groupdate (6.2.1)
|
||||||
activesupport (>= 5.2)
|
activesupport (>= 5.2)
|
||||||
grpc (1.63.0-arm64-darwin)
|
grpc (1.62.0)
|
||||||
|
google-protobuf (~> 3.25)
|
||||||
|
googleapis-common-protos-types (~> 1.0)
|
||||||
|
grpc (1.62.0-arm64-darwin)
|
||||||
|
google-protobuf (~> 3.25)
|
||||||
|
googleapis-common-protos-types (~> 1.0)
|
||||||
|
grpc (1.62.0-x86_64-darwin)
|
||||||
|
google-protobuf (~> 3.25)
|
||||||
|
googleapis-common-protos-types (~> 1.0)
|
||||||
|
grpc (1.62.0-x86_64-linux)
|
||||||
google-protobuf (~> 3.25)
|
google-protobuf (~> 3.25)
|
||||||
googleapis-common-protos-types (~> 1.0)
|
googleapis-common-protos-types (~> 1.0)
|
||||||
haikunator (1.1.1)
|
haikunator (1.1.1)
|
||||||
@@ -346,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)
|
||||||
@@ -361,7 +374,7 @@ GEM
|
|||||||
mini_mime (>= 1.0.0)
|
mini_mime (>= 1.0.0)
|
||||||
multi_xml (>= 0.5.2)
|
multi_xml (>= 0.5.2)
|
||||||
httpclient (2.8.3)
|
httpclient (2.8.3)
|
||||||
i18n (1.14.1)
|
i18n (1.14.5)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
image_processing (1.12.2)
|
image_processing (1.12.2)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
@@ -385,7 +398,8 @@ GEM
|
|||||||
hana (~> 1.3)
|
hana (~> 1.3)
|
||||||
regexp_parser (~> 2.0)
|
regexp_parser (~> 2.0)
|
||||||
uri_template (~> 0.7)
|
uri_template (~> 0.7)
|
||||||
jwt (2.7.0)
|
jwt (2.8.1)
|
||||||
|
base64
|
||||||
kaminari (1.2.2)
|
kaminari (1.2.2)
|
||||||
activesupport (>= 4.1.0)
|
activesupport (>= 4.1.0)
|
||||||
kaminari-actionview (= 1.2.2)
|
kaminari-actionview (= 1.2.2)
|
||||||
@@ -409,8 +423,15 @@ GEM
|
|||||||
letter_opener (1.8.1)
|
letter_opener (1.8.1)
|
||||||
launchy (>= 2.2, < 3)
|
launchy (>= 2.2, < 3)
|
||||||
libdatadog (7.0.0.1.0)
|
libdatadog (7.0.0.1.0)
|
||||||
|
libdatadog (7.0.0.1.0-x86_64-linux)
|
||||||
|
libddwaf (1.14.0.0.0)
|
||||||
|
ffi (~> 1.0)
|
||||||
libddwaf (1.14.0.0.0-arm64-darwin)
|
libddwaf (1.14.0.0.0-arm64-darwin)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
|
libddwaf (1.14.0.0.0-x86_64-darwin)
|
||||||
|
ffi (~> 1.0)
|
||||||
|
libddwaf (1.14.0.0.0-x86_64-linux)
|
||||||
|
ffi (~> 1.0)
|
||||||
line-bot-api (1.28.0)
|
line-bot-api (1.28.0)
|
||||||
liquid (5.4.0)
|
liquid (5.4.0)
|
||||||
listen (3.8.0)
|
listen (3.8.0)
|
||||||
@@ -439,12 +460,13 @@ GEM
|
|||||||
meta_request (0.8.2)
|
meta_request (0.8.2)
|
||||||
rack-contrib (>= 1.1, < 3)
|
rack-contrib (>= 1.1, < 3)
|
||||||
railties (>= 3.0.0, < 8)
|
railties (>= 3.0.0, < 8)
|
||||||
method_source (1.0.0)
|
method_source (1.1.0)
|
||||||
mime-types (3.4.1)
|
mime-types (3.4.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2023.0218.1)
|
mime-types-data (3.2023.0218.1)
|
||||||
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)
|
||||||
minitest (5.24.1)
|
minitest (5.24.1)
|
||||||
mock_redis (0.36.0)
|
mock_redis (0.36.0)
|
||||||
ruby2_keywords
|
ruby2_keywords
|
||||||
@@ -454,6 +476,8 @@ GEM
|
|||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
neighbor (0.2.3)
|
neighbor (0.2.3)
|
||||||
activerecord (>= 5.2)
|
activerecord (>= 5.2)
|
||||||
|
net-http (0.4.1)
|
||||||
|
uri
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.4.12)
|
net-imap (0.4.12)
|
||||||
@@ -472,8 +496,15 @@ GEM
|
|||||||
newrelic_rpm (9.6.0)
|
newrelic_rpm (9.6.0)
|
||||||
base64
|
base64
|
||||||
nio4r (2.7.3)
|
nio4r (2.7.3)
|
||||||
|
nokogiri (1.16.6)
|
||||||
|
mini_portile2 (~> 2.8.2)
|
||||||
|
racc (~> 1.4)
|
||||||
nokogiri (1.16.6-arm64-darwin)
|
nokogiri (1.16.6-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.16.6-x86_64-darwin)
|
||||||
|
racc (~> 1.4)
|
||||||
|
nokogiri (1.16.6-x86_64-linux)
|
||||||
|
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)
|
||||||
snaky_hash (~> 2.0)
|
snaky_hash (~> 2.0)
|
||||||
@@ -520,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)
|
||||||
@@ -536,7 +567,8 @@ GEM
|
|||||||
rack (>= 2.0.0)
|
rack (>= 2.0.0)
|
||||||
rack-mini-profiler (3.2.0)
|
rack-mini-profiler (3.2.0)
|
||||||
rack (>= 1.2.0)
|
rack (>= 1.2.0)
|
||||||
rack-protection (3.1.0)
|
rack-protection (3.2.0)
|
||||||
|
base64 (>= 0.1.0)
|
||||||
rack (~> 2.2, >= 2.2.4)
|
rack (~> 2.2, >= 2.2.4)
|
||||||
rack-proxy (0.7.6)
|
rack-proxy (0.7.6)
|
||||||
rack
|
rack
|
||||||
@@ -572,7 +604,7 @@ GEM
|
|||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
zeitwerk (~> 2.5)
|
zeitwerk (~> 2.5)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.1.0)
|
rake (13.2.1)
|
||||||
rb-fsevent (0.11.2)
|
rb-fsevent (0.11.2)
|
||||||
rb-inotify (0.10.1)
|
rb-inotify (0.10.1)
|
||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
@@ -602,7 +634,7 @@ GEM
|
|||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
reverse_markdown (2.1.1)
|
reverse_markdown (2.1.1)
|
||||||
nokogiri
|
nokogiri
|
||||||
rexml (3.3.1)
|
rexml (3.3.2)
|
||||||
strscan
|
strscan
|
||||||
rspec-core (3.13.0)
|
rspec-core (3.13.0)
|
||||||
rspec-support (~> 3.13.0)
|
rspec-support (~> 3.13.0)
|
||||||
@@ -737,7 +769,7 @@ GEM
|
|||||||
strscan (3.1.0)
|
strscan (3.1.0)
|
||||||
telephone_number (1.4.20)
|
telephone_number (1.4.20)
|
||||||
test-prof (1.2.1)
|
test-prof (1.2.1)
|
||||||
thor (1.3.0)
|
thor (1.3.1)
|
||||||
tilt (2.3.0)
|
tilt (2.3.0)
|
||||||
time_diff (0.3.0)
|
time_diff (0.3.0)
|
||||||
activesupport
|
activesupport
|
||||||
@@ -762,11 +794,12 @@ GEM
|
|||||||
unf_ext (0.0.8.2)
|
unf_ext (0.0.8.2)
|
||||||
unicode-display_width (2.4.2)
|
unicode-display_width (2.4.2)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
|
uri (0.13.0)
|
||||||
uri_template (0.7.0)
|
uri_template (0.7.0)
|
||||||
valid_email2 (4.0.6)
|
valid_email2 (4.0.6)
|
||||||
activemodel (>= 3.2)
|
activemodel (>= 3.2)
|
||||||
mail (~> 2.5)
|
mail (~> 2.5)
|
||||||
version_gem (1.1.3)
|
version_gem (1.1.4)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.2.1)
|
web-console (4.2.1)
|
||||||
@@ -777,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)
|
||||||
@@ -797,7 +830,15 @@ GEM
|
|||||||
zeitwerk (2.6.16)
|
zeitwerk (2.6.16)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-23
|
arm64-darwin-20
|
||||||
|
arm64-darwin-21
|
||||||
|
arm64-darwin-22
|
||||||
|
ruby
|
||||||
|
x86_64-darwin-18
|
||||||
|
x86_64-darwin-20
|
||||||
|
x86_64-darwin-21
|
||||||
|
x86_64-darwin-22
|
||||||
|
x86_64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
active_record_query_trace
|
active_record_query_trace
|
||||||
@@ -930,4 +971,4 @@ RUBY VERSION
|
|||||||
ruby 3.3.3p89
|
ruby 3.3.3p89
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.14
|
2.5.16
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
class V2::BaseConversationReportBuilder
|
|
||||||
pattr_initialize :account, :params
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
AVG_METRICS = %w[avg_first_response_time avg_resolution_time reply_time].freeze
|
|
||||||
COUNT_METRICS = %w[
|
|
||||||
conversations_count
|
|
||||||
incoming_messages_count
|
|
||||||
outgoing_messages_count
|
|
||||||
resolutions_count
|
|
||||||
bot_resolutions_count
|
|
||||||
bot_handoffs_count
|
|
||||||
].freeze
|
|
||||||
|
|
||||||
def builder_class(metric)
|
|
||||||
case metric
|
|
||||||
when *AVG_METRICS
|
|
||||||
V2::Reports::Timeseries::AverageReportBuilder
|
|
||||||
when *COUNT_METRICS
|
|
||||||
V2::Reports::Timeseries::CountReportBuilder
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def log_invalid_metric
|
|
||||||
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
|
|
||||||
|
|
||||||
{}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
class V2::ConversationMetricBuilder < V2::BaseConversationReportBuilder
|
|
||||||
def summary
|
|
||||||
{
|
|
||||||
conversations_count: count('conversations_count'),
|
|
||||||
incoming_messages_count: count('incoming_messages_count'),
|
|
||||||
outgoing_messages_count: count('outgoing_messages_count'),
|
|
||||||
avg_first_response_time: count('avg_first_response_time'),
|
|
||||||
avg_resolution_time: count('avg_resolution_time'),
|
|
||||||
resolutions_count: count('resolutions_count'),
|
|
||||||
reply_time: count('reply_time')
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def bot_summary
|
|
||||||
{
|
|
||||||
bot_resolutions_count: count('bot_resolutions_count'),
|
|
||||||
bot_handoffs_count: count('bot_handoffs_count')
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def count(metric)
|
|
||||||
builder_class(metric).new(account, builder_params(metric)).aggregate_value
|
|
||||||
end
|
|
||||||
|
|
||||||
def builder_params(metric)
|
|
||||||
params.merge({ metric: metric })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
class V2::ConversationReportBuilder < V2::BaseConversationReportBuilder
|
|
||||||
include DateRangeHelper
|
|
||||||
|
|
||||||
pattr_initialize :account, :params
|
|
||||||
|
|
||||||
def timeseries
|
|
||||||
perform_action(:timeseries)
|
|
||||||
end
|
|
||||||
|
|
||||||
def aggregate_value
|
|
||||||
perform_action(:aggregate_value)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def perform_action(method_name)
|
|
||||||
return builder.new(account, params).public_send(method_name) if builder.present?
|
|
||||||
|
|
||||||
log_invalid_metric
|
|
||||||
end
|
|
||||||
|
|
||||||
def builder
|
|
||||||
builder_class(params[:metric])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -3,7 +3,6 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|||||||
|
|
||||||
def build
|
def build
|
||||||
set_grouped_conversations_count
|
set_grouped_conversations_count
|
||||||
set_grouped_resolved_conversations_count
|
|
||||||
set_grouped_avg_reply_time
|
set_grouped_avg_reply_time
|
||||||
set_grouped_avg_first_response_time
|
set_grouped_avg_first_response_time
|
||||||
set_grouped_avg_resolution_time
|
set_grouped_avg_resolution_time
|
||||||
@@ -28,15 +27,6 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|||||||
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_grouped_resolved_conversations_count
|
|
||||||
@grouped_resolved_conversations_count = reporting_events
|
|
||||||
.where(name: 'conversation_resolved')
|
|
||||||
.select(group_by_key, :conversation_id)
|
|
||||||
.distinct
|
|
||||||
.group(group_by_key)
|
|
||||||
.count(:conversation_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def group_by_key
|
def group_by_key
|
||||||
:user_id
|
:user_id
|
||||||
end
|
end
|
||||||
@@ -47,14 +37,12 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|||||||
|
|
||||||
def prepare_report
|
def prepare_report
|
||||||
account.account_users.each_with_object([]) do |account_user, arr|
|
account.account_users.each_with_object([]) do |account_user, arr|
|
||||||
user_id = account_user.user_id
|
|
||||||
arr << {
|
arr << {
|
||||||
id: user_id,
|
id: account_user.user_id,
|
||||||
conversations_count: @grouped_conversations_count[user_id],
|
conversations_count: @grouped_conversations_count[account_user.user_id],
|
||||||
resolved_conversations_count: @grouped_resolved_conversations_count[user_id],
|
avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id],
|
||||||
avg_resolution_time: @grouped_avg_resolution_time[user_id],
|
avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id],
|
||||||
avg_first_response_time: @grouped_avg_first_response_time[user_id],
|
avg_reply_time: @grouped_avg_reply_time[account_user.user_id]
|
||||||
avg_reply_time: @grouped_avg_reply_time[user_id]
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
class V2::Reports::InboxSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|
||||||
pattr_initialize [:account!, :params!]
|
|
||||||
|
|
||||||
def build
|
|
||||||
set_grouped_conversations_count
|
|
||||||
set_grouped_resolved_conversations_count
|
|
||||||
set_grouped_avg_reply_time
|
|
||||||
set_grouped_avg_first_response_time
|
|
||||||
set_grouped_avg_resolution_time
|
|
||||||
prepare_report
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_grouped_conversations_count
|
|
||||||
@grouped_conversations_count = Current.account.conversations.where(created_at: range).group('inbox_id').count
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_grouped_avg_resolution_time
|
|
||||||
@grouped_avg_resolution_time = get_grouped_average(reporting_events.where(name: 'conversation_resolved'))
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_grouped_avg_first_response_time
|
|
||||||
@grouped_avg_first_response_time = get_grouped_average(reporting_events.where(name: 'first_response'))
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_grouped_avg_reply_time
|
|
||||||
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_grouped_resolved_conversations_count
|
|
||||||
@grouped_resolved_conversations_count = reporting_events
|
|
||||||
.where(name: 'conversation_resolved')
|
|
||||||
.select(group_by_key, :conversation_id)
|
|
||||||
.distinct
|
|
||||||
.group(group_by_key)
|
|
||||||
.count(:conversation_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def group_by_key
|
|
||||||
:inbox_id
|
|
||||||
end
|
|
||||||
|
|
||||||
def reporting_events
|
|
||||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range)
|
|
||||||
end
|
|
||||||
|
|
||||||
def prepare_report
|
|
||||||
account.inboxes.each_with_object([]) do |inbox, arr|
|
|
||||||
inbox_id = inbox.id
|
|
||||||
arr << {
|
|
||||||
id: inbox_id,
|
|
||||||
conversations_count: @grouped_conversations_count[inbox_id],
|
|
||||||
resolved_conversations_count: @grouped_resolved_conversations_count[inbox_id],
|
|
||||||
avg_resolution_time: @grouped_avg_resolution_time[inbox_id],
|
|
||||||
avg_first_response_time: @grouped_avg_first_response_time[inbox_id],
|
|
||||||
avg_reply_time: @grouped_avg_reply_time[inbox_id]
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def average_value_key
|
|
||||||
ActiveModel::Type::Boolean.new.cast(params[:business_hours]).present? ? :value_in_business_hours : :value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -3,7 +3,6 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|||||||
|
|
||||||
def build
|
def build
|
||||||
set_grouped_conversations_count
|
set_grouped_conversations_count
|
||||||
set_grouped_resolved_conversations_count
|
|
||||||
set_grouped_avg_reply_time
|
set_grouped_avg_reply_time
|
||||||
set_grouped_avg_first_response_time
|
set_grouped_avg_first_response_time
|
||||||
set_grouped_avg_resolution_time
|
set_grouped_avg_resolution_time
|
||||||
@@ -28,15 +27,6 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|||||||
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
@grouped_avg_reply_time = get_grouped_average(reporting_events.where(name: 'reply_time'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_grouped_resolved_conversations_count
|
|
||||||
@grouped_resolved_conversations_count = reporting_events
|
|
||||||
.where(name: 'conversation_resolved')
|
|
||||||
.select(group_by_key, :conversation_id)
|
|
||||||
.distinct
|
|
||||||
.group(group_by_key)
|
|
||||||
.count(:conversation_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def reporting_events
|
def reporting_events
|
||||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
|
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
|
||||||
end
|
end
|
||||||
@@ -50,7 +40,6 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
|||||||
arr << {
|
arr << {
|
||||||
id: team.id,
|
id: team.id,
|
||||||
conversations_count: @grouped_conversations_count[team.id],
|
conversations_count: @grouped_conversations_count[team.id],
|
||||||
resolved_conversations_count: @grouped_resolved_conversations_count[team.id],
|
|
||||||
avg_resolution_time: @grouped_avg_resolution_time[team.id],
|
avg_resolution_time: @grouped_avg_resolution_time[team.id],
|
||||||
avg_first_response_time: @grouped_avg_first_response_time[team.id],
|
avg_first_response_time: @grouped_avg_first_response_time[team.id],
|
||||||
avg_reply_time: @grouped_avg_reply_time[team.id]
|
avg_reply_time: @grouped_avg_reply_time[team.id]
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
class Api::V2::Accounts::LiveReportsController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :load_conversations, only: [:conversation_metrics, :grouped_conversation_metrics]
|
|
||||||
before_action :set_group_scope, only: [:grouped_conversation_metrics]
|
|
||||||
|
|
||||||
def conversation_metrics
|
|
||||||
render json: {
|
|
||||||
open: @conversations.open.count,
|
|
||||||
unattended: @conversations.open.unattended.count,
|
|
||||||
unassigned: @conversations.open.unassigned.count
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def grouped_conversation_metrics
|
|
||||||
count_by_group = @conversations.open.group(@group_scope).count
|
|
||||||
unattended_by_group = @conversations.open.unattended.group(@group_scope).count
|
|
||||||
unassigned_by_group = @conversations.open.unassigned.group(@group_scope).count
|
|
||||||
|
|
||||||
group_metrics = count_by_group.map do |group_id, count|
|
|
||||||
metric = {
|
|
||||||
open: count,
|
|
||||||
unattended: unattended_by_group[group_id] || 0,
|
|
||||||
unassigned: unassigned_by_group[group_id] || 0
|
|
||||||
}
|
|
||||||
metric[@group_scope] = group_id
|
|
||||||
metric
|
|
||||||
end
|
|
||||||
|
|
||||||
render json: group_metrics
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_group_scope
|
|
||||||
render json: { error: 'invalid group_by' }, status: :unprocessable_entity and return unless %w[
|
|
||||||
team_id
|
|
||||||
assignee_id
|
|
||||||
].include?(permitted_params[:group_by])
|
|
||||||
|
|
||||||
@group_scope = permitted_params[:group_by]
|
|
||||||
end
|
|
||||||
|
|
||||||
def team
|
|
||||||
return unless permitted_params[:team_id]
|
|
||||||
|
|
||||||
@team ||= Current.account.teams.find(permitted_params[:team_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_conversations
|
|
||||||
scope = Current.account.conversations
|
|
||||||
scope = scope.where(team_id: team.id) if team.present?
|
|
||||||
@conversations = scope
|
|
||||||
end
|
|
||||||
|
|
||||||
def permitted_params
|
|
||||||
params.permit(:team_id, :group_by)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseController
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :prepare_builder_params, only: [:agent, :team, :inbox]
|
before_action :prepare_builder_params, only: [:agent, :team]
|
||||||
|
|
||||||
def agent
|
def agent
|
||||||
render_report_with(V2::Reports::AgentSummaryBuilder)
|
render_report_with(V2::Reports::AgentSummaryBuilder)
|
||||||
@@ -10,10 +10,6 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
|
|||||||
render_report_with(V2::Reports::TeamSummaryBuilder)
|
render_report_with(V2::Reports::TeamSummaryBuilder)
|
||||||
end
|
end
|
||||||
|
|
||||||
def inbox
|
|
||||||
render_report_with(V2::Reports::InboxSummaryBuilder)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def check_authorization
|
def check_authorization
|
||||||
|
|||||||
@@ -94,8 +94,7 @@ module Api::V2::Accounts::HeatmapHelper
|
|||||||
end
|
end
|
||||||
|
|
||||||
def since_timestamp(date)
|
def since_timestamp(date)
|
||||||
number_of_days = params[:days_before].present? ? params[:days_before].to_i.days : 6.days
|
(date - 6.days).to_i.to_s
|
||||||
(date - number_of_days).to_i.to_s
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def until_timestamp(date)
|
def until_timestamp(date)
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
/* global axios */
|
|
||||||
import ApiClient from './ApiClient';
|
|
||||||
|
|
||||||
class LiveReportsAPI extends ApiClient {
|
|
||||||
constructor() {
|
|
||||||
super('live_reports', { accountScoped: true, apiVersion: 'v2' });
|
|
||||||
}
|
|
||||||
|
|
||||||
getConversationMetric(params = {}) {
|
|
||||||
return axios.get(`${this.url}/conversation_metrics`, { params });
|
|
||||||
}
|
|
||||||
|
|
||||||
getGroupedConversations({ groupBy } = { groupBy: 'assignee_id' }) {
|
|
||||||
return axios.get(`${this.url}/grouped_conversation_metrics`, {
|
|
||||||
params: { group_by: groupBy },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new LiveReportsAPI();
|
|
||||||
@@ -61,9 +61,9 @@ class ReportsAPI extends ApiClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getConversationTrafficCSV({ daysBefore = 6 } = {}) {
|
getConversationTrafficCSV() {
|
||||||
return axios.get(`${this.url}/conversation_traffic`, {
|
return axios.get(`${this.url}/conversation_traffic`, {
|
||||||
params: { timezone_offset: getTimeOffset(), days_before: daysBefore },
|
params: { timezone_offset: getTimeOffset() },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
/* global axios */
|
|
||||||
import ApiClient from './ApiClient';
|
|
||||||
|
|
||||||
class SummaryReportsAPI extends ApiClient {
|
|
||||||
constructor() {
|
|
||||||
super('summary_reports', { accountScoped: true, apiVersion: 'v2' });
|
|
||||||
}
|
|
||||||
|
|
||||||
getTeamReports({ since, until, businessHours } = {}) {
|
|
||||||
return axios.get(`${this.url}/team`, {
|
|
||||||
params: {
|
|
||||||
since,
|
|
||||||
until,
|
|
||||||
business_hours: businessHours,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getAgentReports({ since, until, businessHours } = {}) {
|
|
||||||
return axios.get(`${this.url}/agent`, {
|
|
||||||
params: {
|
|
||||||
since,
|
|
||||||
until,
|
|
||||||
business_hours: businessHours,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getInboxReports({ since, until, businessHours } = {}) {
|
|
||||||
return axios.get(`${this.url}/inbox`, {
|
|
||||||
params: {
|
|
||||||
since,
|
|
||||||
until,
|
|
||||||
business_hours: businessHours,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new SummaryReportsAPI();
|
|
||||||
@@ -116,13 +116,14 @@
|
|||||||
|
|
||||||
<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';
|
||||||
@@ -131,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';
|
||||||
@@ -158,14 +157,7 @@ export default {
|
|||||||
IntersectionObserver,
|
IntersectionObserver,
|
||||||
VirtualList,
|
VirtualList,
|
||||||
},
|
},
|
||||||
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.
|
||||||
@@ -210,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,
|
||||||
@@ -812,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',
|
||||||
{
|
{
|
||||||
@@ -822,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) {
|
||||||
@@ -840,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,
|
||||||
@@ -883,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',
|
||||||
{
|
{
|
||||||
@@ -893,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')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -910,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',
|
||||||
{
|
{
|
||||||
@@ -920,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) {
|
||||||
@@ -936,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) {
|
||||||
@@ -952,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) {
|
||||||
@@ -965,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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -30,9 +30,7 @@
|
|||||||
|
|
||||||
<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';
|
||||||
@@ -45,7 +43,7 @@ export default {
|
|||||||
PrimarySidebar,
|
PrimarySidebar,
|
||||||
SecondarySidebar,
|
SecondarySidebar,
|
||||||
},
|
},
|
||||||
mixins: [adminMixin, alertMixin, keyboardEventListenerMixins],
|
mixins: [keyboardEventListenerMixins],
|
||||||
props: {
|
props: {
|
||||||
showSecondarySidebar: {
|
showSecondarySidebar: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@@ -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'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -94,8 +94,7 @@
|
|||||||
|
|
||||||
<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,
|
||||||
@@ -111,13 +110,19 @@ import Policy from '../../policy.vue';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { SecondaryChildNavItem, Policy },
|
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',
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -481,6 +481,9 @@ export default {
|
|||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
isEmailContentType() {
|
||||||
|
return this.contentType === CONTENT_TYPES.INCOMING_EMAIL;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
data() {
|
data() {
|
||||||
@@ -502,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';
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { cloneObject } from '../clone'; // Update this path
|
||||||
|
|
||||||
|
describe('cloneObject', () => {
|
||||||
|
it('should clone a simple object', () => {
|
||||||
|
const original = { a: 1, b: 'string', c: true };
|
||||||
|
const cloned = cloneObject(original);
|
||||||
|
expect(cloned).toEqual(original);
|
||||||
|
expect(cloned).not.toBe(original);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clone a nested object', () => {
|
||||||
|
const original = { a: 1, b: { c: 2, d: { e: 3 } } };
|
||||||
|
const cloned = cloneObject(original);
|
||||||
|
expect(cloned).toEqual(original);
|
||||||
|
expect(cloned.b).not.toBe(original.b);
|
||||||
|
expect(cloned.b.d).not.toBe(original.b.d);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clone an array', () => {
|
||||||
|
const original = [1, 2, [3, 4]];
|
||||||
|
const cloned = cloneObject(original);
|
||||||
|
expect(cloned).toEqual(original);
|
||||||
|
expect(cloned).not.toBe(original);
|
||||||
|
expect(cloned[2]).not.toBe(original[2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clone a Date object', () => {
|
||||||
|
const original = new Date();
|
||||||
|
const cloned = cloneObject(original);
|
||||||
|
expect(cloned).toEqual(original);
|
||||||
|
expect(cloned).not.toBe(original);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use structuredClone when available', () => {
|
||||||
|
const structuredCloneSpy = vi.fn(x => x);
|
||||||
|
global.structuredClone = structuredCloneSpy;
|
||||||
|
const obj = { a: 1 };
|
||||||
|
|
||||||
|
cloneObject(obj);
|
||||||
|
|
||||||
|
expect(structuredCloneSpy).toHaveBeenCalledWith(obj);
|
||||||
|
delete global.structuredClone;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fall back to JSON methods when structuredClone is not available', () => {
|
||||||
|
const jsonParseSpy = vi.spyOn(JSON, 'parse');
|
||||||
|
const jsonStringifySpy = vi.spyOn(JSON, 'stringify');
|
||||||
|
|
||||||
|
const original = { a: 1 };
|
||||||
|
global.structuredClone = undefined;
|
||||||
|
|
||||||
|
cloneObject(original);
|
||||||
|
|
||||||
|
expect(jsonStringifySpy).toHaveBeenCalledWith(original);
|
||||||
|
expect(jsonParseSpy).toHaveBeenCalled();
|
||||||
|
|
||||||
|
jsonParseSpy.mockRestore();
|
||||||
|
jsonStringifySpy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -379,6 +379,7 @@
|
|||||||
},
|
},
|
||||||
"DETAILS": {
|
"DETAILS": {
|
||||||
"LOADING_FB": "Authenticating you with Facebook...",
|
"LOADING_FB": "Authenticating you with Facebook...",
|
||||||
|
"ERROR_FB_LOADING": "Error loading Facebook SDK. Please disable any ad-blockers and try again from a different browser.",
|
||||||
"ERROR_FB_AUTH": "Something went wrong, Please refresh page...",
|
"ERROR_FB_AUTH": "Something went wrong, Please refresh page...",
|
||||||
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
|
"ERROR_FB_UNAUTHORIZED": "You're not authorized to perform this action. ",
|
||||||
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
|
"ERROR_FB_UNAUTHORIZED_HELP": "Please ensure you have access to the Facebook page with full control. You can read more about Facebook roles <a href=\" https://www.facebook.com/help/187316341316631\">here</a>.",
|
||||||
|
|||||||
@@ -494,17 +494,6 @@
|
|||||||
"STATUS": "Status"
|
"STATUS": "Status"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"TEAM_CONVERSATIONS": {
|
|
||||||
"HEADER": "Conversations by teams",
|
|
||||||
"LOADING_MESSAGE": "Loading team metrics...",
|
|
||||||
"NO_AGENTS": "There is no data available",
|
|
||||||
"TABLE_HEADER": {
|
|
||||||
"AGENT": "Team",
|
|
||||||
"OPEN": "open",
|
|
||||||
"UNATTENDED": "Unattended",
|
|
||||||
"STATUS": "Status"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AGENT_STATUS": {
|
"AGENT_STATUS": {
|
||||||
"HEADER": "Agent status",
|
"HEADER": "Agent status",
|
||||||
"ONLINE": "Online",
|
"ONLINE": "Online",
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { OPEN_AI_EVENTS } from '../helper/AnalyticsHelper/events';
|
import { OPEN_AI_EVENTS } from '../helper/AnalyticsHelper/events';
|
||||||
import OpenAPI from '../api/integrations/openapi';
|
import OpenAPI from '../api/integrations/openapi';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [alertMixin],
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchIntegrationsIfRequired();
|
this.fetchIntegrationsIfRequired();
|
||||||
},
|
},
|
||||||
@@ -101,7 +100,7 @@ export default {
|
|||||||
} = result;
|
} = result;
|
||||||
return generatedMessage;
|
return generatedMessage;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR'));
|
useAlert(this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR'));
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
generateCustomAttributes,
|
generateCustomAttributes,
|
||||||
} from 'dashboard/helper/automationHelper';
|
} from 'dashboard/helper/automationHelper';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
computed: {
|
computed: {
|
||||||
@@ -138,14 +139,14 @@ export default {
|
|||||||
},
|
},
|
||||||
removeFilter(index) {
|
removeFilter(index) {
|
||||||
if (this.automation.conditions.length <= 1) {
|
if (this.automation.conditions.length <= 1) {
|
||||||
this.showAlert(this.$t('AUTOMATION.CONDITION.DELETE_MESSAGE'));
|
useAlert(this.$t('AUTOMATION.CONDITION.DELETE_MESSAGE'));
|
||||||
} else {
|
} else {
|
||||||
this.automation.conditions.splice(index, 1);
|
this.automation.conditions.splice(index, 1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeAction(index) {
|
removeAction(index) {
|
||||||
if (this.automation.actions.length <= 1) {
|
if (this.automation.actions.length <= 1) {
|
||||||
this.showAlert(this.$t('AUTOMATION.ACTION.DELETE_MESSAGE'));
|
useAlert(this.$t('AUTOMATION.ACTION.DELETE_MESSAGE'));
|
||||||
} else {
|
} else {
|
||||||
this.automation.actions.splice(index, 1);
|
this.automation.actions.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
export default {
|
|
||||||
props: {
|
|
||||||
trianglePosition: {
|
|
||||||
type: String,
|
|
||||||
default: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
cssVars() {
|
|
||||||
return {
|
|
||||||
'--triangle-position': this.trianglePosition + 'rem',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
import {
|
import {
|
||||||
MAXIMUM_FILE_UPLOAD_SIZE,
|
MAXIMUM_FILE_UPLOAD_SIZE,
|
||||||
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL,
|
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL,
|
||||||
@@ -38,13 +39,13 @@ export default {
|
|||||||
|
|
||||||
upload.create((error, blob) => {
|
upload.create((error, blob) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
this.showAlert(error);
|
useAlert(error);
|
||||||
} else {
|
} else {
|
||||||
this.attachFile({ file, blob });
|
this.attachFile({ file, blob });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
|
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
|
||||||
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE,
|
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE,
|
||||||
})
|
})
|
||||||
@@ -61,7 +62,7 @@ export default {
|
|||||||
if (checkFileSizeLimit(file, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE)) {
|
if (checkFileSizeLimit(file, MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE)) {
|
||||||
this.attachFile({ file });
|
this.attachFile({ file });
|
||||||
} else {
|
} else {
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
|
this.$t('CONVERSATION.FILE_SIZE_LIMIT', {
|
||||||
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE,
|
MAXIMUM_SUPPORTED_FILE_UPLOAD_SIZE,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
computed: {
|
|
||||||
...mapGetters({
|
|
||||||
currentUserRole: 'getCurrentRole',
|
|
||||||
}),
|
|
||||||
isAdmin() {
|
|
||||||
return this.currentUserRole === 'administrator';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { mount } from '@vue/test-utils';
|
|
||||||
import bulkActionsMixin from '../bulkActionsMixin';
|
|
||||||
describe('bulkActionsMixin', () => {
|
|
||||||
it('returns the prop and computed values for triangle position:', async () => {
|
|
||||||
const Component = {
|
|
||||||
render() {},
|
|
||||||
title: 'MyComponent',
|
|
||||||
mixins: [bulkActionsMixin],
|
|
||||||
};
|
|
||||||
const wrapper = mount(Component);
|
|
||||||
await wrapper.setProps({
|
|
||||||
trianglePosition: '10',
|
|
||||||
});
|
|
||||||
expect(wrapper.props().trianglePosition).toEqual('10');
|
|
||||||
expect(wrapper.vm.cssVars).toEqual({
|
|
||||||
'--triangle-position': '10rem',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
||||||
|
|
||||||
vi.mock('shared/helpers/FileHelper', () => ({
|
vi.mock('shared/helpers/FileHelper', () => ({
|
||||||
checkFileSizeLimit: vi.fn(),
|
checkFileSizeLimit: vi.fn(),
|
||||||
@@ -11,6 +12,10 @@ vi.mock('activestorage', () => ({
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('dashboard/composables', () => ({
|
||||||
|
useAlert: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('FileUploadMixin', () => {
|
describe('FileUploadMixin', () => {
|
||||||
let vm;
|
let vm;
|
||||||
|
|
||||||
@@ -53,9 +58,8 @@ describe('FileUploadMixin', () => {
|
|||||||
|
|
||||||
it('shows an alert if the file size exceeds the maximum limit', () => {
|
it('shows an alert if the file size exceeds the maximum limit', () => {
|
||||||
const fakeFile = { size: 999999999 };
|
const fakeFile = { size: 999999999 };
|
||||||
vm.showAlert = vi.fn();
|
|
||||||
vm.onDirectFileUpload(fakeFile);
|
vm.onDirectFileUpload(fakeFile);
|
||||||
expect(vm.showAlert).toHaveBeenCalledWith(expect.any(String));
|
expect(useAlert).toHaveBeenCalledWith(expect.any(String));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,9 +71,8 @@ describe('FileUploadMixin', () => {
|
|||||||
|
|
||||||
it('shows an alert if the file size exceeds the maximum limit', () => {
|
it('shows an alert if the file size exceeds the maximum limit', () => {
|
||||||
const fakeFile = { size: 999999999 };
|
const fakeFile = { size: 999999999 };
|
||||||
vm.showAlert = vi.fn();
|
|
||||||
vm.onIndirectFileUpload(fakeFile);
|
vm.onIndirectFileUpload(fakeFile);
|
||||||
expect(vm.showAlert).toHaveBeenCalledWith(expect.any(String));
|
expect(useAlert).toHaveBeenCalledWith(expect.any(String));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
|
||||||
import Vuex from 'vuex';
|
|
||||||
import isAdminMixin from '../isAdmin';
|
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
|
||||||
localVue.use(Vuex);
|
|
||||||
|
|
||||||
describe('isAdminMixin', () => {
|
|
||||||
let getters;
|
|
||||||
let store;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
getters = {
|
|
||||||
getCurrentRole: () => 'administrator',
|
|
||||||
};
|
|
||||||
|
|
||||||
store = new Vuex.Store({ getters });
|
|
||||||
});
|
|
||||||
it('set accountId properly', () => {
|
|
||||||
const Component = {
|
|
||||||
render() {},
|
|
||||||
title: 'TestComponent',
|
|
||||||
mixins: [isAdminMixin],
|
|
||||||
};
|
|
||||||
const wrapper = shallowMount(Component, { store, localVue });
|
|
||||||
expect(wrapper.vm.isAdmin).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import TimeMixin from '../time';
|
|
||||||
|
|
||||||
describe('#messageStamp', () => {
|
|
||||||
it('returns correct value', () => {
|
|
||||||
expect(TimeMixin.methods.messageStamp(1612971343)).toEqual('3:35 PM');
|
|
||||||
expect(TimeMixin.methods.messageStamp(1612971343, 'LLL d, h:mm a')).toEqual(
|
|
||||||
'Feb 10, 3:35 PM'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#messageTimestamp', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.useFakeTimers('modern');
|
|
||||||
|
|
||||||
const mockDate = new Date(2023, 4, 5);
|
|
||||||
vi.setSystemTime(mockDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return the message date in the specified format if the message was sent in the current year', () => {
|
|
||||||
expect(TimeMixin.methods.messageTimestamp(1680777464)).toEqual(
|
|
||||||
'Apr 6, 2023'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it('should return the message date and time in a different format if the message was sent in a different year', () => {
|
|
||||||
expect(TimeMixin.methods.messageTimestamp(1612971343)).toEqual(
|
|
||||||
'Feb 10 2021, 3:35 PM'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#dynamicTime', () => {
|
|
||||||
it('returns correct value', () => {
|
|
||||||
Date.now = vi.fn(() => new Date(Date.UTC(2023, 1, 14)).valueOf());
|
|
||||||
expect(TimeMixin.methods.dynamicTime(1612971343)).toEqual(
|
|
||||||
'about 2 years ago'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#dateFormat', () => {
|
|
||||||
it('returns correct value', () => {
|
|
||||||
expect(TimeMixin.methods.dateFormat(1612971343)).toEqual('Feb 10, 2021');
|
|
||||||
expect(TimeMixin.methods.dateFormat(1612971343, 'LLL d, yyyy')).toEqual(
|
|
||||||
'Feb 10, 2021'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#shortTimestamp', () => {
|
|
||||||
// Test cases when withAgo is false or not provided
|
|
||||||
it('returns correct value without ago', () => {
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('less than a minute ago')).toEqual(
|
|
||||||
'now'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 minute ago')).toEqual('1m');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('12 minutes ago')).toEqual('12m');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a minute ago')).toEqual('1m');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('an hour ago')).toEqual('1h');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 hour ago')).toEqual('1h');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('2 hours ago')).toEqual('2h');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 day ago')).toEqual('1d');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a day ago')).toEqual('1d');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('3 days ago')).toEqual('3d');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a month ago')).toEqual('1mo');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 month ago')).toEqual('1mo');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('2 months ago')).toEqual('2mo');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a year ago')).toEqual('1y');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 year ago')).toEqual('1y');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('4 years ago')).toEqual('4y');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test cases when withAgo is true
|
|
||||||
it('returns correct value with ago', () => {
|
|
||||||
expect(
|
|
||||||
TimeMixin.methods.shortTimestamp('less than a minute ago', true)
|
|
||||||
).toEqual('now');
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 minute ago', true)).toEqual(
|
|
||||||
'1m ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('12 minutes ago', true)).toEqual(
|
|
||||||
'12m ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a minute ago', true)).toEqual(
|
|
||||||
'1m ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('an hour ago', true)).toEqual(
|
|
||||||
'1h ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 hour ago', true)).toEqual(
|
|
||||||
'1h ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('2 hours ago', true)).toEqual(
|
|
||||||
'2h ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 day ago', true)).toEqual(
|
|
||||||
'1d ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a day ago', true)).toEqual(
|
|
||||||
'1d ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('3 days ago', true)).toEqual(
|
|
||||||
'3d ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a month ago', true)).toEqual(
|
|
||||||
'1mo ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 month ago', true)).toEqual(
|
|
||||||
'1mo ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('2 months ago', true)).toEqual(
|
|
||||||
'2mo ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('a year ago', true)).toEqual(
|
|
||||||
'1y ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('1 year ago', true)).toEqual(
|
|
||||||
'1y ago'
|
|
||||||
);
|
|
||||||
expect(TimeMixin.methods.shortTimestamp('4 years ago', true)).toEqual(
|
|
||||||
'4y ago'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
|
||||||
import uiSettingsMixin, {
|
|
||||||
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
|
||||||
DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
|
||||||
isEditorHotKeyEnabled,
|
|
||||||
} from '../uiSettings';
|
|
||||||
import Vuex from 'vuex';
|
|
||||||
const localVue = createLocalVue();
|
|
||||||
localVue.use(Vuex);
|
|
||||||
|
|
||||||
describe('uiSettingsMixin', () => {
|
|
||||||
let getters;
|
|
||||||
let actions;
|
|
||||||
let store;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
actions = { updateUISettings: vi.fn(), toggleSidebarUIState: vi.fn() };
|
|
||||||
getters = {
|
|
||||||
getUISettings: () => ({
|
|
||||||
enter_to_send_enabled: false,
|
|
||||||
is_ct_labels_open: true,
|
|
||||||
conversation_sidebar_items_order:
|
|
||||||
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
|
||||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
store = new Vuex.Store({ actions, getters });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns uiSettings', () => {
|
|
||||||
const Component = {
|
|
||||||
render() {},
|
|
||||||
title: 'TestComponent',
|
|
||||||
mixins: [uiSettingsMixin],
|
|
||||||
};
|
|
||||||
const wrapper = shallowMount(Component, { store, localVue });
|
|
||||||
expect(wrapper.vm.uiSettings).toEqual({
|
|
||||||
enter_to_send_enabled: false,
|
|
||||||
is_ct_labels_open: true,
|
|
||||||
conversation_sidebar_items_order:
|
|
||||||
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
|
||||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#updateUISettings', () => {
|
|
||||||
it('dispatches store actions correctly', () => {
|
|
||||||
const Component = {
|
|
||||||
render() {},
|
|
||||||
title: 'TestComponent',
|
|
||||||
mixins: [uiSettingsMixin],
|
|
||||||
};
|
|
||||||
const wrapper = shallowMount(Component, { store, localVue });
|
|
||||||
wrapper.vm.updateUISettings({ enter_to_send_enabled: true });
|
|
||||||
expect(actions.updateUISettings).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#toggleSidebarUIState', () => {
|
|
||||||
it('dispatches store actions correctly', () => {
|
|
||||||
const Component = {
|
|
||||||
render() {},
|
|
||||||
title: 'TestComponent',
|
|
||||||
mixins: [uiSettingsMixin],
|
|
||||||
};
|
|
||||||
const wrapper = shallowMount(Component, { store, localVue });
|
|
||||||
wrapper.vm.toggleSidebarUIState('is_ct_labels_open');
|
|
||||||
expect(actions.updateUISettings).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
{
|
|
||||||
uiSettings: {
|
|
||||||
enter_to_send_enabled: false,
|
|
||||||
is_ct_labels_open: false,
|
|
||||||
conversation_sidebar_items_order:
|
|
||||||
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER,
|
|
||||||
contact_sidebar_items_order: DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#isContactSidebarItemOpen', () => {
|
|
||||||
it('returns correct values', () => {
|
|
||||||
const Component = {
|
|
||||||
render() {},
|
|
||||||
title: 'TestComponent',
|
|
||||||
mixins: [uiSettingsMixin],
|
|
||||||
};
|
|
||||||
const wrapper = shallowMount(Component, { store, localVue });
|
|
||||||
expect(wrapper.vm.isContactSidebarItemOpen('is_ct_labels_open')).toEqual(
|
|
||||||
true
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
wrapper.vm.isContactSidebarItemOpen('is_ct_prev_conv_open')
|
|
||||||
).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#conversationSidebarItemsOrder', () => {
|
|
||||||
it('returns correct values', () => {
|
|
||||||
const Component = {
|
|
||||||
render() {},
|
|
||||||
title: 'TestComponent',
|
|
||||||
mixins: [uiSettingsMixin],
|
|
||||||
};
|
|
||||||
const wrapper = shallowMount(Component, { store, localVue });
|
|
||||||
expect(wrapper.vm.conversationSidebarItemsOrder).toEqual([
|
|
||||||
{ name: 'conversation_actions' },
|
|
||||||
{ name: 'macros' },
|
|
||||||
{ name: 'conversation_info' },
|
|
||||||
{ name: 'contact_attributes' },
|
|
||||||
{ name: 'previous_conversation' },
|
|
||||||
{ name: 'conversation_participants' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('#contactSidebarItemsOrder', () => {
|
|
||||||
it('returns correct values', () => {
|
|
||||||
const Component = {
|
|
||||||
render() {},
|
|
||||||
title: 'TestComponent',
|
|
||||||
mixins: [uiSettingsMixin],
|
|
||||||
};
|
|
||||||
const wrapper = shallowMount(Component, { store, localVue });
|
|
||||||
expect(wrapper.vm.contactSidebarItemsOrder).toEqual([
|
|
||||||
{ name: 'contact_attributes' },
|
|
||||||
{ name: 'contact_labels' },
|
|
||||||
{ name: 'previous_conversation' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isEditorHotKeyEnabled', () => {
|
|
||||||
it('returns true if hot key is not configured and enter to send flag is true', () => {
|
|
||||||
expect(
|
|
||||||
isEditorHotKeyEnabled({ enter_to_send_enabled: true }, 'enter')
|
|
||||||
).toEqual(true);
|
|
||||||
expect(
|
|
||||||
isEditorHotKeyEnabled({ enter_to_send_enabled: true }, 'cmd_enter')
|
|
||||||
).toEqual(false);
|
|
||||||
|
|
||||||
expect(isEditorHotKeyEnabled({}, 'cmd_enter')).toEqual(true);
|
|
||||||
expect(isEditorHotKeyEnabled({}, 'enter')).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns correct value if hot key is configured', () => {
|
|
||||||
expect(
|
|
||||||
isEditorHotKeyEnabled({ editor_message_key: 'enter' }, 'enter')
|
|
||||||
).toEqual(true);
|
|
||||||
expect(
|
|
||||||
isEditorHotKeyEnabled({ editor_message_key: 'cmd_enter' }, 'enter')
|
|
||||||
).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import {
|
|
||||||
format,
|
|
||||||
isSameYear,
|
|
||||||
fromUnixTime,
|
|
||||||
formatDistanceToNow,
|
|
||||||
} from 'date-fns';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
methods: {
|
|
||||||
messageStamp(time, dateFormat = 'h:mm a') {
|
|
||||||
const unixTime = fromUnixTime(time);
|
|
||||||
return format(unixTime, dateFormat);
|
|
||||||
},
|
|
||||||
messageTimestamp(time, dateFormat = 'MMM d, yyyy') {
|
|
||||||
const messageTime = fromUnixTime(time);
|
|
||||||
const now = new Date();
|
|
||||||
const messageDate = format(messageTime, dateFormat);
|
|
||||||
if (!isSameYear(messageTime, now)) {
|
|
||||||
return format(messageTime, 'LLL d y, h:mm a');
|
|
||||||
}
|
|
||||||
return messageDate;
|
|
||||||
},
|
|
||||||
dynamicTime(time) {
|
|
||||||
const unixTime = fromUnixTime(time);
|
|
||||||
return formatDistanceToNow(unixTime, { addSuffix: true });
|
|
||||||
},
|
|
||||||
dateFormat(time, dateFormat = 'MMM d, yyyy') {
|
|
||||||
const unixTime = fromUnixTime(time);
|
|
||||||
return format(unixTime, dateFormat);
|
|
||||||
},
|
|
||||||
shortTimestamp(time, withAgo = false) {
|
|
||||||
// This function takes a time string and converts it to a short time string
|
|
||||||
// with the following format: 1m, 1h, 1d, 1mo, 1y
|
|
||||||
// The function also takes an optional boolean parameter withAgo
|
|
||||||
// which will add the word "ago" to the end of the time string
|
|
||||||
const suffix = withAgo ? ' ago' : '';
|
|
||||||
const timeMappings = {
|
|
||||||
'less than a minute ago': 'now',
|
|
||||||
'a minute ago': `1m${suffix}`,
|
|
||||||
'an hour ago': `1h${suffix}`,
|
|
||||||
'a day ago': `1d${suffix}`,
|
|
||||||
'a month ago': `1mo${suffix}`,
|
|
||||||
'a year ago': `1y${suffix}`,
|
|
||||||
};
|
|
||||||
// Check if the time string is one of the specific cases
|
|
||||||
if (timeMappings[time]) {
|
|
||||||
return timeMappings[time];
|
|
||||||
}
|
|
||||||
const convertToShortTime = time
|
|
||||||
.replace(/about|over|almost|/g, '')
|
|
||||||
.replace(' minute ago', `m${suffix}`)
|
|
||||||
.replace(' minutes ago', `m${suffix}`)
|
|
||||||
.replace(' hour ago', `h${suffix}`)
|
|
||||||
.replace(' hours ago', `h${suffix}`)
|
|
||||||
.replace(' day ago', `d${suffix}`)
|
|
||||||
.replace(' days ago', `d${suffix}`)
|
|
||||||
.replace(' month ago', `mo${suffix}`)
|
|
||||||
.replace(' months ago', `mo${suffix}`)
|
|
||||||
.replace(' year ago', `y${suffix}`)
|
|
||||||
.replace(' years ago', `y${suffix}`);
|
|
||||||
return convertToShortTime;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
export const DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER = [
|
|
||||||
{ name: 'conversation_actions' },
|
|
||||||
{ name: 'macros' },
|
|
||||||
{ name: 'conversation_info' },
|
|
||||||
{ name: 'contact_attributes' },
|
|
||||||
{ name: 'previous_conversation' },
|
|
||||||
{ name: 'conversation_participants' },
|
|
||||||
];
|
|
||||||
export const DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER = [
|
|
||||||
{ name: 'contact_attributes' },
|
|
||||||
{ name: 'contact_labels' },
|
|
||||||
{ name: 'previous_conversation' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const slugifyChannel = name =>
|
|
||||||
name?.toLowerCase().replace(' ', '_').replace('-', '_').replace('::', '_');
|
|
||||||
|
|
||||||
export const isEditorHotKeyEnabled = (uiSettings, key) => {
|
|
||||||
const {
|
|
||||||
editor_message_key: editorMessageKey,
|
|
||||||
enter_to_send_enabled: enterToSendEnabled,
|
|
||||||
} = uiSettings || {};
|
|
||||||
if (!editorMessageKey) {
|
|
||||||
if (enterToSendEnabled) {
|
|
||||||
return key === 'enter';
|
|
||||||
}
|
|
||||||
return key === 'cmd_enter';
|
|
||||||
}
|
|
||||||
return editorMessageKey === key;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
computed: {
|
|
||||||
...mapGetters({ uiSettings: 'getUISettings' }),
|
|
||||||
conversationSidebarItemsOrder() {
|
|
||||||
const { conversation_sidebar_items_order: itemsOrder } = this.uiSettings;
|
|
||||||
// If the sidebar order is not set, use the default order.
|
|
||||||
if (!itemsOrder) {
|
|
||||||
return DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER;
|
|
||||||
}
|
|
||||||
// If the sidebar order doesn't have the new elements, then add them to the list.
|
|
||||||
DEFAULT_CONVERSATION_SIDEBAR_ITEMS_ORDER.forEach(item => {
|
|
||||||
if (!itemsOrder.find(i => i.name === item.name)) {
|
|
||||||
itemsOrder.push(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return itemsOrder;
|
|
||||||
},
|
|
||||||
contactSidebarItemsOrder() {
|
|
||||||
const { contact_sidebar_items_order: itemsOrder } = this.uiSettings;
|
|
||||||
return itemsOrder || DEFAULT_CONTACT_SIDEBAR_ITEMS_ORDER;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
updateUISettings(uiSettings = {}) {
|
|
||||||
this.$store.dispatch('updateUISettings', {
|
|
||||||
uiSettings: {
|
|
||||||
...this.uiSettings,
|
|
||||||
...uiSettings,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
isContactSidebarItemOpen(key) {
|
|
||||||
const { [key]: isOpen } = this.uiSettings;
|
|
||||||
return !!isOpen;
|
|
||||||
},
|
|
||||||
toggleSidebarUIState(key) {
|
|
||||||
this.updateUISettings({ [key]: !this.isContactSidebarItemOpen(key) });
|
|
||||||
},
|
|
||||||
setSignatureFlagForInbox(channelType, value) {
|
|
||||||
if (!channelType) return;
|
|
||||||
|
|
||||||
channelType = slugifyChannel(channelType);
|
|
||||||
this.updateUISettings({
|
|
||||||
[`${channelType}_signature_enabled`]: value,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
fetchSignatureFlagFromUiSettings(channelType) {
|
|
||||||
if (!channelType) return false;
|
|
||||||
|
|
||||||
channelType = slugifyChannel(channelType);
|
|
||||||
return this.uiSettings[`${channelType}_signature_enabled`];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import MergeContact from 'dashboard/modules/contact/components/MergeContact.vue';
|
import MergeContact from 'dashboard/modules/contact/components/MergeContact.vue';
|
||||||
|
|
||||||
import ContactAPI from 'dashboard/api/contacts';
|
import ContactAPI from 'dashboard/api/contacts';
|
||||||
@@ -29,7 +29,6 @@ import { CONTACTS_EVENTS } from '../../helper/AnalyticsHelper/events';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { MergeContact },
|
components: { MergeContact },
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
primaryContact: {
|
primaryContact: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -68,7 +67,7 @@ export default {
|
|||||||
contact => contact.id !== this.primaryContact.id
|
contact => contact.id !== this.primaryContact.id
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(this.$t('MERGE_CONTACTS.SEARCH.ERROR_MESSAGE'));
|
useAlert(this.$t('MERGE_CONTACTS.SEARCH.ERROR_MESSAGE'));
|
||||||
} finally {
|
} finally {
|
||||||
this.isSearching = false;
|
this.isSearching = false;
|
||||||
}
|
}
|
||||||
@@ -80,10 +79,10 @@ export default {
|
|||||||
childId: this.primaryContact.id,
|
childId: this.primaryContact.id,
|
||||||
parentId: parentContactId,
|
parentId: parentContactId,
|
||||||
});
|
});
|
||||||
this.showAlert(this.$t('MERGE_CONTACTS.FORM.SUCCESS_MESSAGE'));
|
useAlert(this.$t('MERGE_CONTACTS.FORM.SUCCESS_MESSAGE'));
|
||||||
this.onClose();
|
this.onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(this.$t('MERGE_CONTACTS.FORM.ERROR_MESSAGE'));
|
useAlert(this.$t('MERGE_CONTACTS.FORM.ERROR_MESSAGE'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
:label="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.LABEL')"
|
:label="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.LABEL')"
|
||||||
:placeholder="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.PLACEHOLDER')"
|
:placeholder="$t('CUSTOM_ATTRIBUTES.FORM.VALUE.PLACEHOLDER')"
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-end items-center py-2 px-0 gap-2">
|
<div class="flex items-center justify-end gap-2 px-0 py-2">
|
||||||
<woot-button
|
<woot-button
|
||||||
:is-disabled="$v.attributeName.$invalid || isCreating"
|
:is-disabled="$v.attributeName.$invalid || isCreating"
|
||||||
:is-loading="isCreating"
|
:is-loading="isCreating"
|
||||||
@@ -38,14 +38,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Modal from 'dashboard/components/Modal.vue';
|
import Modal from 'dashboard/components/Modal.vue';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
import { required, minLength } from 'vuelidate/lib/validators';
|
import { required, minLength } from 'vuelidate/lib/validators';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Modal,
|
Modal,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
show: {
|
show: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="multiselect-wrap--medium flex">
|
<div class="flex multiselect-wrap--medium">
|
||||||
<div
|
<div
|
||||||
class="w-8 relative text-base text-slate-100 dark:text-slate-600 after:content-[''] after:h-12 after:w-0 after:left-4 after:absolute after:border-l after:border-solid after:border-slate-100 after:dark:border-slate-600 before:content-[''] before:h-0 before:w-4 before:left-4 before:top-12 before:absolute before:border-b before:border-solid before:border-slate-100 before:dark:border-slate-600"
|
class="w-8 relative text-base text-slate-100 dark:text-slate-600 after:content-[''] after:h-12 after:w-0 after:left-4 after:absolute after:border-l after:border-solid after:border-slate-100 after:dark:border-slate-600 before:content-[''] before:h-0 before:w-4 before:left-4 before:top-12 before:absolute before:border-b before:border-solid before:border-slate-100 before:dark:border-slate-600"
|
||||||
>
|
>
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
:primary-contact-name="primaryContact.name"
|
:primary-contact-name="primaryContact.name"
|
||||||
:parent-contact-name="parentContactName"
|
:parent-contact-name="parentContactName"
|
||||||
/>
|
/>
|
||||||
<div class="mt-6 flex gap-2 justify-end">
|
<div class="flex justify-end gap-2 mt-6">
|
||||||
<woot-button variant="clear" @click.prevent="onCancel">
|
<woot-button variant="clear" @click.prevent="onCancel">
|
||||||
{{ $t('MERGE_CONTACTS.FORM.CANCEL') }}
|
{{ $t('MERGE_CONTACTS.FORM.CANCEL') }}
|
||||||
</woot-button>
|
</woot-button>
|
||||||
@@ -114,7 +114,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
import { required } from 'vuelidate/lib/validators';
|
import { required } from 'vuelidate/lib/validators';
|
||||||
|
|
||||||
import MergeContactSummary from 'dashboard/modules/contact/components/MergeContactSummary.vue';
|
import MergeContactSummary from 'dashboard/modules/contact/components/MergeContactSummary.vue';
|
||||||
@@ -122,7 +121,6 @@ import ContactDropdownItem from './ContactDropdownItem.vue';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { MergeContactSummary, ContactDropdownItem },
|
components: { MergeContactSummary, ContactDropdownItem },
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
primaryContact: {
|
primaryContact: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue';
|
import AddCannedModal from 'dashboard/routes/dashboard/settings/canned/AddCanned.vue';
|
||||||
@@ -123,7 +123,7 @@ export default {
|
|||||||
TranslateModal,
|
TranslateModal,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, messageFormatterMixin],
|
mixins: [messageFormatterMixin],
|
||||||
props: {
|
props: {
|
||||||
message: {
|
message: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -183,12 +183,12 @@ export default {
|
|||||||
await copyTextToClipboard(
|
await copyTextToClipboard(
|
||||||
`${fullConversationURL}?messageId=${this.messageId}`
|
`${fullConversationURL}?messageId=${this.messageId}`
|
||||||
);
|
);
|
||||||
this.showAlert(this.$t('CONVERSATION.CONTEXT_MENU.LINK_COPIED'));
|
useAlert(this.$t('CONVERSATION.CONTEXT_MENU.LINK_COPIED'));
|
||||||
this.handleClose();
|
this.handleClose();
|
||||||
},
|
},
|
||||||
async handleCopy() {
|
async handleCopy() {
|
||||||
await copyTextToClipboard(this.plainTextContent);
|
await copyTextToClipboard(this.plainTextContent);
|
||||||
this.showAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
useAlert(this.$t('CONTACT_PANEL.COPY_SUCCESSFUL'));
|
||||||
this.handleClose();
|
this.handleClose();
|
||||||
},
|
},
|
||||||
showCannedResponseModal() {
|
showCannedResponseModal() {
|
||||||
@@ -233,10 +233,10 @@ export default {
|
|||||||
conversationId: this.conversationId,
|
conversationId: this.conversationId,
|
||||||
messageId: this.messageId,
|
messageId: this.messageId,
|
||||||
});
|
});
|
||||||
this.showAlert(this.$t('CONVERSATION.SUCCESS_DELETE_MESSAGE'));
|
useAlert(this.$t('CONVERSATION.SUCCESS_DELETE_MESSAGE'));
|
||||||
this.handleClose();
|
this.handleClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(this.$t('CONVERSATION.FAIL_DELETE_MESSSAGE'));
|
useAlert(this.$t('CONVERSATION.FAIL_DELETE_MESSSAGE'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closeDeleteModal() {
|
closeDeleteModal() {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col mb-2 p-4 border border-solid border-slate-75 dark:border-slate-700 overflow-hidden rounded-md flex-grow shadow-sm bg-white dark:bg-slate-900 text-slate-700 dark:text-slate-100 note-wrap"
|
class="flex flex-col flex-grow p-4 mb-2 overflow-hidden bg-white border border-solid rounded-md shadow-sm border-slate-75 dark:border-slate-700 dark:bg-slate-900 text-slate-700 dark:text-slate-100 note-wrap"
|
||||||
>
|
>
|
||||||
<div class="flex justify-between items-end text-xs gap-1">
|
<div class="flex items-end justify-between gap-1 text-xs">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<thumbnail
|
<thumbnail
|
||||||
:title="noteAuthorName"
|
:title="noteAuthorName"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions flex invisible">
|
<div class="flex invisible actions">
|
||||||
<woot-button
|
<woot-button
|
||||||
v-tooltip="$t('NOTES.CONTENT_HEADER.DELETE')"
|
v-tooltip="$t('NOTES.CONTENT_HEADER.DELETE')"
|
||||||
variant="smooth"
|
variant="smooth"
|
||||||
@@ -45,22 +45,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-dompurify-html="formatMessage(note || '')"
|
v-dompurify-html="formatMessage(note || '')"
|
||||||
class="note__content mt-4"
|
class="mt-4 note__content"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [timeMixin, messageFormatterMixin],
|
mixins: [messageFormatterMixin],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
id: {
|
id: {
|
||||||
@@ -87,7 +87,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
readableTime() {
|
readableTime() {
|
||||||
return this.dynamicTime(this.createdAt);
|
return dynamicTime(this.createdAt);
|
||||||
},
|
},
|
||||||
noteAuthor() {
|
noteAuthor() {
|
||||||
return this.user || {};
|
return this.user || {};
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
</h5>
|
</h5>
|
||||||
<h5
|
<h5
|
||||||
v-if="email"
|
v-if="email"
|
||||||
class="text-sm email text-slate-700 dark:text-slate-200 overflow-hidden whitespace-nowrap text-ellipsis"
|
class="overflow-hidden text-sm email text-slate-700 dark:text-slate-200 whitespace-nowrap text-ellipsis"
|
||||||
>
|
>
|
||||||
<span class="pre-text">{{ $t('SEARCH.EMAIL') }}:</span>
|
<span class="pre-text">{{ $t('SEARCH.EMAIL') }}:</span>
|
||||||
{{ email }}
|
{{ email }}
|
||||||
@@ -40,14 +40,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { frontendURL } from 'dashboard/helper/URLHelper.js';
|
import { frontendURL } from 'dashboard/helper/URLHelper.js';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
import InboxName from 'dashboard/components/widgets/InboxName.vue';
|
import InboxName from 'dashboard/components/widgets/InboxName.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
InboxName,
|
InboxName,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin],
|
|
||||||
props: {
|
props: {
|
||||||
id: {
|
id: {
|
||||||
type: Number,
|
type: Number,
|
||||||
@@ -90,7 +89,7 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
createdAtTime() {
|
createdAtTime() {
|
||||||
return this.dynamicTime(this.createdAt);
|
return dynamicTime(this.createdAt);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="app-wrapper h-full flex-grow-0 min-h-0 w-full max-w-full ml-auto mr-auto flex flex-wrap dark:text-slate-300"
|
class="flex flex-wrap flex-grow-0 w-full h-full max-w-full min-h-0 ml-auto mr-auto app-wrapper dark:text-slate-300"
|
||||||
>
|
>
|
||||||
<sidebar
|
<sidebar
|
||||||
:route="currentRoute"
|
:route="currentRoute"
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||||
@show-add-label-popup="showAddLabelPopup"
|
@show-add-label-popup="showAddLabelPopup"
|
||||||
/>
|
/>
|
||||||
<section class="flex h-full min-h-0 overflow-hidden flex-1 px-0">
|
<section class="flex flex-1 h-full min-h-0 px-0 overflow-hidden">
|
||||||
<router-view />
|
<router-view />
|
||||||
<command-bar />
|
<command-bar />
|
||||||
<account-selector
|
<account-selector
|
||||||
@@ -47,7 +47,7 @@ import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAc
|
|||||||
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
|
import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector.vue';
|
||||||
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
|
import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
|
||||||
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
import NotificationPanel from 'dashboard/routes/dashboard/notifications/components/NotificationPanel.vue';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
const CommandBar = () => import('./commands/commandbar.vue');
|
const CommandBar = () => import('./commands/commandbar.vue');
|
||||||
|
|
||||||
@@ -61,7 +61,14 @@ export default {
|
|||||||
AddLabelModal,
|
AddLabelModal,
|
||||||
NotificationPanel,
|
NotificationPanel,
|
||||||
},
|
},
|
||||||
mixins: [uiSettingsMixin],
|
setup() {
|
||||||
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
uiSettings,
|
||||||
|
updateUISettings,
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showAccountModal: false,
|
showAccountModal: false,
|
||||||
|
|||||||
@@ -21,12 +21,10 @@ import appearanceHotKeys from './appearanceHotKeys';
|
|||||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||||
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||||
import conversationTeamMixin from 'dashboard/mixins/conversation/teamMixin';
|
import conversationTeamMixin from 'dashboard/mixins/conversation/teamMixin';
|
||||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
|
||||||
import { GENERAL_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
import { GENERAL_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [
|
mixins: [
|
||||||
adminMixin,
|
|
||||||
agentMixin,
|
agentMixin,
|
||||||
conversationHotKeysMixin,
|
conversationHotKeysMixin,
|
||||||
bulkActionsHotKeysMixin,
|
bulkActionsHotKeysMixin,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from './CommandBarIcons';
|
} from './CommandBarIcons';
|
||||||
import { frontendURL } from '../../../helper/URLHelper';
|
import { frontendURL } from '../../../helper/URLHelper';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||||
import { FEATURE_FLAGS } from '../../../featureFlags';
|
import { FEATURE_FLAGS } from '../../../featureFlags';
|
||||||
|
|
||||||
const GO_TO_COMMANDS = [
|
const GO_TO_COMMANDS = [
|
||||||
@@ -172,6 +173,12 @@ const GO_TO_COMMANDS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
setup() {
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
|
return {
|
||||||
|
isAdmin,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ import ContactInfo from 'dashboard/routes/dashboard/conversation/contact/Contact
|
|||||||
import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue';
|
import ContactLabel from 'dashboard/routes/dashboard/contacts/components/ContactLabels.vue';
|
||||||
import CustomAttributes from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue';
|
import CustomAttributes from 'dashboard/routes/dashboard/conversation/customAttributes/CustomAttributes.vue';
|
||||||
import draggable from 'vuedraggable';
|
import draggable from 'vuedraggable';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -97,7 +97,6 @@ export default {
|
|||||||
CustomAttributes,
|
CustomAttributes,
|
||||||
draggable,
|
draggable,
|
||||||
},
|
},
|
||||||
mixins: [uiSettingsMixin],
|
|
||||||
props: {
|
props: {
|
||||||
contact: {
|
contact: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@@ -116,6 +115,21 @@ export default {
|
|||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const {
|
||||||
|
updateUISettings,
|
||||||
|
isContactSidebarItemOpen,
|
||||||
|
contactSidebarItemsOrder,
|
||||||
|
toggleSidebarUIState,
|
||||||
|
} = useUISettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateUISettings,
|
||||||
|
isContactSidebarItemOpen,
|
||||||
|
contactSidebarItemsOrder,
|
||||||
|
toggleSidebarUIState,
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dragEnabled: true,
|
dragEnabled: true,
|
||||||
|
|||||||
@@ -9,12 +9,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
import LabelSelector from 'dashboard/components/widgets/LabelSelector.vue';
|
import LabelSelector from 'dashboard/components/widgets/LabelSelector.vue';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { LabelSelector },
|
components: { LabelSelector },
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
contactId: {
|
contactId: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
@@ -59,7 +58,7 @@ export default {
|
|||||||
labels: selectedLabels,
|
labels: selectedLabels,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(this.$t('CONTACT_PANEL.LABELS.CONTACT.ERROR'));
|
useAlert(this.$t('CONTACT_PANEL.LABELS.CONTACT.ERROR'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -89,7 +89,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { required } from 'vuelidate/lib/validators';
|
import { required } from 'vuelidate/lib/validators';
|
||||||
import FilterInputBox from '../../../../components/widgets/FilterInput/Index.vue';
|
import FilterInputBox from '../../../../components/widgets/FilterInput/Index.vue';
|
||||||
import countries from 'shared/constants/countries.js';
|
import countries from 'shared/constants/countries.js';
|
||||||
@@ -98,11 +98,13 @@ import { filterAttributeGroups } from '../contactFilterItems';
|
|||||||
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 { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
import { CONTACTS_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,
|
||||||
@@ -308,7 +310,7 @@ export default {
|
|||||||
},
|
},
|
||||||
removeFilter(index) {
|
removeFilter(index) {
|
||||||
if (this.appliedFilters.length <= 1) {
|
if (this.appliedFilters.length <= 1) {
|
||||||
this.showAlert(this.$t('CONTACTS_FILTER.FILTER_DELETE_ERROR'));
|
useAlert(this.$t('CONTACTS_FILTER.FILTER_DELETE_ERROR'));
|
||||||
} else {
|
} else {
|
||||||
this.appliedFilters.splice(index, 1);
|
this.appliedFilters.splice(index, 1);
|
||||||
}
|
}
|
||||||
@@ -318,7 +320,7 @@ export default {
|
|||||||
if (this.$v.$invalid) return;
|
if (this.$v.$invalid) return;
|
||||||
this.$store.dispatch(
|
this.$store.dispatch(
|
||||||
'contacts/setContactFilters',
|
'contacts/setContactFilters',
|
||||||
JSON.parse(JSON.stringify(this.appliedFilters))
|
cloneObject(this.appliedFilters)
|
||||||
);
|
);
|
||||||
this.$emit('applyFilter', this.appliedFilters);
|
this.$emit('applyFilter', this.appliedFilters);
|
||||||
this.$track(CONTACTS_EVENTS.APPLY_FILTER, {
|
this.$track(CONTACTS_EVENTS.APPLY_FILTER, {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
class="contacts-table-wrap bg-white dark:bg-slate-900 flex-1 h-full overflow-hidden -mt-1"
|
class="flex-1 h-full -mt-1 overflow-hidden bg-white contacts-table-wrap dark:bg-slate-900"
|
||||||
>
|
>
|
||||||
<ve-table
|
<ve-table
|
||||||
:fixed-header="true"
|
:fixed-header="true"
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
v-else-if="!isLoading && !contacts.length"
|
v-else-if="!isLoading && !contacts.length"
|
||||||
:title="$t('CONTACTS_PAGE.LIST.NO_CONTACTS')"
|
:title="$t('CONTACTS_PAGE.LIST.NO_CONTACTS')"
|
||||||
/>
|
/>
|
||||||
<div v-if="isLoading" class="items-center flex text-base justify-center">
|
<div v-if="isLoading" class="flex items-center justify-center text-base">
|
||||||
<spinner />
|
<spinner />
|
||||||
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
<span>{{ $t('CONTACTS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,7 @@ import { getCountryFlag } from 'dashboard/helper/flag';
|
|||||||
import Spinner from 'shared/components/Spinner.vue';
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||||
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export default {
|
|||||||
Spinner,
|
Spinner,
|
||||||
VeTable,
|
VeTable,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, rtlMixin],
|
mixins: [rtlMixin],
|
||||||
props: {
|
props: {
|
||||||
contacts: {
|
contacts: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@@ -105,9 +105,9 @@ export default {
|
|||||||
countryCode: additional.country_code,
|
countryCode: additional.country_code,
|
||||||
conversationsCount: item.conversations_count || '---',
|
conversationsCount: item.conversations_count || '---',
|
||||||
last_activity_at: lastActivityAt
|
last_activity_at: lastActivityAt
|
||||||
? this.dynamicTime(lastActivityAt)
|
? dynamicTime(lastActivityAt)
|
||||||
: '---',
|
: '---',
|
||||||
created_at: createdAt ? this.dynamicTime(createdAt) : '---',
|
created_at: createdAt ? dynamicTime(createdAt) : '---',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -134,7 +134,7 @@ export default {
|
|||||||
status={row.availability_status}
|
status={row.availability_status}
|
||||||
/>
|
/>
|
||||||
<div class="user-block">
|
<div class="user-block">
|
||||||
<h6 class="text-base overflow-hidden whitespace-nowrap text-ellipsis">
|
<h6 class="overflow-hidden text-base whitespace-nowrap text-ellipsis">
|
||||||
<router-link
|
<router-link
|
||||||
to={`/app/accounts/${this.$route.params.accountId}/contacts/${row.id}`}
|
to={`/app/accounts/${this.$route.params.accountId}/contacts/${row.id}`}
|
||||||
class="user-name"
|
class="user-name"
|
||||||
|
|||||||
@@ -81,6 +81,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
|
||||||
import ContactsHeader from './Header.vue';
|
import ContactsHeader from './Header.vue';
|
||||||
import ContactsTable from './ContactsTable.vue';
|
import ContactsTable from './ContactsTable.vue';
|
||||||
@@ -94,7 +95,6 @@ import filterQueryGenerator from '../../../../helper/filterQueryGenerator';
|
|||||||
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 { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
import countries from 'shared/constants/countries.js';
|
import countries from 'shared/constants/countries.js';
|
||||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||||
|
|
||||||
@@ -113,7 +113,6 @@ export default {
|
|||||||
AddCustomViews,
|
AddCustomViews,
|
||||||
DeleteCustomViews,
|
DeleteCustomViews,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
label: { type: String, default: '' },
|
label: { type: String, default: '' },
|
||||||
segmentsId: {
|
segmentsId: {
|
||||||
@@ -404,11 +403,9 @@ export default {
|
|||||||
...query,
|
...query,
|
||||||
label: this.label,
|
label: this.label,
|
||||||
});
|
});
|
||||||
this.showAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE'));
|
useAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(
|
useAlert(error.message || this.$t('EXPORT_CONTACTS.ERROR_MESSAGE'));
|
||||||
error.message || this.$t('EXPORT_CONTACTS.ERROR_MESSAGE')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setParamsForEditSegmentModal() {
|
setParamsForEditSegmentModal() {
|
||||||
|
|||||||
@@ -125,10 +125,9 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [adminMixin],
|
|
||||||
props: {
|
props: {
|
||||||
headerTitle: {
|
headerTitle: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -143,6 +142,12 @@ export default {
|
|||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const { isAdmin } = useAdmin();
|
||||||
|
return {
|
||||||
|
isAdmin,
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showCreateModal: false,
|
showCreateModal: false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<modal :show.sync="show" :on-close="onClose">
|
<modal :show.sync="show" :on-close="onClose">
|
||||||
<div class="h-auto overflow-auto flex flex-col">
|
<div class="flex flex-col h-auto overflow-auto">
|
||||||
<woot-modal-header :header-title="$t('IMPORT_CONTACTS.TITLE')">
|
<woot-modal-header :header-title="$t('IMPORT_CONTACTS.TITLE')">
|
||||||
<p>
|
<p>
|
||||||
{{ $t('IMPORT_CONTACTS.DESC') }}
|
{{ $t('IMPORT_CONTACTS.DESC') }}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
|
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<woot-button
|
<woot-button
|
||||||
:disabled="uiFlags.isCreating || !file"
|
:disabled="uiFlags.isCreating || !file"
|
||||||
@@ -42,16 +42,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Modal from '../../../../components/Modal.vue';
|
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||||
|
import Modal from '../../../../components/Modal.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Modal,
|
Modal,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
onClose: {
|
onClose: {
|
||||||
type: Function,
|
type: Function,
|
||||||
@@ -81,12 +80,10 @@ export default {
|
|||||||
if (!this.file) return;
|
if (!this.file) return;
|
||||||
await this.$store.dispatch('contacts/import', this.file);
|
await this.$store.dispatch('contacts/import', this.file);
|
||||||
this.onClose();
|
this.onClose();
|
||||||
this.showAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
|
useAlert(this.$t('IMPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||||
this.$track(CONTACTS_EVENTS.IMPORT_SUCCESS);
|
this.$track(CONTACTS_EVENTS.IMPORT_SUCCESS);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showAlert(
|
useAlert(error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE'));
|
||||||
error.message || this.$t('IMPORT_CONTACTS.ERROR_MESSAGE')
|
|
||||||
);
|
|
||||||
this.$track(CONTACTS_EVENTS.IMPORT_FAILURE);
|
this.$track(CONTACTS_EVENTS.IMPORT_FAILURE);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -28,10 +28,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import { dynamicTime } from 'shared/helpers/timeHelper';
|
||||||
export default {
|
export default {
|
||||||
mixins: [timeMixin],
|
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
eventType: {
|
eventType: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -53,7 +51,7 @@ export default {
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
readableTime() {
|
readableTime() {
|
||||||
return this.dynamicTime(this.timeStamp);
|
return dynamicTime(this.timeStamp);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue';
|
import AccordionItem from 'dashboard/components/Accordion/AccordionItem.vue';
|
||||||
import ContactConversations from './ContactConversations.vue';
|
import ContactConversations from './ContactConversations.vue';
|
||||||
import ConversationAction from './ConversationAction.vue';
|
import ConversationAction from './ConversationAction.vue';
|
||||||
@@ -143,7 +143,6 @@ import ContactInfo from './contact/ContactInfo.vue';
|
|||||||
import ConversationInfo from './ConversationInfo.vue';
|
import ConversationInfo from './ConversationInfo.vue';
|
||||||
import CustomAttributes from './customAttributes/CustomAttributes.vue';
|
import CustomAttributes from './customAttributes/CustomAttributes.vue';
|
||||||
import draggable from 'vuedraggable';
|
import draggable from 'vuedraggable';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
|
||||||
import MacrosList from './Macros/List.vue';
|
import MacrosList from './Macros/List.vue';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -157,7 +156,6 @@ export default {
|
|||||||
draggable,
|
draggable,
|
||||||
MacrosList,
|
MacrosList,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, uiSettingsMixin],
|
|
||||||
props: {
|
props: {
|
||||||
conversationId: {
|
conversationId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@@ -172,6 +170,21 @@ export default {
|
|||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const {
|
||||||
|
updateUISettings,
|
||||||
|
isContactSidebarItemOpen,
|
||||||
|
conversationSidebarItemsOrder,
|
||||||
|
toggleSidebarUIState,
|
||||||
|
} = useUISettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateUISettings,
|
||||||
|
isContactSidebarItemOpen,
|
||||||
|
conversationSidebarItemsOrder,
|
||||||
|
toggleSidebarUIState,
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dragEnabled: true,
|
dragEnabled: true,
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||||
import ConversationLabels from './labels/LabelBox.vue';
|
import ConversationLabels from './labels/LabelBox.vue';
|
||||||
@@ -97,7 +97,7 @@ export default {
|
|||||||
MultiselectDropdown,
|
MultiselectDropdown,
|
||||||
ConversationLabels,
|
ConversationLabels,
|
||||||
},
|
},
|
||||||
mixins: [agentMixin, alertMixin, teamMixin],
|
mixins: [agentMixin, teamMixin],
|
||||||
props: {
|
props: {
|
||||||
conversationId: {
|
conversationId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@@ -157,7 +157,7 @@ export default {
|
|||||||
agentId,
|
agentId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.showAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
|
useAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -172,7 +172,7 @@ export default {
|
|||||||
this.$store
|
this.$store
|
||||||
.dispatch('assignTeam', { conversationId, teamId })
|
.dispatch('assignTeam', { conversationId, teamId })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.showAlert(this.$t('CONVERSATION.CHANGE_TEAM'));
|
useAlert(this.$t('CONVERSATION.CHANGE_TEAM'));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -201,7 +201,7 @@ export default {
|
|||||||
newValue: priority,
|
newValue: priority,
|
||||||
from: 'Conversation Sidebar',
|
from: 'Conversation Sidebar',
|
||||||
});
|
});
|
||||||
this.showAlert(
|
useAlert(
|
||||||
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
|
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
|
||||||
priority: priorityItem.name,
|
priority: priorityItem.name,
|
||||||
conversationId,
|
conversationId,
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Spinner from 'shared/components/Spinner.vue';
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import agentMixin from 'dashboard/mixins/agentMixin';
|
import agentMixin from 'dashboard/mixins/agentMixin';
|
||||||
import ThumbnailGroup from 'dashboard/components/widgets/ThumbnailGroup.vue';
|
import ThumbnailGroup from 'dashboard/components/widgets/ThumbnailGroup.vue';
|
||||||
@@ -91,7 +91,7 @@ export default {
|
|||||||
ThumbnailGroup,
|
ThumbnailGroup,
|
||||||
MultiselectDropdownItems,
|
MultiselectDropdownItems,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, agentMixin],
|
mixins: [agentMixin],
|
||||||
props: {
|
props: {
|
||||||
conversationId: {
|
conversationId: {
|
||||||
type: [Number, String],
|
type: [Number, String],
|
||||||
@@ -197,7 +197,7 @@ export default {
|
|||||||
error?.message ||
|
error?.message ||
|
||||||
this.$t('CONVERSATION_PARTICIPANTS.API.ERROR_MESSAGE');
|
this.$t('CONVERSATION_PARTICIPANTS.API.ERROR_MESSAGE');
|
||||||
} finally {
|
} finally {
|
||||||
this.showAlert(alertMessage);
|
useAlert(alertMessage);
|
||||||
}
|
}
|
||||||
this.fetchParticipants();
|
this.fetchParticipants();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="conversation-page bg-white dark:bg-slate-900">
|
<section class="bg-white conversation-page dark:bg-slate-900">
|
||||||
<chat-list
|
<chat-list
|
||||||
:show-conversation-list="showConversationList"
|
:show-conversation-list="showConversationList"
|
||||||
:conversation-inbox="inboxId"
|
:conversation-inbox="inboxId"
|
||||||
@@ -36,13 +36,13 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||||
import { getUnixTime } from 'date-fns';
|
import { getUnixTime } from 'date-fns';
|
||||||
import ChatList from '../../../components/ChatList.vue';
|
import ChatList from '../../../components/ChatList.vue';
|
||||||
import ConversationBox from '../../../components/widgets/conversation/ConversationBox.vue';
|
import ConversationBox from '../../../components/widgets/conversation/ConversationBox.vue';
|
||||||
import PopOverSearch from './search/PopOverSearch.vue';
|
import PopOverSearch from './search/PopOverSearch.vue';
|
||||||
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
|
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
|
||||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
import { CMD_SNOOZE_CONVERSATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||||
@@ -55,7 +55,6 @@ export default {
|
|||||||
PopOverSearch,
|
PopOverSearch,
|
||||||
CustomSnoozeModal,
|
CustomSnoozeModal,
|
||||||
},
|
},
|
||||||
mixins: [uiSettingsMixin, alertMixin],
|
|
||||||
props: {
|
props: {
|
||||||
inboxId: {
|
inboxId: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
@@ -82,6 +81,14 @@ export default {
|
|||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
setup() {
|
||||||
|
const { uiSettings, updateUISettings } = useUISettings();
|
||||||
|
|
||||||
|
return {
|
||||||
|
uiSettings,
|
||||||
|
updateUISettings,
|
||||||
|
};
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showSearchModal: false,
|
showSearchModal: false,
|
||||||
@@ -235,7 +242,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$store.dispatch('setContextMenuChatId', null);
|
this.$store.dispatch('setContextMenuChatId', null);
|
||||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
hideCustomSnoozeModal() {
|
hideCustomSnoozeModal() {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user