Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff6c9dcc67 | ||
|
|
63b9b9fcc1 | ||
|
|
924d3ef2df | ||
|
|
4ef06530e5 | ||
|
|
3f10e89dba | ||
|
|
184325dcae | ||
|
|
ce2a4f5c34 | ||
|
|
7c2f4ec334 | ||
|
|
a69894e0fc | ||
|
|
6f36a02519 | ||
|
|
54e697ec51 | ||
|
|
ee044e8602 | ||
|
|
3b7421221c | ||
|
|
bc9327c09f | ||
|
|
ac71dacc61 | ||
|
|
072c607062 | ||
|
|
4b67066027 | ||
|
|
08834b6016 | ||
|
|
cbdc5b1ab0 | ||
|
|
25c6adbd07 | ||
|
|
d552b41fac | ||
|
|
a315321c4f | ||
|
|
2e768e2042 | ||
|
|
bd481ad314 | ||
|
|
b90737ee5d | ||
|
|
6e57e76358 | ||
|
|
75998945a5 | ||
|
|
cc80e4122a | ||
|
|
8aa0a347d6 | ||
|
|
2a34ceb66b | ||
|
|
db30a05b19 | ||
|
|
73fcb91ca3 | ||
|
|
bcd5633d2a | ||
|
|
1a272e7a18 | ||
|
|
7d4374a3bf | ||
|
|
2617d9b2b7 | ||
|
|
375d216d12 |
@@ -257,5 +257,3 @@ AZURE_APP_SECRET=
|
||||
# Set to true if you want to remove stale contact inboxes
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
||||
|
||||
# CAPTAIN_API_URL=http://localhost:3001/api
|
||||
|
||||
@@ -111,9 +111,9 @@ gem 'elastic-apm', require: false
|
||||
gem 'newrelic_rpm', require: false
|
||||
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
|
||||
gem 'scout_apm', require: false
|
||||
gem 'sentry-rails', '>= 5.18.2', require: false
|
||||
gem 'sentry-rails', '>= 5.18.1', require: false
|
||||
gem 'sentry-ruby', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.18.2', require: false
|
||||
gem 'sentry-sidekiq', '>= 5.18.1', require: false
|
||||
|
||||
##-- background job processing --##
|
||||
gem 'sidekiq', '>= 7.3.0'
|
||||
|
||||
+29
-70
@@ -103,8 +103,8 @@ GEM
|
||||
tzinfo (~> 2.0)
|
||||
acts-as-taggable-on (9.0.1)
|
||||
activerecord (>= 6.0, < 7.1)
|
||||
addressable (2.8.7)
|
||||
public_suffix (>= 2.0.2, < 7.0)
|
||||
addressable (2.8.4)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
administrate (0.20.1)
|
||||
actionpack (>= 6.0, < 8.0)
|
||||
actionview (>= 6.0, < 8.0)
|
||||
@@ -148,7 +148,7 @@ GEM
|
||||
barnes (0.0.9)
|
||||
multi_json (~> 1)
|
||||
statsd-ruby (~> 1.1)
|
||||
base64 (0.2.0)
|
||||
base64 (0.1.1)
|
||||
bcrypt (3.1.20)
|
||||
bigdecimal (3.1.8)
|
||||
bindex (0.8.1)
|
||||
@@ -171,8 +171,7 @@ GEM
|
||||
commonmarker (0.23.10)
|
||||
concurrent-ruby (1.3.3)
|
||||
connection_pool (2.4.1)
|
||||
crack (1.0.0)
|
||||
bigdecimal
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
csv (3.3.0)
|
||||
@@ -243,8 +242,9 @@ GEM
|
||||
railties (>= 5.0.0)
|
||||
faker (3.2.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.9.0)
|
||||
faraday-net_http (>= 2.0, < 3.2)
|
||||
faraday (2.7.4)
|
||||
faraday-net_http (>= 2.0, < 3.1)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-follow_redirects (0.3.0)
|
||||
faraday (>= 1, < 3)
|
||||
faraday-mashify (0.1.1)
|
||||
@@ -252,8 +252,7 @@ GEM
|
||||
hashie
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-net_http (3.1.0)
|
||||
net-http
|
||||
faraday-net_http (3.0.2)
|
||||
faraday-net_http_persistent (2.1.0)
|
||||
faraday (~> 2.5)
|
||||
net-http-persistent (~> 4.0)
|
||||
@@ -322,10 +321,7 @@ GEM
|
||||
google-cloud-translate-v3 (0.6.0)
|
||||
gapic-common (>= 0.17.1, < 2.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-protobuf (3.25.3)
|
||||
google-protobuf (3.25.3-arm64-darwin)
|
||||
google-protobuf (3.25.3-x86_64-darwin)
|
||||
google-protobuf (3.25.3-x86_64-linux)
|
||||
googleapis-common-protos (1.4.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (~> 1.2)
|
||||
@@ -341,16 +337,7 @@ GEM
|
||||
signet (>= 0.16, < 2.a)
|
||||
groupdate (6.2.1)
|
||||
activesupport (>= 5.2)
|
||||
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)
|
||||
grpc (1.63.0-arm64-darwin)
|
||||
google-protobuf (~> 3.25)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
@@ -359,7 +346,7 @@ GEM
|
||||
ruby2ruby (~> 2.4)
|
||||
ruby_parser (~> 3.10)
|
||||
hana (1.3.7)
|
||||
hashdiff (1.1.0)
|
||||
hashdiff (1.0.1)
|
||||
hashie (5.0.0)
|
||||
http (5.1.1)
|
||||
addressable (~> 2.8)
|
||||
@@ -374,7 +361,7 @@ GEM
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.5)
|
||||
i18n (1.14.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
@@ -398,8 +385,7 @@ GEM
|
||||
hana (~> 1.3)
|
||||
regexp_parser (~> 2.0)
|
||||
uri_template (~> 0.7)
|
||||
jwt (2.8.1)
|
||||
base64
|
||||
jwt (2.7.0)
|
||||
kaminari (1.2.2)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.2)
|
||||
@@ -423,15 +409,8 @@ GEM
|
||||
letter_opener (1.8.1)
|
||||
launchy (>= 2.2, < 3)
|
||||
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)
|
||||
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)
|
||||
liquid (5.4.0)
|
||||
listen (3.8.0)
|
||||
@@ -460,13 +439,12 @@ GEM
|
||||
meta_request (0.8.2)
|
||||
rack-contrib (>= 1.1, < 3)
|
||||
railties (>= 3.0.0, < 8)
|
||||
method_source (1.1.0)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.4.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2023.0218.1)
|
||||
mini_magick (4.12.0)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.24.1)
|
||||
mock_redis (0.36.0)
|
||||
ruby2_keywords
|
||||
@@ -476,8 +454,6 @@ GEM
|
||||
multipart-post (2.3.0)
|
||||
neighbor (0.2.3)
|
||||
activerecord (>= 5.2)
|
||||
net-http (0.4.1)
|
||||
uri
|
||||
net-http-persistent (4.0.2)
|
||||
connection_pool (~> 2.2)
|
||||
net-imap (0.4.12)
|
||||
@@ -496,15 +472,8 @@ GEM
|
||||
newrelic_rpm (9.6.0)
|
||||
base64
|
||||
nio4r (2.7.3)
|
||||
nokogiri (1.16.6)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.16.6-arm64-darwin)
|
||||
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-tty (~> 1.0, >= 1.0.1)
|
||||
snaky_hash (~> 2.0)
|
||||
@@ -551,7 +520,7 @@ GEM
|
||||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (6.0.0)
|
||||
public_suffix (5.0.1)
|
||||
puma (6.4.2)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.0)
|
||||
@@ -567,8 +536,7 @@ GEM
|
||||
rack (>= 2.0.0)
|
||||
rack-mini-profiler (3.2.0)
|
||||
rack (>= 1.2.0)
|
||||
rack-protection (3.2.0)
|
||||
base64 (>= 0.1.0)
|
||||
rack-protection (3.1.0)
|
||||
rack (~> 2.2, >= 2.2.4)
|
||||
rack-proxy (0.7.6)
|
||||
rack
|
||||
@@ -604,7 +572,7 @@ GEM
|
||||
thor (~> 1.0)
|
||||
zeitwerk (~> 2.5)
|
||||
rainbow (3.1.1)
|
||||
rake (13.2.1)
|
||||
rake (13.1.0)
|
||||
rb-fsevent (0.11.2)
|
||||
rb-inotify (0.10.1)
|
||||
ffi (~> 1.0)
|
||||
@@ -634,7 +602,7 @@ GEM
|
||||
retriable (3.1.2)
|
||||
reverse_markdown (2.1.1)
|
||||
nokogiri
|
||||
rexml (3.3.2)
|
||||
rexml (3.3.1)
|
||||
strscan
|
||||
rspec-core (3.13.0)
|
||||
rspec-support (~> 3.13.0)
|
||||
@@ -710,14 +678,14 @@ GEM
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (5.18.2)
|
||||
sentry-rails (5.18.1)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.18.2)
|
||||
sentry-ruby (5.18.2)
|
||||
sentry-ruby (~> 5.18.1)
|
||||
sentry-ruby (5.18.1)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.18.2)
|
||||
sentry-ruby (~> 5.18.2)
|
||||
sentry-sidekiq (5.18.1)
|
||||
sentry-ruby (~> 5.18.1)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.17.0)
|
||||
shoulda-matchers (5.3.0)
|
||||
@@ -769,7 +737,7 @@ GEM
|
||||
strscan (3.1.0)
|
||||
telephone_number (1.4.20)
|
||||
test-prof (1.2.1)
|
||||
thor (1.3.1)
|
||||
thor (1.3.0)
|
||||
tilt (2.3.0)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
@@ -794,12 +762,11 @@ GEM
|
||||
unf_ext (0.0.8.2)
|
||||
unicode-display_width (2.4.2)
|
||||
uniform_notifier (1.16.0)
|
||||
uri (0.13.0)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (4.0.6)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
version_gem (1.1.4)
|
||||
version_gem (1.1.3)
|
||||
warden (1.2.9)
|
||||
rack (>= 2.0.9)
|
||||
web-console (4.2.1)
|
||||
@@ -810,7 +777,7 @@ GEM
|
||||
web-push (3.0.1)
|
||||
jwt (~> 2.0)
|
||||
openssl (~> 3.0)
|
||||
webmock (3.23.1)
|
||||
webmock (3.18.1)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
@@ -830,15 +797,7 @@ GEM
|
||||
zeitwerk (2.6.16)
|
||||
|
||||
PLATFORMS
|
||||
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
|
||||
arm64-darwin-23
|
||||
|
||||
DEPENDENCIES
|
||||
active_record_query_trace
|
||||
@@ -939,9 +898,9 @@ DEPENDENCIES
|
||||
scout_apm
|
||||
scss_lint
|
||||
seed_dump
|
||||
sentry-rails (>= 5.18.2)
|
||||
sentry-rails (>= 5.18.1)
|
||||
sentry-ruby
|
||||
sentry-sidekiq (>= 5.18.2)
|
||||
sentry-sidekiq (>= 5.18.1)
|
||||
shoulda-matchers
|
||||
sidekiq (>= 7.3.0)
|
||||
sidekiq-cron (>= 1.12.0)
|
||||
@@ -971,4 +930,4 @@ RUBY VERSION
|
||||
ruby 3.3.3p89
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.16
|
||||
2.5.14
|
||||
|
||||
@@ -27,7 +27,7 @@ class Messages::Messenger::MessageBuilder
|
||||
file_type = attachment['type'].to_sym
|
||||
params = { file_type: file_type, account_id: @message.account_id }
|
||||
|
||||
if [:image, :file, :audio, :video, :share, :story_mention, :ig_reel].include? file_type
|
||||
if [:image, :file, :audio, :video, :share, :story_mention].include? file_type
|
||||
params.merge!(file_type_params(attachment))
|
||||
elsif file_type == :location
|
||||
params.merge!(location_params(attachment))
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
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
|
||||
@@ -0,0 +1,30 @@
|
||||
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
|
||||
@@ -0,0 +1,25 @@
|
||||
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,6 +3,7 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
|
||||
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
|
||||
@@ -27,6 +28,15 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
@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
|
||||
:user_id
|
||||
end
|
||||
@@ -37,12 +47,14 @@ class V2::Reports::AgentSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
|
||||
def prepare_report
|
||||
account.account_users.each_with_object([]) do |account_user, arr|
|
||||
user_id = account_user.user_id
|
||||
arr << {
|
||||
id: account_user.user_id,
|
||||
conversations_count: @grouped_conversations_count[account_user.user_id],
|
||||
avg_resolution_time: @grouped_avg_resolution_time[account_user.user_id],
|
||||
avg_first_response_time: @grouped_avg_first_response_time[account_user.user_id],
|
||||
avg_reply_time: @grouped_avg_reply_time[account_user.user_id]
|
||||
id: user_id,
|
||||
conversations_count: @grouped_conversations_count[user_id],
|
||||
resolved_conversations_count: @grouped_resolved_conversations_count[user_id],
|
||||
avg_resolution_time: @grouped_avg_resolution_time[user_id],
|
||||
avg_first_response_time: @grouped_avg_first_response_time[user_id],
|
||||
avg_reply_time: @grouped_avg_reply_time[user_id]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
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,6 +3,7 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
|
||||
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
|
||||
@@ -27,6 +28,15 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
@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 reporting_events
|
||||
@reporting_events ||= Current.account.reporting_events.where(created_at: range).joins(:conversation)
|
||||
end
|
||||
@@ -40,6 +50,7 @@ class V2::Reports::TeamSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
arr << {
|
||||
id: 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_first_response_time: @grouped_avg_first_response_time[team.id],
|
||||
avg_reply_time: @grouped_avg_reply_time[team.id]
|
||||
|
||||
@@ -10,7 +10,7 @@ class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseC
|
||||
private
|
||||
|
||||
def fetch_apps
|
||||
@apps = Integrations::App.all.select { |app| app.active?(Current.account) }
|
||||
@apps = Integrations::App.all.select(&:active?)
|
||||
end
|
||||
|
||||
def fetch_app
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
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
|
||||
before_action :check_authorization
|
||||
before_action :prepare_builder_params, only: [:agent, :team]
|
||||
before_action :prepare_builder_params, only: [:agent, :team, :inbox]
|
||||
|
||||
def agent
|
||||
render_report_with(V2::Reports::AgentSummaryBuilder)
|
||||
@@ -10,6 +10,10 @@ class Api::V2::Accounts::SummaryReportsController < Api::V1::Accounts::BaseContr
|
||||
render_report_with(V2::Reports::TeamSummaryBuilder)
|
||||
end
|
||||
|
||||
def inbox
|
||||
render_report_with(V2::Reports::InboxSummaryBuilder)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
|
||||
@@ -94,7 +94,8 @@ module Api::V2::Accounts::HeatmapHelper
|
||||
end
|
||||
|
||||
def since_timestamp(date)
|
||||
(date - 6.days).to_i.to_s
|
||||
number_of_days = params[:days_before].present? ? params[:days_before].to_i.days : 6.days
|
||||
(date - number_of_days).to_i.to_s
|
||||
end
|
||||
|
||||
def until_timestamp(date)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/* 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() {
|
||||
getConversationTrafficCSV({ daysBefore = 6 } = {}) {
|
||||
return axios.get(`${this.url}/conversation_traffic`, {
|
||||
params: { timezone_offset: getTimeOffset() },
|
||||
params: { timezone_offset: getTimeOffset(), days_before: daysBefore },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/* 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();
|
||||
@@ -3,9 +3,6 @@
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
@import 'shared/assets/fonts/plus-jakarta';
|
||||
@import 'shared/assets/fonts/InterDisplay/inter-display';
|
||||
@import 'shared/assets/fonts/inter';
|
||||
|
||||
@import 'shared/assets/stylesheets/animations';
|
||||
@import 'shared/assets/stylesheets/colors';
|
||||
@import 'shared/assets/stylesheets/spacing';
|
||||
@@ -45,7 +42,6 @@
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
// scss-lint:disable PropertySortOrder
|
||||
:root {
|
||||
--color-amber-25: 254 253 251;
|
||||
@@ -217,7 +213,6 @@
|
||||
--color-orange-800: 204 78 0;
|
||||
--color-orange-900: 88 45 29;
|
||||
}
|
||||
|
||||
// scss-lint:disable QualifyingElement
|
||||
body.dark {
|
||||
--color-amber-25: 31 19 0;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
table {
|
||||
@apply border-spacing-0 text-sm w-full;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.woot-table {
|
||||
thead {
|
||||
th {
|
||||
@apply font-semibold tracking-[1px] text-left px-2.5 uppercase text-slate-900 dark:text-slate-200;
|
||||
@@ -20,7 +16,9 @@ table {
|
||||
@apply p-2.5 text-slate-700 dark:text-slate-100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.woot-table {
|
||||
tr {
|
||||
.show-if-hover {
|
||||
transition: opacity 0.2s $swift-ease-out-function;
|
||||
|
||||
@@ -116,14 +116,13 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import VirtualList from 'vue-virtual-scroll-list';
|
||||
|
||||
import ChatListHeader from './ChatListHeader.vue';
|
||||
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
|
||||
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
||||
import ConversationItem from './ConversationItem.vue';
|
||||
import timeMixin from '../mixins/time';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import conversationMixin from '../mixins/conversations';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
@@ -132,7 +131,9 @@ import filterQueryGenerator from '../helper/filterQueryGenerator.js';
|
||||
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews.vue';
|
||||
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
||||
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
@@ -157,7 +158,14 @@ export default {
|
||||
IntersectionObserver,
|
||||
VirtualList,
|
||||
},
|
||||
mixins: [conversationMixin, keyboardEventListenerMixins, filterMixin],
|
||||
mixins: [
|
||||
timeMixin,
|
||||
conversationMixin,
|
||||
keyboardEventListenerMixins,
|
||||
alertMixin,
|
||||
filterMixin,
|
||||
uiSettingsMixin,
|
||||
],
|
||||
provide() {
|
||||
return {
|
||||
// Actions to be performed on virtual list item and context menu.
|
||||
@@ -202,13 +210,6 @@ export default {
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { uiSettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
|
||||
@@ -811,7 +812,7 @@ export default {
|
||||
});
|
||||
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
||||
if (conversationId) {
|
||||
useAlert(
|
||||
this.showAlert(
|
||||
this.$t(
|
||||
'CONVERSATION.CARD_CONTEXT_MENU.API.AGENT_ASSIGNMENT.SUCCESFUL',
|
||||
{
|
||||
@@ -821,10 +822,10 @@ export default {
|
||||
)
|
||||
);
|
||||
} else {
|
||||
useAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
|
||||
this.showAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
|
||||
}
|
||||
} catch (err) {
|
||||
useAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
|
||||
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
|
||||
}
|
||||
},
|
||||
async assignPriority(priority, conversationId = null) {
|
||||
@@ -839,7 +840,7 @@ export default {
|
||||
newValue: priority,
|
||||
from: 'Context menu',
|
||||
});
|
||||
useAlert(
|
||||
this.showAlert(
|
||||
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
|
||||
priority,
|
||||
conversationId,
|
||||
@@ -882,7 +883,7 @@ export default {
|
||||
conversationId,
|
||||
teamId: team.id,
|
||||
});
|
||||
useAlert(
|
||||
this.showAlert(
|
||||
this.$t(
|
||||
'CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.SUCCESFUL',
|
||||
{
|
||||
@@ -892,7 +893,7 @@ export default {
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
this.showAlert(
|
||||
this.$t('CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.FAILED')
|
||||
);
|
||||
}
|
||||
@@ -909,7 +910,7 @@ export default {
|
||||
});
|
||||
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
||||
if (conversationId) {
|
||||
useAlert(
|
||||
this.showAlert(
|
||||
this.$t(
|
||||
'CONVERSATION.CARD_CONTEXT_MENU.API.LABEL_ASSIGNMENT.SUCCESFUL',
|
||||
{
|
||||
@@ -919,10 +920,10 @@ export default {
|
||||
)
|
||||
);
|
||||
} else {
|
||||
useAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
|
||||
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
|
||||
}
|
||||
} catch (err) {
|
||||
useAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
|
||||
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
|
||||
}
|
||||
},
|
||||
async onAssignTeamsForBulk(team) {
|
||||
@@ -935,9 +936,9 @@ export default {
|
||||
},
|
||||
});
|
||||
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
||||
useAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
|
||||
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
|
||||
} catch (err) {
|
||||
useAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
|
||||
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
|
||||
}
|
||||
},
|
||||
async onUpdateConversations(status, snoozedUntil) {
|
||||
@@ -951,9 +952,9 @@ export default {
|
||||
snoozed_until: snoozedUntil,
|
||||
});
|
||||
this.$store.dispatch('bulkActions/clearSelectedConversationIds');
|
||||
useAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
|
||||
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
|
||||
} catch (err) {
|
||||
useAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
|
||||
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
|
||||
}
|
||||
},
|
||||
toggleConversationStatus(conversationId, status, snoozedUntil) {
|
||||
@@ -964,7 +965,7 @@ export default {
|
||||
snoozedUntil,
|
||||
})
|
||||
.then(() => {
|
||||
useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -25,9 +25,10 @@
|
||||
<script>
|
||||
import 'highlight.js/styles/default.css';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
script: {
|
||||
type: String,
|
||||
@@ -60,7 +61,7 @@ export default {
|
||||
async onCopy(e) {
|
||||
e.preventDefault();
|
||||
await copyTextToClipboard(this.script);
|
||||
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<script setup>
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { computed } from 'vue';
|
||||
const props = defineProps({
|
||||
showOnCustomBrandedInstance: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const getters = useStoreGetters();
|
||||
|
||||
const isACustomBrandedInstance =
|
||||
getters['globalConfig/isACustomBrandedInstance'];
|
||||
|
||||
const shouldShowContent = computed(
|
||||
() => props.showOnCustomBrandedInstance || !isACustomBrandedInstance.value
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="shouldShowContent">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,9 +18,10 @@
|
||||
<script>
|
||||
import 'highlight.js/styles/default.css';
|
||||
import { copyTextToClipboard } from 'shared/helpers/clipboard';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
value: {
|
||||
type: String,
|
||||
@@ -36,7 +37,7 @@ export default {
|
||||
async onCopy(e) {
|
||||
e.preventDefault();
|
||||
await copyTextToClipboard(this.value);
|
||||
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||
this.showAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
||||
},
|
||||
toggleMasked() {
|
||||
this.masked = !this.masked;
|
||||
|
||||
@@ -15,11 +15,13 @@
|
||||
|
||||
<script>
|
||||
import WootSnackbar from './Snackbar.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootSnackbar,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
duration: {
|
||||
type: Number,
|
||||
@@ -40,7 +42,7 @@ export default {
|
||||
this.$emitter.off('newToastMessage', this.onNewToastMessage);
|
||||
},
|
||||
methods: {
|
||||
onNewToastMessage({ message, action }) {
|
||||
onNewToastMessage(message, action) {
|
||||
this.snackMessages.push({
|
||||
key: new Date().getTime(),
|
||||
message,
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||
import accountMixin from 'dashboard/mixins/account';
|
||||
|
||||
const EMPTY_SUBSCRIPTION_INFO = {
|
||||
@@ -22,13 +22,7 @@ const EMPTY_SUBSCRIPTION_INFO = {
|
||||
|
||||
export default {
|
||||
components: { Banner },
|
||||
mixins: [accountMixin],
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
mixins: [adminMixin, accountMixin],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
isOnChatwootCloud: 'globalConfig/isOnChatwootCloud',
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import accountMixin from 'dashboard/mixins/account';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: { Banner },
|
||||
mixins: [accountMixin],
|
||||
mixins: [accountMixin, alertMixin],
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
@@ -36,7 +36,7 @@ export default {
|
||||
methods: {
|
||||
resendVerificationEmail() {
|
||||
this.$store.dispatch('resendConfirmation');
|
||||
useAlert(this.$t('APP_GLOBAL.EMAIL_VERIFICATION_SENT'));
|
||||
this.showAlert(this.$t('APP_GLOBAL.EMAIL_VERIFICATION_SENT'));
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -14,20 +14,15 @@ import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||
import { hasAnUpdateAvailable } from './versionCheckHelper';
|
||||
|
||||
export default {
|
||||
components: { Banner },
|
||||
mixins: [adminMixin],
|
||||
props: {
|
||||
latestChatwootVersion: { type: String, default: '' },
|
||||
},
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return { userDismissedBanner: false };
|
||||
},
|
||||
|
||||
@@ -12,12 +12,13 @@
|
||||
<script>
|
||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||
import accountMixin from 'dashboard/mixins/account';
|
||||
import { differenceInDays } from 'date-fns';
|
||||
|
||||
export default {
|
||||
components: { Banner },
|
||||
mixins: [accountMixin],
|
||||
mixins: [adminMixin, accountMixin],
|
||||
data() {
|
||||
return { conversationMeta: {} };
|
||||
},
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
@@ -94,7 +94,7 @@ export default {
|
||||
WootDropdownItem,
|
||||
WootDropdownMenu,
|
||||
},
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
mixins: [alertMixin, keyboardEventListenerMixins],
|
||||
props: { conversationId: { type: [String, Number], required: true } },
|
||||
data() {
|
||||
return {
|
||||
@@ -209,7 +209,7 @@ export default {
|
||||
snoozedUntil,
|
||||
})
|
||||
.then(() => {
|
||||
useAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_STATUS'));
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</woot-button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-divider />
|
||||
<woot-dropdown-item class="flex items-center justify-between p-2 m-0">
|
||||
<woot-dropdown-item class="m-0 flex items-center justify-between p-2">
|
||||
<div class="flex items-center">
|
||||
<fluent-icon
|
||||
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
|
||||
@@ -28,7 +28,7 @@
|
||||
/>
|
||||
|
||||
<span
|
||||
class="mx-1 my-0 text-xs font-medium text-slate-600 dark:text-slate-100"
|
||||
class="my-0 mx-1 text-xs font-medium text-slate-600 dark:text-slate-100"
|
||||
>
|
||||
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
|
||||
</span>
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<woot-switch
|
||||
size="small"
|
||||
class="mx-1 mt-px mb-0"
|
||||
class="mt-px mx-1 mb-0"
|
||||
:value="currentUserAutoOffline"
|
||||
@input="updateAutoOffline"
|
||||
/>
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader.vue';
|
||||
@@ -65,6 +65,9 @@ export default {
|
||||
WootDropdownItem,
|
||||
AvailabilityStatusBadge,
|
||||
},
|
||||
|
||||
mixins: [alertMixin],
|
||||
|
||||
data() {
|
||||
return {
|
||||
isStatusMenuOpened: false,
|
||||
@@ -126,7 +129,7 @@ export default {
|
||||
account_id: this.currentAccountId,
|
||||
});
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
this.showAlert(
|
||||
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -30,7 +30,9 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import adminMixin from '../../mixins/isAdmin';
|
||||
import { getSidebarItems } from './config/default-sidebar';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
import PrimarySidebar from './sidebarComponents/Primary.vue';
|
||||
import SecondarySidebar from './sidebarComponents/Secondary.vue';
|
||||
@@ -43,7 +45,7 @@ export default {
|
||||
PrimarySidebar,
|
||||
SecondarySidebar,
|
||||
},
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
mixins: [adminMixin, alertMixin, keyboardEventListenerMixins],
|
||||
props: {
|
||||
showSecondarySidebar: {
|
||||
type: Boolean,
|
||||
|
||||
@@ -163,6 +163,17 @@ const settings = accountId => ({
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||
toStateName: 'settings_integrations',
|
||||
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||
},
|
||||
{
|
||||
icon: 'star-emphasis',
|
||||
label: 'APPLICATIONS',
|
||||
hasSubMenu: false,
|
||||
meta: {
|
||||
permissions: ['administrator'],
|
||||
},
|
||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||
toStateName: 'settings_applications',
|
||||
featureFlag: FEATURE_FLAGS.INTEGRATIONS,
|
||||
},
|
||||
|
||||
@@ -51,9 +51,10 @@
|
||||
<script>
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
@@ -87,13 +88,13 @@ export default {
|
||||
account_name: this.accountName,
|
||||
});
|
||||
this.$emit('close-account-create-modal');
|
||||
useAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
|
||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
|
||||
window.location = `/app/accounts/${account_id}/dashboard`;
|
||||
} catch (error) {
|
||||
if (error.response.status === 422) {
|
||||
useAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
|
||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
|
||||
} else {
|
||||
useAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
|
||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -94,7 +94,8 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
|
||||
import adminMixin from '../../../mixins/isAdmin';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import {
|
||||
getInboxClassByType,
|
||||
@@ -110,19 +111,13 @@ import Policy from '../../policy.vue';
|
||||
|
||||
export default {
|
||||
components: { SecondaryChildNavItem, Policy },
|
||||
mixins: [configMixin],
|
||||
mixins: [adminMixin, configMixin],
|
||||
props: {
|
||||
menuItem: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
activeInbox: 'getSelectedInbox',
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
delay: { show: 1500, hide: 0 },
|
||||
hideOnClick: true,
|
||||
}"
|
||||
class="ml-auto leading-4 text-xxs text-slate-500 dark:text-slate-500 hover:text-slate-900 dark:hover:text-slate-100"
|
||||
class="text-xxs text-slate-500 dark:text-slate-500 leading-4 ml-auto hover:text-slate-900 dark:hover:text-slate-100"
|
||||
>
|
||||
<span>{{ `${createdAtTime} • ${lastActivityTime}` }}</span>
|
||||
</div>
|
||||
@@ -16,14 +16,11 @@ const MINUTE_IN_MILLI_SECONDS = 60000;
|
||||
const HOUR_IN_MILLI_SECONDS = MINUTE_IN_MILLI_SECONDS * 60;
|
||||
const DAY_IN_MILLI_SECONDS = HOUR_IN_MILLI_SECONDS * 24;
|
||||
|
||||
import {
|
||||
dynamicTime,
|
||||
dateFormat,
|
||||
shortTimestamp,
|
||||
} from 'shared/helpers/timeHelper';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
|
||||
export default {
|
||||
name: 'TimeAgo',
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
isAutoRefreshEnabled: {
|
||||
type: Boolean,
|
||||
@@ -40,17 +37,17 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lastActivityAtTimeAgo: dynamicTime(this.lastActivityTimestamp),
|
||||
createdAtTimeAgo: dynamicTime(this.createdAtTimestamp),
|
||||
lastActivityAtTimeAgo: this.dynamicTime(this.lastActivityTimestamp),
|
||||
createdAtTimeAgo: this.dynamicTime(this.createdAtTimestamp),
|
||||
timer: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
lastActivityTime() {
|
||||
return shortTimestamp(this.lastActivityAtTimeAgo);
|
||||
return this.shortTimestamp(this.lastActivityAtTimeAgo);
|
||||
},
|
||||
createdAtTime() {
|
||||
return shortTimestamp(this.createdAtTimeAgo);
|
||||
return this.shortTimestamp(this.createdAtTimeAgo);
|
||||
},
|
||||
createdAt() {
|
||||
const createdTimeDiff = Date.now() - this.createdAtTimestamp * 1000;
|
||||
@@ -59,9 +56,9 @@ export default {
|
||||
? `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.LATEST')} ${
|
||||
this.createdAtTimeAgo
|
||||
}`
|
||||
: `${this.$t('CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST')} ${dateFormat(
|
||||
this.createdAtTimestamp
|
||||
)}`;
|
||||
: `${this.$t(
|
||||
'CHAT_LIST.CHAT_TIME_STAMP.CREATED.OLDEST'
|
||||
)} ${this.dateFormat(this.createdAtTimestamp)}`;
|
||||
},
|
||||
lastActivity() {
|
||||
const lastActivityTimeDiff =
|
||||
@@ -73,7 +70,7 @@ export default {
|
||||
}`
|
||||
: `${this.$t(
|
||||
'CHAT_LIST.CHAT_TIME_STAMP.LAST_ACTIVITY.NOT_ACTIVE'
|
||||
)} ${dateFormat(this.lastActivityTimestamp)}`;
|
||||
)} ${this.dateFormat(this.lastActivityTimestamp)}`;
|
||||
},
|
||||
tooltipText() {
|
||||
return `${this.createdAt}
|
||||
@@ -82,10 +79,10 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
lastActivityTimestamp() {
|
||||
this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
|
||||
this.lastActivityAtTimeAgo = this.dynamicTime(this.lastActivityTimestamp);
|
||||
},
|
||||
createdAtTimestamp() {
|
||||
this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
|
||||
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -99,8 +96,10 @@ export default {
|
||||
methods: {
|
||||
createTimer() {
|
||||
this.timer = setTimeout(() => {
|
||||
this.lastActivityAtTimeAgo = dynamicTime(this.lastActivityTimestamp);
|
||||
this.createdAtTimeAgo = dynamicTime(this.createdAtTimestamp);
|
||||
this.lastActivityAtTimeAgo = this.dynamicTime(
|
||||
this.lastActivityTimestamp
|
||||
);
|
||||
this.createdAtTimeAgo = this.dynamicTime(this.createdAtTimestamp);
|
||||
this.createTimer();
|
||||
}, this.refreshTime());
|
||||
},
|
||||
|
||||
@@ -35,13 +35,13 @@
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import AICTAModal from './AICTAModal.vue';
|
||||
import AIAssistanceModal from './AIAssistanceModal.vue';
|
||||
import aiMixin from 'dashboard/mixins/aiMixin';
|
||||
import adminMixin from 'dashboard/mixins/aiMixin';
|
||||
import aiMixin from 'dashboard/mixins/isAdmin';
|
||||
import { CMD_AI_ASSIST } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import AIAssistanceCTAButton from './AIAssistanceCTAButton.vue';
|
||||
|
||||
export default {
|
||||
@@ -50,17 +50,7 @@ export default {
|
||||
AICTAModal,
|
||||
AIAssistanceCTAButton,
|
||||
},
|
||||
mixins: [aiMixin, keyboardEventListenerMixins],
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const { isAdmin } = useAdmin();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
mixins: [aiMixin, keyboardEventListenerMixins, adminMixin, uiSettingsMixin],
|
||||
data: () => ({
|
||||
showAIAssistanceModal: false,
|
||||
showAICtaModal: false,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="flex-1 min-w-0 px-0">
|
||||
<div class="px-0 min-w-0 flex-1">
|
||||
<woot-modal-header
|
||||
:header-title="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.TITLE')"
|
||||
:header-content="$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DESC')"
|
||||
/>
|
||||
<form
|
||||
class="flex flex-col flex-wrap modal-content"
|
||||
class="flex flex-wrap flex-col modal-content"
|
||||
@submit.prevent="finishOpenAI"
|
||||
>
|
||||
<div class="w-full mt-2">
|
||||
<div class="mt-2 w-full">
|
||||
<woot-input
|
||||
v-model="value"
|
||||
type="text"
|
||||
@@ -19,7 +19,7 @@
|
||||
@blur="$v.value.$touch"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between w-full gap-2 px-0 py-2">
|
||||
<div class="flex flex-row justify-between gap-2 py-2 px-0 w-full">
|
||||
<woot-button variant="link" @click.prevent="openOpenAIDoc">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.BUTTONS.NEED_HELP') }}
|
||||
</woot-button>
|
||||
@@ -39,20 +39,13 @@
|
||||
<script>
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
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';
|
||||
|
||||
export default {
|
||||
mixins: [aiMixin],
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
mixins: [aiMixin, alertMixin, uiSettingsMixin],
|
||||
data() {
|
||||
return {
|
||||
value: '',
|
||||
@@ -74,7 +67,7 @@ export default {
|
||||
},
|
||||
|
||||
onDismiss() {
|
||||
useAlert(
|
||||
this.showAlert(
|
||||
this.$t('INTEGRATION_SETTINGS.OPEN_AI.CTA_MODAL.DISMISS_MESSAGE')
|
||||
);
|
||||
this.updateUISettings({
|
||||
@@ -104,7 +97,7 @@ export default {
|
||||
this.alertMessage =
|
||||
errorMessage || this.$t('INTEGRATION_APPS.ADD.API.ERROR_MESSAGE');
|
||||
} finally {
|
||||
useAlert(this.alertMessage);
|
||||
this.showAlert(this.alertMessage);
|
||||
}
|
||||
},
|
||||
openOpenAIDoc() {
|
||||
|
||||
@@ -26,12 +26,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
export default {
|
||||
components: {
|
||||
Spinner,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
@@ -70,7 +71,7 @@ export default {
|
||||
} catch (error) {
|
||||
this.uploadState = 'failed';
|
||||
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOAD_FAILED');
|
||||
useAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
|
||||
this.showAlert(this.$t('AUTOMATION.ATTACHMENT.UPLOAD_ERROR'));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,40 +1,34 @@
|
||||
<script setup>
|
||||
import router from '../../routes/index';
|
||||
const props = defineProps({
|
||||
backUrl: {
|
||||
type: [String, Object],
|
||||
default: '',
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const goBack = () => {
|
||||
if (props.backUrl !== '') {
|
||||
router.push(props.backUrl);
|
||||
} else {
|
||||
router.go(-1);
|
||||
}
|
||||
};
|
||||
|
||||
const buttonStyleClass = props.compact
|
||||
? 'text-sm text-slate-600 dark:text-slate-300'
|
||||
: 'text-base text-woot-500 dark:text-woot-500';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="flex items-center font-normal p-0 cursor-pointer"
|
||||
:class="buttonStyleClass"
|
||||
class="header-section flex items-center text-base font-normal mr-4 ml-2 p-0 cursor-pointer text-woot-500 dark:text-woot-500"
|
||||
@click.capture="goBack"
|
||||
>
|
||||
<fluent-icon icon="chevron-left" class="-ml-1" />
|
||||
<fluent-icon icon="chevron-left" />
|
||||
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
|
||||
</button>
|
||||
</template>
|
||||
<script>
|
||||
import router from '../../routes/index';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
backUrl: {
|
||||
type: [String, Object],
|
||||
default: '',
|
||||
},
|
||||
buttonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
goBack() {
|
||||
if (this.backUrl !== '') {
|
||||
router.push(this.backUrl);
|
||||
} else {
|
||||
router.go(-1);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
LabelDropdown,
|
||||
},
|
||||
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
mixins: [adminMixin, keyboardEventListenerMixins],
|
||||
|
||||
props: {
|
||||
allLabels: {
|
||||
@@ -54,13 +54,6 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
showSearchDropdownLabel: false,
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import DyteAPI from 'dashboard/api/integrations/dyte';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: Number,
|
||||
@@ -46,7 +47,7 @@ export default {
|
||||
try {
|
||||
await DyteAPI.createAMeeting(this.conversationId);
|
||||
} catch (error) {
|
||||
useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR'));
|
||||
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.CREATE_ERROR'));
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import 'videojs-record/dist/css/videojs.record.css';
|
||||
|
||||
import videojs from 'video.js';
|
||||
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import alertMixin from '../../../../shared/mixins/alertMixin';
|
||||
|
||||
import Recorder from 'opus-recorder';
|
||||
|
||||
@@ -50,6 +50,7 @@ const RECORDER_CONFIG = {
|
||||
|
||||
export default {
|
||||
name: 'WootAudioRecorder',
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
audioRecordFormat: {
|
||||
type: String,
|
||||
@@ -187,10 +188,14 @@ export default {
|
||||
deviceErrorName?.includes('notallowederror') ||
|
||||
deviceErrorName?.includes('permissiondeniederror')
|
||||
) {
|
||||
useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION'));
|
||||
this.showAlert(
|
||||
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_PERMISSION')
|
||||
);
|
||||
this.fireStateRecorderChanged('notallowederror');
|
||||
} else {
|
||||
useAlert(this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR'));
|
||||
this.showAlert(
|
||||
this.$t('CONVERSATION.REPLYBOX.TIP_AUDIORECORDER_ERROR')
|
||||
);
|
||||
}
|
||||
},
|
||||
formatTimeProgress() {
|
||||
|
||||
@@ -80,7 +80,8 @@ import {
|
||||
hasPressedCommandAndEnter,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||
import {
|
||||
replaceVariablesInMessage,
|
||||
createTypingIndicator,
|
||||
@@ -88,7 +89,7 @@ import {
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import {
|
||||
MESSAGE_EDITOR_MENU_OPTIONS,
|
||||
MESSAGE_EDITOR_IMAGE_RESIZES,
|
||||
@@ -118,7 +119,7 @@ const createState = (
|
||||
export default {
|
||||
name: 'WootMessageEditor',
|
||||
components: { TagAgents, CannedResponse, VariableList },
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
editorId: { type: String, default: '' },
|
||||
@@ -138,19 +139,6 @@ export default {
|
||||
channelType: { type: String, default: '' },
|
||||
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() {
|
||||
return {
|
||||
typingIndicator: createTypingIndicator(
|
||||
@@ -290,7 +278,7 @@ export default {
|
||||
// this is considered the source of truth, we watch this property
|
||||
// on change, we toggle the signature in the editor
|
||||
if (this.allowSignature && !this.isPrivate && this.channelType) {
|
||||
return this.fetchSignatureFlagFromUISettings(this.channelType);
|
||||
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -533,10 +521,10 @@ export default {
|
||||
}
|
||||
},
|
||||
isEnterToSendEnabled() {
|
||||
return this.isEditorHotKeyEnabled('enter');
|
||||
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
|
||||
},
|
||||
isCmdPlusEnterToSendEnabled() {
|
||||
return this.isEditorHotKeyEnabled('cmd_enter');
|
||||
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter');
|
||||
},
|
||||
getKeyboardEvents() {
|
||||
return {
|
||||
@@ -623,7 +611,7 @@ export default {
|
||||
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
||||
this.uploadImageToStorage(file);
|
||||
} else {
|
||||
useAlert(
|
||||
this.showAlert(
|
||||
this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR',
|
||||
{
|
||||
@@ -641,13 +629,13 @@ export default {
|
||||
if (fileUrl) {
|
||||
this.onImageInsertInEditor(fileUrl);
|
||||
}
|
||||
useAlert(
|
||||
this.showAlert(
|
||||
this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SUCCESS'
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(
|
||||
this.showAlert(
|
||||
this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_ERROR'
|
||||
)
|
||||
|
||||
@@ -24,9 +24,9 @@ import {
|
||||
Selection,
|
||||
} from '@chatwoot/prosemirror-schema';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
|
||||
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||
const createState = (
|
||||
@@ -51,21 +51,13 @@ const createState = (
|
||||
};
|
||||
|
||||
export default {
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins, uiSettingsMixin, alertMixin],
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
editorId: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
enabledMenuOptions: { type: Array, default: () => [] },
|
||||
},
|
||||
setup() {
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editorView: null,
|
||||
@@ -116,7 +108,7 @@ export default {
|
||||
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
||||
this.uploadImageToStorage(file);
|
||||
} else {
|
||||
useAlert(
|
||||
this.showAlert(
|
||||
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR_FILE_SIZE', {
|
||||
size: MAXIMUM_FILE_UPLOAD_SIZE,
|
||||
})
|
||||
@@ -135,9 +127,13 @@ export default {
|
||||
if (fileUrl) {
|
||||
this.onImageUploadStart(fileUrl);
|
||||
}
|
||||
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS'));
|
||||
this.showAlert(
|
||||
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.SUCCESS')
|
||||
);
|
||||
} catch (error) {
|
||||
useAlert(this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR'));
|
||||
this.showAlert(
|
||||
this.$t('HELP_CENTER.ARTICLE_EDITOR.IMAGE_UPLOAD.ERROR')
|
||||
);
|
||||
}
|
||||
},
|
||||
onImageUploadStart(fileUrl) {
|
||||
|
||||
@@ -134,10 +134,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import FileUpload from 'vue-upload-component';
|
||||
import * as ActiveStorage from 'activestorage';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import {
|
||||
@@ -153,7 +153,7 @@ import { mapGetters } from 'vuex';
|
||||
export default {
|
||||
name: 'ReplyBottomPanel',
|
||||
components: { FileUpload, VideoCallButton, AIAssistanceButton },
|
||||
mixins: [keyboardEventListenerMixins, inboxMixin],
|
||||
mixins: [keyboardEventListenerMixins, uiSettingsMixin, inboxMixin],
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
@@ -248,15 +248,6 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { setSignatureFlagForInbox, fetchSignatureFlagFromUISettings } =
|
||||
useUISettings();
|
||||
|
||||
return {
|
||||
setSignatureFlagForInbox,
|
||||
fetchSignatureFlagFromUISettings,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
@@ -329,7 +320,7 @@ export default {
|
||||
},
|
||||
sendWithSignature() {
|
||||
// channelType is sourced from inboxMixin
|
||||
return this.fetchSignatureFlagFromUISettings(this.channelType);
|
||||
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
||||
},
|
||||
signatureToggleTooltip() {
|
||||
return this.sendWithSignature
|
||||
|
||||
+3
-3
@@ -79,7 +79,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required, requiredIf } from 'vuelidate/lib/validators';
|
||||
import FilterInputBox from '../FilterInput/Index.vue';
|
||||
import languages from './advancedFilterItems/languages';
|
||||
@@ -94,7 +94,7 @@ export default {
|
||||
components: {
|
||||
FilterInputBox,
|
||||
},
|
||||
mixins: [filterMixin],
|
||||
mixins: [alertMixin, filterMixin],
|
||||
props: {
|
||||
onClose: {
|
||||
type: Function,
|
||||
@@ -341,7 +341,7 @@ export default {
|
||||
},
|
||||
removeFilter(index) {
|
||||
if (this.appliedFilters.length <= 1) {
|
||||
useAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
|
||||
this.showAlert(this.$t('FILTER.FILTER_DELETE_ERROR'));
|
||||
} else {
|
||||
this.appliedFilters.splice(index, 1);
|
||||
}
|
||||
|
||||
+7
-13
@@ -12,10 +12,10 @@
|
||||
<div
|
||||
v-if="showActionsDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="right-0 mt-1 dropdown-pane dropdown-pane--open basic-filter"
|
||||
class="dropdown-pane dropdown-pane--open mt-1 right-0 basic-filter"
|
||||
>
|
||||
<div class="flex items-center justify-between last:mt-4">
|
||||
<span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
|
||||
<div class="items-center flex justify-between last:mt-4">
|
||||
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
|
||||
$t('CHAT_LIST.CHAT_SORT.STATUS')
|
||||
}}</span>
|
||||
<filter-item
|
||||
@@ -26,8 +26,8 @@
|
||||
@onChangeFilter="onChangeFilter"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between last:mt-4">
|
||||
<span class="text-xs font-medium text-slate-800 dark:text-slate-100">{{
|
||||
<div class="items-center flex justify-between last:mt-4">
|
||||
<span class="text-slate-800 dark:text-slate-100 text-xs font-medium">{{
|
||||
$t('CHAT_LIST.CHAT_SORT.ORDER_BY')
|
||||
}}</span>
|
||||
<filter-item
|
||||
@@ -46,19 +46,13 @@
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { mapGetters } from 'vuex';
|
||||
import FilterItem from './FilterItem.vue';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FilterItem,
|
||||
},
|
||||
setup() {
|
||||
const { updateUISettings } = useUISettings();
|
||||
|
||||
return {
|
||||
updateUISettings,
|
||||
};
|
||||
},
|
||||
mixins: [uiSettingsMixin],
|
||||
data() {
|
||||
return {
|
||||
showActionsDropdown: false,
|
||||
|
||||
@@ -120,11 +120,13 @@ import { mapGetters } from 'vuex';
|
||||
import Thumbnail from '../Thumbnail.vue';
|
||||
import MessagePreview from './MessagePreview.vue';
|
||||
import conversationMixin from '../../../mixins/conversations';
|
||||
import timeMixin from '../../../mixins/time';
|
||||
import router from '../../../routes';
|
||||
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
|
||||
import InboxName from '../InboxName.vue';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import ConversationContextMenu from './contextMenu/Index.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
|
||||
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
||||
import PriorityMark from './PriorityMark.vue';
|
||||
@@ -141,7 +143,8 @@ export default {
|
||||
PriorityMark,
|
||||
SLACardLabel,
|
||||
},
|
||||
mixins: [inboxMixin, conversationMixin],
|
||||
|
||||
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
|
||||
props: {
|
||||
activeLabel: {
|
||||
type: String,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<woot-modal :show.sync="show" :on-close="onCancel">
|
||||
<div class="flex flex-col h-auto overflow-auto">
|
||||
<div class="h-auto overflow-auto flex flex-col">
|
||||
<woot-modal-header
|
||||
:header-title="$t('EMAIL_TRANSCRIPT.TITLE')"
|
||||
:header-content="$t('EMAIL_TRANSCRIPT.DESC')"
|
||||
@@ -61,7 +61,7 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-end w-full gap-2 px-0 py-2">
|
||||
<div class="flex flex-row justify-end gap-2 py-2 px-0 w-full">
|
||||
<woot-submit-button
|
||||
:button-text="$t('EMAIL_TRANSCRIPT.SUBMIT')"
|
||||
:disabled="!isFormValid"
|
||||
@@ -77,8 +77,9 @@
|
||||
|
||||
<script>
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
@@ -141,10 +142,10 @@ export default {
|
||||
email: this.selectedEmailAddress,
|
||||
conversationId: this.currentChat.id,
|
||||
});
|
||||
useAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'));
|
||||
this.showAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'));
|
||||
this.onCancel();
|
||||
} catch (error) {
|
||||
useAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'));
|
||||
this.showAlert(this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'));
|
||||
} finally {
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAdmin } from 'dashboard/composables/useAdmin';
|
||||
import adminMixin from 'dashboard/mixins/isAdmin';
|
||||
import accountMixin from 'dashboard/mixins/account';
|
||||
import OnboardingView from '../OnboardingView.vue';
|
||||
import EmptyStateMessage from './EmptyStateMessage.vue';
|
||||
@@ -45,19 +45,13 @@ export default {
|
||||
OnboardingView,
|
||||
EmptyStateMessage,
|
||||
},
|
||||
mixins: [accountMixin],
|
||||
mixins: [accountMixin, adminMixin],
|
||||
props: {
|
||||
isOnExpandedLayout: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { isAdmin } = useAdmin();
|
||||
return {
|
||||
isAdmin,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="shouldShowContextMenu"
|
||||
class="invisible context-menu-wrap group-hover:visible"
|
||||
class="context-menu-wrap invisible group-hover:visible"
|
||||
>
|
||||
<context-menu
|
||||
v-if="isBubble && !isMessageDeleted"
|
||||
@@ -157,7 +157,8 @@ import ContextMenu from 'dashboard/modules/conversations/components/MessageConte
|
||||
import InstagramStory from './bubble/InstagramStory.vue';
|
||||
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import { CONTENT_TYPES } from 'shared/constants/contentType';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
@@ -165,7 +166,6 @@ import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
|
||||
import { LocalStorage } from 'shared/helpers/localStorage';
|
||||
import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -183,7 +183,7 @@ export default {
|
||||
InstagramStoryReply,
|
||||
Spinner,
|
||||
},
|
||||
mixins: [messageFormatterMixin],
|
||||
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
@@ -481,9 +481,6 @@ export default {
|
||||
}
|
||||
return '';
|
||||
},
|
||||
isEmailContentType() {
|
||||
return this.contentType === CONTENT_TYPES.INCOMING_EMAIL;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
data() {
|
||||
@@ -501,35 +498,19 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
isAttachmentImageVideoAudio(fileType) {
|
||||
return ['image', 'audio', 'video', 'story_mention', 'ig_reel'].includes(
|
||||
fileType
|
||||
);
|
||||
return ['image', 'audio', 'video', 'story_mention'].includes(fileType);
|
||||
},
|
||||
hasMediaAttachment(type) {
|
||||
if (this.hasAttachments && this.data.attachments.length > 0) {
|
||||
return this.compareMessageFileType(this.data, type);
|
||||
const { attachments = [{}] } = this.data;
|
||||
const { file_type: fileType } = attachments[0];
|
||||
return fileType === type && !this.hasMediaLoadError;
|
||||
}
|
||||
if (this.storyReply) {
|
||||
return true;
|
||||
}
|
||||
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() {
|
||||
this.showContextMenu = !this.showContextMenu;
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="relative flex items-center gap-2 actions--container">
|
||||
<div class="flex actions--container relative items-center gap-2">
|
||||
<woot-button
|
||||
v-if="!currentChat.muted"
|
||||
v-tooltip="$t('CONTACT_PANEL.MUTE_CONTACT')"
|
||||
@@ -37,7 +37,7 @@
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import EmailTranscriptModal from './EmailTranscriptModal.vue';
|
||||
import ResolveAction from '../../buttons/ResolveAction.vue';
|
||||
import {
|
||||
@@ -51,6 +51,7 @@ export default {
|
||||
EmailTranscriptModal,
|
||||
ResolveAction,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
showEmailActionsModal: false,
|
||||
@@ -72,11 +73,11 @@ export default {
|
||||
methods: {
|
||||
mute() {
|
||||
this.$store.dispatch('muteConversation', this.currentChat.id);
|
||||
useAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS'));
|
||||
this.showAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS'));
|
||||
},
|
||||
unmute() {
|
||||
this.$store.dispatch('unmuteConversation', this.currentChat.id);
|
||||
useAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS'));
|
||||
this.showAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS'));
|
||||
},
|
||||
toggleEmailActionsModal() {
|
||||
this.showEmailActionsModal = !this.showEmailActionsModal;
|
||||
|
||||
@@ -153,8 +153,7 @@
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
|
||||
import CannedResponse from './CannedResponse.vue';
|
||||
@@ -181,8 +180,10 @@ import {
|
||||
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { trimContent, debounce } from '@chatwoot/utils';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import fileUploadMixin from 'dashboard/mixins/fileUploadMixin';
|
||||
@@ -217,6 +218,8 @@ export default {
|
||||
},
|
||||
mixins: [
|
||||
inboxMixin,
|
||||
uiSettingsMixin,
|
||||
alertMixin,
|
||||
messageFormatterMixin,
|
||||
rtlMixin,
|
||||
fileUploadMixin,
|
||||
@@ -228,21 +231,6 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
isEditorHotKeyEnabled,
|
||||
fetchSignatureFlagFromUISettings,
|
||||
} = useUISettings();
|
||||
|
||||
return {
|
||||
uiSettings,
|
||||
updateUISettings,
|
||||
isEditorHotKeyEnabled,
|
||||
fetchSignatureFlagFromUISettings,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
message: '',
|
||||
@@ -322,7 +310,7 @@ export default {
|
||||
agentId,
|
||||
})
|
||||
.then(() => {
|
||||
useAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
|
||||
this.showAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -418,7 +406,7 @@ export default {
|
||||
if (this.isPrivate) {
|
||||
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
|
||||
}
|
||||
const keyLabel = this.isEditorHotKeyEnabled('cmd_enter')
|
||||
const keyLabel = isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter')
|
||||
? '(⌘ + ↵)'
|
||||
: '(↵)';
|
||||
return `${sendMessageText} ${keyLabel}`;
|
||||
@@ -491,7 +479,7 @@ export default {
|
||||
return !!this.signatureToApply;
|
||||
},
|
||||
sendWithSignature() {
|
||||
return this.fetchSignatureFlagFromUISettings(this.channelType);
|
||||
return this.fetchSignatureFlagFromUiSettings(this.channelType);
|
||||
},
|
||||
editorMessageKey() {
|
||||
const { editor_message_key: isEnabled } = this.uiSettings;
|
||||
@@ -760,7 +748,7 @@ export default {
|
||||
!this.showCannedMenu &&
|
||||
!this.showVariablesMenu &&
|
||||
this.isFocused &&
|
||||
this.isEditorHotKeyEnabled(selectedKey)
|
||||
isEditorHotKeyEnabled(this.uiSettings, selectedKey)
|
||||
);
|
||||
},
|
||||
onPaste(e) {
|
||||
@@ -892,7 +880,7 @@ export default {
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
|
||||
useAlert(errorMessage);
|
||||
this.showAlert(errorMessage);
|
||||
}
|
||||
},
|
||||
async onSendWhatsAppReply(messagePayload) {
|
||||
|
||||
@@ -77,10 +77,10 @@
|
||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { messageTimestamp } from 'shared/helpers/timeHelper';
|
||||
import timeMixin from '../../../../mixins/time';
|
||||
|
||||
export default {
|
||||
mixins: [inboxMixin],
|
||||
mixins: [inboxMixin, timeMixin],
|
||||
props: {
|
||||
sender: {
|
||||
type: Object,
|
||||
@@ -159,7 +159,7 @@ export default {
|
||||
return MESSAGE_STATUS.SENT === this.messageStatus;
|
||||
},
|
||||
readableTime() {
|
||||
return messageTimestamp(this.createdAt, 'LLL d, h:mm a');
|
||||
return this.messageTimestamp(this.createdAt, 'LLL d, h:mm a');
|
||||
},
|
||||
screenName() {
|
||||
const { additional_attributes: additionalAttributes = {} } =
|
||||
|
||||
@@ -17,13 +17,14 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import {
|
||||
DuplicateContactException,
|
||||
ExceptionWithMessage,
|
||||
} from 'shared/helpers/CustomErrors';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
@@ -51,18 +52,18 @@ export default {
|
||||
'contacts/create',
|
||||
this.getContactObject()
|
||||
);
|
||||
useAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||
this.showAlert(this.$t('CONTACT_FORM.SUCCESS_MESSAGE'));
|
||||
}
|
||||
this.openContactNewTab(contact.id);
|
||||
} catch (error) {
|
||||
if (error instanceof DuplicateContactException) {
|
||||
if (error.data.includes('phone_number')) {
|
||||
useAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
|
||||
this.showAlert(this.$t('CONTACT_FORM.FORM.PHONE_NUMBER.DUPLICATE'));
|
||||
}
|
||||
} else if (error instanceof ExceptionWithMessage) {
|
||||
useAlert(error.data);
|
||||
this.showAlert(error.data);
|
||||
} else {
|
||||
useAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||
this.showAlert(this.$t('CONTACT_FORM.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -40,7 +40,6 @@ const ALLOWED_FILE_TYPES = {
|
||||
IMAGE: 'image',
|
||||
VIDEO: 'video',
|
||||
AUDIO: 'audio',
|
||||
IG_REEL: 'ig_reel',
|
||||
};
|
||||
|
||||
export default {
|
||||
@@ -67,10 +66,7 @@ export default {
|
||||
return this.attachment.file_type === ALLOWED_FILE_TYPES.IMAGE;
|
||||
},
|
||||
isVideo() {
|
||||
return (
|
||||
this.attachment.file_type === ALLOWED_FILE_TYPES.VIDEO ||
|
||||
this.attachment.file_type === ALLOWED_FILE_TYPES.IG_REEL
|
||||
);
|
||||
return this.attachment.file_type === ALLOWED_FILE_TYPES.VIDEO;
|
||||
},
|
||||
isAudio() {
|
||||
return this.attachment.file_type === ALLOWED_FILE_TYPES.AUDIO;
|
||||
|
||||
+3
-2
@@ -31,9 +31,10 @@
|
||||
<script>
|
||||
import DyteAPI from 'dashboard/api/integrations/dyte';
|
||||
import { buildDyteURL } from 'shared/helpers/IntegrationHelper';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
messageId: {
|
||||
type: Number,
|
||||
@@ -60,7 +61,7 @@ export default {
|
||||
await DyteAPI.addParticipantToMeeting(this.messageId);
|
||||
this.dyteAuthToken = authToken;
|
||||
} catch (err) {
|
||||
useAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
|
||||
this.showAlert(this.$t('INTEGRATION_SETTINGS.DYTE.JOIN_ERROR'));
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
+6
-10
@@ -12,7 +12,7 @@
|
||||
@click="onClose"
|
||||
>
|
||||
<div
|
||||
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-white dark:bg-slate-900"
|
||||
class="bg-white dark:bg-slate-900 z-10 flex items-center justify-between w-full h-16 px-6 py-2"
|
||||
@click.stop
|
||||
>
|
||||
<div
|
||||
@@ -167,7 +167,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="z-10 flex items-center justify-center w-full h-16 px-6 py-2">
|
||||
<div class="flex items-center justify-center w-full h-16 px-6 py-2 z-10">
|
||||
<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"
|
||||
>
|
||||
@@ -182,14 +182,13 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import keyboardEventListenerMixins from 'shared/mixins/keyboardEventListenerMixins';
|
||||
import { messageTimestamp } from 'shared/helpers/timeHelper';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
|
||||
const ALLOWED_FILE_TYPES = {
|
||||
IMAGE: 'image',
|
||||
VIDEO: 'video',
|
||||
IG_REEL: 'ig_reel',
|
||||
AUDIO: 'audio',
|
||||
};
|
||||
|
||||
@@ -200,7 +199,7 @@ export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [keyboardEventListenerMixins],
|
||||
mixins: [keyboardEventListenerMixins, timeMixin],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
@@ -237,16 +236,13 @@ export default {
|
||||
readableTime() {
|
||||
const { created_at: createdAt } = this.activeAttachment;
|
||||
if (!createdAt) return '';
|
||||
return messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || '';
|
||||
return this.messageTimestamp(createdAt, 'LLL d yyyy, h:mm a') || '';
|
||||
},
|
||||
isImage() {
|
||||
return this.activeFileType === ALLOWED_FILE_TYPES.IMAGE;
|
||||
},
|
||||
isVideo() {
|
||||
return (
|
||||
this.activeFileType === ALLOWED_FILE_TYPES.VIDEO ||
|
||||
this.activeFileType === ALLOWED_FILE_TYPES.IG_REEL
|
||||
);
|
||||
return this.activeFileType === ALLOWED_FILE_TYPES.VIDEO;
|
||||
},
|
||||
isAudio() {
|
||||
return this.activeFileType === ALLOWED_FILE_TYPES.AUDIO;
|
||||
|
||||
@@ -85,11 +85,11 @@ describe('MoveActions', () => {
|
||||
it('shows alert', async () => {
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
|
||||
expect(emitter.emit).toBeCalledWith('newToastMessage', {
|
||||
message:
|
||||
'This contact is blocked successfully. You will not be notified of any future conversations.',
|
||||
action: null,
|
||||
});
|
||||
expect(emitter.emit).toBeCalledWith(
|
||||
'newToastMessage',
|
||||
'This contact is blocked successfully. You will not be notified of any future conversations.',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,10 +111,11 @@ describe('MoveActions', () => {
|
||||
it('shows alert', async () => {
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
|
||||
expect(emitter.emit).toBeCalledWith('newToastMessage', {
|
||||
message: 'This contact is unblocked successfully.',
|
||||
action: null,
|
||||
});
|
||||
expect(emitter.emit).toBeCalledWith(
|
||||
'newToastMessage',
|
||||
'This contact is unblocked successfully.',
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { getCurrentInstance } from 'vue';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
/**
|
||||
* Custom hook to track events
|
||||
* @returns {Function} The track function
|
||||
*/
|
||||
export const useTrack = () => {
|
||||
const vm = getCurrentInstance();
|
||||
if (!vm) throw new Error('must be called in setup');
|
||||
@@ -12,11 +8,6 @@ export const useTrack = () => {
|
||||
return vm.proxy.$track;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 });
|
||||
};
|
||||
export function useAlert(message, action) {
|
||||
emitter.emit('newToastMessage', message, action);
|
||||
}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,138 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
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,10 +38,9 @@ export class AnalyticsHelper {
|
||||
* @param {Object} user - User object
|
||||
*/
|
||||
identify(user) {
|
||||
if (!this.analytics || !user) {
|
||||
if (!this.analytics) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.user = user;
|
||||
this.analytics.identify(this.user.email, {
|
||||
userId: this.user.id,
|
||||
|
||||
@@ -52,10 +52,6 @@ class DashboardAudioNotificationHelper {
|
||||
};
|
||||
|
||||
executeRecurringNotification = () => {
|
||||
if (!window.WOOT || !window.WOOT.$store) {
|
||||
this.clearSetTimeout();
|
||||
return;
|
||||
}
|
||||
const mineConversation = window.WOOT.$store.getters.getMineChats({
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
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,20 +1,4 @@
|
||||
const FEATURE_HELP_URLS = {
|
||||
channel_email: 'https://chwt.app/hc/email',
|
||||
channel_facebook: 'https://chwt.app/hc/fb',
|
||||
help_center: 'https://chwt.app/hc/help-center',
|
||||
agent_bots: 'https://chwt.app/hc/agent-bots',
|
||||
team_management: 'https://chwt.app/hc/teams',
|
||||
labels: 'https://chwt.app/hc/labels',
|
||||
custom_attributes: 'https://chwt.app/hc/custom-attributes',
|
||||
canned_responses: 'https://chwt.app/hc/canned',
|
||||
integrations: 'https://chwt.app/hc/integrations',
|
||||
campaigns: 'https://chwt.app/hc/campaigns',
|
||||
reports: 'https://chwt.app/hc/reports',
|
||||
message_reply_to: 'https://chwt.app/hc/reply-to',
|
||||
sla: 'https://chwt.app/hc/sla',
|
||||
dashboard_apps: 'https://chwt.app/hc/dashboard-apps',
|
||||
};
|
||||
|
||||
export function getHelpUrlForFeature(featureName) {
|
||||
return FEATURE_HELP_URLS[featureName];
|
||||
const { helpUrls } = window.chatwootConfig;
|
||||
return helpUrls[featureName];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-console */
|
||||
import NotificationSubscriptions from '../api/notificationSubscription';
|
||||
import auth from '../api/auth';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
export const verifyServiceWorkerExistence = (callback = () => {}) => {
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
@@ -69,13 +69,19 @@ export const registerSubscription = (onSuccess = () => {}) => {
|
||||
onSuccess();
|
||||
})
|
||||
.catch(() => {
|
||||
useAlert('This browser does not support desktop notification');
|
||||
emitter.emit(
|
||||
'newToastMessage',
|
||||
'This browser does not support desktop notification'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const requestPushPermissions = ({ onSuccess }) => {
|
||||
if (!('Notification' in window)) {
|
||||
useAlert('This browser does not support desktop notification');
|
||||
emitter.emit(
|
||||
'newToastMessage',
|
||||
'This browser does not support desktop notification'
|
||||
);
|
||||
} else if (Notification.permission === 'granted') {
|
||||
registerSubscription(onSuccess);
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -33,7 +33,7 @@
|
||||
"NONE": "None",
|
||||
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
|
||||
"ASSIGN_SELECTED_TEAMS": "Assign selected team.",
|
||||
"ASSIGN_SUCCESFUL": "Teams assigned successfully.",
|
||||
"ASSIGN_SUCCESFUL": "Teams assiged successfully.",
|
||||
"ASSIGN_FAILED": "Failed to assign team. Please try again."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +95,7 @@
|
||||
},
|
||||
"NETWORK": {
|
||||
"NOTIFICATION": {
|
||||
"OFFLINE": "Offline",
|
||||
"RECONNECTING": "Reconnecting...",
|
||||
"RECONNECT_SUCCESS": "Reconnected"
|
||||
"OFFLINE": "Offline"
|
||||
},
|
||||
"BUTTON": {
|
||||
"REFRESH": "Refresh"
|
||||
@@ -156,7 +154,7 @@
|
||||
"UNTIL_TOMORROW": "Until tomorrow",
|
||||
"UNTIL_NEXT_MONTH": "Until next month",
|
||||
"AN_HOUR_FROM_NOW": "Until an hour from now",
|
||||
"UNTIL_CUSTOM_TIME": "Custom...",
|
||||
"CUSTOM": "Custom...",
|
||||
"CHANGE_APPEARANCE": "Change Appearance",
|
||||
"LIGHT_MODE": "Light",
|
||||
"DARK_MODE": "Dark",
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
"INBOX_MGMT": {
|
||||
"HEADER": "Inboxes",
|
||||
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
|
||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||
"LIST": {
|
||||
"404": "There are no inboxes attached to this account."
|
||||
},
|
||||
@@ -366,7 +364,6 @@
|
||||
"TITLE": "Microsoft Email",
|
||||
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"SIGN_IN": "Sign in with Microsoft",
|
||||
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
|
||||
}
|
||||
|
||||
@@ -209,87 +209,6 @@
|
||||
"API_SUCCESS": "Dashboard app deleted successfully",
|
||||
"API_ERROR": "We couldn't delete the app. Please try again later"
|
||||
}
|
||||
},
|
||||
"LINEAR": {
|
||||
"ADD_OR_LINK_BUTTON": "Create/Link Linear Issue",
|
||||
"LOADING": "Fetching linear issues...",
|
||||
"LOADING_ERROR": "There was an error fetching the linear issues, please try again",
|
||||
"CREATE": "Create",
|
||||
"LINK": {
|
||||
"SEARCH": "Search issues",
|
||||
"SELECT": "Select issue",
|
||||
"TITLE": "Link",
|
||||
"EMPTY_LIST": "No linear issues found",
|
||||
"LOADING": "Loading",
|
||||
"ERROR": "There was an error fetching the linear issues, please try again",
|
||||
"LINK_SUCCESS": "Issue linked successfully",
|
||||
"LINK_ERROR": "There was an error linking the issue, please try again",
|
||||
"LINK_TITLE": "Conversation (#%{conversationId}) with %{name}"
|
||||
},
|
||||
"ADD_OR_LINK": {
|
||||
"TITLE": "Create/link linear issue",
|
||||
"DESCRIPTION": "Create Linear issues from conversations, or link existing ones for seamless tracking.",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Title",
|
||||
"PLACEHOLDER": "Enter title",
|
||||
"REQUIRED_ERROR": "Title is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Description",
|
||||
"PLACEHOLDER": "Enter description"
|
||||
},
|
||||
"TEAM": {
|
||||
"LABEL": "Team",
|
||||
"PLACEHOLDER": "Select team",
|
||||
"SEARCH": "Search team",
|
||||
"REQUIRED_ERROR": "Team is required"
|
||||
},
|
||||
"ASSIGNEE": {
|
||||
"LABEL": "Assignee",
|
||||
"PLACEHOLDER": "Select assignee",
|
||||
"SEARCH": "Search assignee"
|
||||
},
|
||||
"PRIORITY": {
|
||||
"LABEL": "Priority",
|
||||
"PLACEHOLDER": "Select priority",
|
||||
"SEARCH": "Search priority"
|
||||
},
|
||||
"LABEL": {
|
||||
"LABEL": "Label",
|
||||
"PLACEHOLDER": "Select label",
|
||||
"SEARCH": "Search label"
|
||||
},
|
||||
"STATUS": {
|
||||
"LABEL": "Status",
|
||||
"PLACEHOLDER": "Select status",
|
||||
"SEARCH": "Search status"
|
||||
},
|
||||
"PROJECT": {
|
||||
"LABEL": "Project",
|
||||
"PLACEHOLDER": "Select project",
|
||||
"SEARCH": "Search project"
|
||||
}
|
||||
},
|
||||
"CREATE": "Create",
|
||||
"CANCEL": "Cancel",
|
||||
"CREATE_SUCCESS": "Issue created successfully",
|
||||
"CREATE_ERROR": "There was an error creating the issue, please try again",
|
||||
"LOADING_TEAM_ERROR": "There was an error fetching the teams, please try again",
|
||||
"LOADING_TEAM_ENTITIES_ERROR": "There was an error fetching the team entities, please try again"
|
||||
},
|
||||
"ISSUE": {
|
||||
"STATUS": "Status",
|
||||
"PRIORITY": "Priority",
|
||||
"ASSIGNEE": "Assignee",
|
||||
"LABELS": "Labels",
|
||||
"CREATED_AT": "Created at %{createdAt}"
|
||||
},
|
||||
"UNLINK": {
|
||||
"TITLE": "Unlink",
|
||||
"SUCCESS": "Issue unlinked successfully",
|
||||
"ERROR": "There was an error unlinking the issue, please try again"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@
|
||||
"SLA": "SLA",
|
||||
"BETA": "Beta",
|
||||
"REPORTS_OVERVIEW": "Overview",
|
||||
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
|
||||
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
|
||||
"HELP_CENTER": {
|
||||
"TITLE": "Help Center",
|
||||
"ALL_ARTICLES": "All Articles",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"FILTER": {
|
||||
"TITLE": "تصفية المحادثات",
|
||||
"SUBTITLE": "أضف عوامل التصفية أدناه واضغط على 'تطبيق الفلترة' لتجنب الفوضى في المحادثات.",
|
||||
"EDIT_CUSTOM_FILTER": "تحرير المجلد",
|
||||
"CUSTOM_VIEWS_SUBTITLE": "أضف أو أزل الفلاتر وقم بتحديث المجلد الخاص بك.",
|
||||
"ADD_NEW_FILTER": "إضافة فلتر",
|
||||
"FILTER_DELETE_ERROR": "عذرًا، يبدو أننا لا نستطيع حفظ أي شيء! يرجى إضافة فلتر واحد على الأقل لحفظه.",
|
||||
"SUBTITLE": "Add your filters below and hit 'Apply filters' to cut through the chat clutter.",
|
||||
"EDIT_CUSTOM_FILTER": "Edit Folder",
|
||||
"CUSTOM_VIEWS_SUBTITLE": "Add or remove filters and update your folder.",
|
||||
"ADD_NEW_FILTER": "Add filter",
|
||||
"FILTER_DELETE_ERROR": "Oops, looks like we can't save nothing! Please add at least one filter to save it.",
|
||||
"SUBMIT_BUTTON_LABEL": "تطبيق عامل التصفية",
|
||||
"UPDATE_BUTTON_LABEL": "تحديث المجلد",
|
||||
"UPDATE_BUTTON_LABEL": "Update folder",
|
||||
"CANCEL_BUTTON_LABEL": "إلغاء",
|
||||
"CLEAR_BUTTON_LABEL": "مسح الفلاتر",
|
||||
"FOLDER_LABEL": "اسم المجلد",
|
||||
"FOLDER_QUERY_LABEL": "استعلام المجلد",
|
||||
"CLEAR_BUTTON_LABEL": "Clear filters",
|
||||
"FOLDER_LABEL": "Folder Name",
|
||||
"FOLDER_QUERY_LABEL": "Folder Query",
|
||||
"EMPTY_VALUE_ERROR": "القيمة مطلوبة.",
|
||||
"TOOLTIP_LABEL": "تصفية المحادثات",
|
||||
"QUERY_DROPDOWN_LABELS": {
|
||||
@@ -36,15 +36,15 @@
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"STATUS": "الحالة",
|
||||
"ASSIGNEE_NAME": "اسم المستلم",
|
||||
"ASSIGNEE_NAME": "Assignee name",
|
||||
"INBOX_NAME": "اسم صندوق الوارد",
|
||||
"TEAM_NAME": "اسم الفريق",
|
||||
"CONVERSATION_IDENTIFIER": "معرف المحادثة",
|
||||
"CAMPAIGN_NAME": "اسم الحملة",
|
||||
"CONVERSATION_IDENTIFIER": "Conversation identifier",
|
||||
"CAMPAIGN_NAME": "Campaign name",
|
||||
"LABELS": "الوسوم",
|
||||
"BROWSER_LANGUAGE": "لغة المتصفح",
|
||||
"BROWSER_LANGUAGE": "Browser language",
|
||||
"PRIORITY": "الأولوية",
|
||||
"COUNTRY_NAME": "اسم الدولة",
|
||||
"COUNTRY_NAME": "Country name",
|
||||
"REFERER_LINK": "رابط المرجع",
|
||||
"CUSTOM_ATTRIBUTE_LIST": "القائمة",
|
||||
"CUSTOM_ATTRIBUTE_TEXT": "النص",
|
||||
@@ -55,15 +55,15 @@
|
||||
"LAST_ACTIVITY": "النشاط الأخير"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "الفلاتر القياسية",
|
||||
"ADDITIONAL_FILTERS": "الفلاتر الإضافية",
|
||||
"CUSTOM_ATTRIBUTES": "الخصائص المخصصة"
|
||||
"STANDARD_FILTERS": "Standard filters",
|
||||
"ADDITIONAL_FILTERS": "Additional filters",
|
||||
"CUSTOM_ATTRIBUTES": "Custom attributes"
|
||||
},
|
||||
"CUSTOM_VIEWS": {
|
||||
"ADD": {
|
||||
"TITLE": "هل تريد حفظ هذا الفلتر؟",
|
||||
"LABEL": "تسمية هذا الفلتر",
|
||||
"PLACEHOLDER": "قم بتسمية الفلتر لتتمكن من الرجوع إليه لاحقًا.",
|
||||
"PLACEHOLDER": "Name your filter to refer it later.",
|
||||
"ERROR_MESSAGE": "الاسم مطلوب.",
|
||||
"SAVE_BUTTON": "حفظ الفلتر",
|
||||
"CANCEL_BUTTON": "إلغاء",
|
||||
@@ -77,7 +77,7 @@
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"EDIT_BUTTON": "تحرير المجلد"
|
||||
"EDIT_BUTTON": "Edit folder"
|
||||
},
|
||||
"DELETE": {
|
||||
"DELETE_BUTTON": "حذف الفلتر",
|
||||
@@ -85,7 +85,7 @@
|
||||
"CONFIRM": {
|
||||
"TITLE": "تأكيد الحذف",
|
||||
"MESSAGE": "هل أنت متأكد من حذف الفلتر ",
|
||||
"YES": "نعم، احذف",
|
||||
"YES": "Yes, delete",
|
||||
"NO": "لا، احتفظ به"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,73 +1,73 @@
|
||||
{
|
||||
"AGENT_BOTS": {
|
||||
"HEADER": "البوتات",
|
||||
"LOADING_EDITOR": "جارٍ تحميل المحرر...",
|
||||
"HEADER_BTN_TXT": "إضافة تكوين بوت",
|
||||
"SIDEBAR_TXT": "<p><b>البوتات</b> <p>البوتات هي مثل الأعضاء الأكثر روعة في فريقك. يمكنهم التعامل مع الأمور الصغيرة، حتى تتمكن من التركيز على الأمور التي تهمك. جربهم.</p> <p> يمكنك إدارة البوتات الخاصة بك من هذه الصفحة أو إنشاء بوتات جديدة باستخدام زر 'إضافة تكوين بوت'.</p> <p> افتح <a href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677497472-how-to-use-agent-bots\" target=\"blank\">دليل البوتات</a> في علامة تبويب أخرى للحصول على مساعدة.</p>",
|
||||
"HEADER": "Bots",
|
||||
"LOADING_EDITOR": "Loading editor...",
|
||||
"HEADER_BTN_TXT": "Add bot configuration",
|
||||
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.</p> <p> You can manage your bots from this page or create new ones using the 'Add bot configuraton' button.</p> <p> Open the <a href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677497472-how-to-use-agent-bots\" target=\"blank\">Agent bots handbook</a> in another tab for a helping hand.</p>",
|
||||
"CSML_BOT_EDITOR": {
|
||||
"NAME": {
|
||||
"LABEL": "اسم البوت",
|
||||
"PLACEHOLDER": "قم بتسمية البوت الخاص بك.",
|
||||
"ERROR": "اسم البوت مطلوب."
|
||||
"LABEL": "Bot name",
|
||||
"PLACEHOLDER": "Name your bot.",
|
||||
"ERROR": "Bot name is required."
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "وصف البوت",
|
||||
"PLACEHOLDER": "ماذا يفعل هذا البوت؟"
|
||||
"LABEL": "Bot description",
|
||||
"PLACEHOLDER": "What does this bot do?"
|
||||
},
|
||||
"BOT_CONFIG": {
|
||||
"ERROR": "يرجى إدخال تكوين بوت CSML الخاص بك أعلاه.",
|
||||
"API_ERROR": "تكوين CSML الخاص بك غير صالح. يرجى إصلاحه والمحاولة مرة أخرى."
|
||||
"ERROR": "Please enter your CSML bot configuration above.",
|
||||
"API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
|
||||
},
|
||||
"SUBMIT": "التحقق والحفظ"
|
||||
"SUBMIT": "Validate and save"
|
||||
},
|
||||
"BOT_CONFIGURATION": {
|
||||
"TITLE": "اختر بوت",
|
||||
"DESC": "قم بتعيين بوت لصندوق الوارد الخاص بك. يمكنهم التعامل مع المحادثات الأولية ونقلها إلى وكيل مباشر عند الضرورة.",
|
||||
"TITLE": "Select an agent bot",
|
||||
"DESC": "Assign an Agent Bot to your inbox. They can handle initial conversations and transfer them to a live agent when necessary.",
|
||||
"SUBMIT": "تحديث",
|
||||
"DISCONNECT": "فصل البوت",
|
||||
"SUCCESS_MESSAGE": "تم تحديث البوت بنجاح.",
|
||||
"DISCONNECTED_SUCCESS_MESSAGE": "تم فصل البوت بنجاح.",
|
||||
"ERROR_MESSAGE": "تعذر تحديث البوت. يرجى المحاولة مرة أخرى.",
|
||||
"DISCONNECTED_ERROR_MESSAGE": "تعذر فصل البوت. يرجى المحاولة مرة أخرى.",
|
||||
"SELECT_PLACEHOLDER": "اختر بوت"
|
||||
"DISCONNECT": "Disconnect bot",
|
||||
"SUCCESS_MESSAGE": "Successfully updated the agent bot.",
|
||||
"DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot.",
|
||||
"ERROR_MESSAGE": "Could not update the agent bot. Please try again.",
|
||||
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot. Please try again.",
|
||||
"SELECT_PLACEHOLDER": "Select bot"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "تكوين بوت جديد",
|
||||
"TITLE": "Configure new bot",
|
||||
"CANCEL_BUTTON_TEXT": "إلغاء",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "تمت إضافة البوت بنجاح.",
|
||||
"ERROR_MESSAGE": "تعذر إضافة البوت. يرجى المحاولة مرة أخرى لاحقًا."
|
||||
"SUCCESS_MESSAGE": "Bot added successfully.",
|
||||
"ERROR_MESSAGE": "Could not add bot. Please try again later."
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"404": "لم يتم العثور على أي بوتات. يمكنك إنشاء بوت بالنقر على زر 'تكوين بوت جديد' ↗",
|
||||
"LOADING": "جارٍ جلب البوتات...",
|
||||
"TYPE": "نوع البوت"
|
||||
"404": "No bots found. You can create a bot by clicking the 'Configure new bot' button ↗",
|
||||
"LOADING": "Fetching bots...",
|
||||
"TYPE": "Bot type"
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "حذف",
|
||||
"TITLE": "حذف البوت",
|
||||
"TITLE": "Delete bot",
|
||||
"SUBMIT": "حذف",
|
||||
"CANCEL_BUTTON_TEXT": "إلغاء",
|
||||
"DESCRIPTION": "هل أنت متأكد أنك تريد حذف هذا البوت؟ هذا الإجراء لا يمكن التراجع عنه.",
|
||||
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "تم حذف البوت بنجاح.",
|
||||
"ERROR_MESSAGE": "تعذر حذف البوت. يرجى المحاولة مرة أخرى."
|
||||
"SUCCESS_MESSAGE": "Bot deleted successfully.",
|
||||
"ERROR_MESSAGE": "Could not delete bot. Please try again."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"BUTTON_TEXT": "تعديل",
|
||||
"LOADING": "جارٍ جلب البوتات...",
|
||||
"TITLE": "تعديل البوت",
|
||||
"LOADING": "Fetching bots...",
|
||||
"TITLE": "Edit bot",
|
||||
"CANCEL_BUTTON_TEXT": "إلغاء",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "تم تحديث البوت بنجاح.",
|
||||
"ERROR_MESSAGE": "تعذر تحديث البوت. يرجى المحاولة مرة أخرى."
|
||||
"SUCCESS_MESSAGE": "Bot updated successfully.",
|
||||
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
||||
}
|
||||
},
|
||||
"TYPES": {
|
||||
"WEBHOOK": "بوت Webhook",
|
||||
"CSML": "بوت CSML"
|
||||
"WEBHOOK": "Webhook bot",
|
||||
"CSML": "CSML bot"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,15 +41,15 @@
|
||||
"IN_VALID": "مفتاح غير صالح"
|
||||
},
|
||||
"REGEX_PATTERN": {
|
||||
"LABEL": "نمط Regex",
|
||||
"PLACEHOLDER": "الرجاء إدخال سمة مخصصة ريمكس (اختياري)"
|
||||
"LABEL": "Regex Pattern",
|
||||
"PLACEHOLDER": "Please enter custom attribute regex pattern. (Optional)"
|
||||
},
|
||||
"REGEX_CUE": {
|
||||
"LABEL": "Regex Cue",
|
||||
"PLACEHOLDER": "الرجاء إدخال تلميح نمط regex (اختياري)"
|
||||
"PLACEHOLDER": "Please enter regex pattern hint. (Optional)"
|
||||
},
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "تمكين التحقق من صحة regex"
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
},
|
||||
"API": {
|
||||
@@ -106,15 +106,15 @@
|
||||
"NOT_FOUND": "لا توجد سمات مخصصة تم تكوينها"
|
||||
},
|
||||
"REGEX_PATTERN": {
|
||||
"LABEL": "نمط Regex",
|
||||
"PLACEHOLDER": "الرجاء إدخال سمة مخصصة ريمكس (اختياري)"
|
||||
"LABEL": "Regex Pattern",
|
||||
"PLACEHOLDER": "Please enter custom attribute regex pattern. (Optional)"
|
||||
},
|
||||
"REGEX_CUE": {
|
||||
"LABEL": "Regex Cue",
|
||||
"PLACEHOLDER": "الرجاء إدخال تلميح نمط regex (اختياري)"
|
||||
"PLACEHOLDER": "Please enter regex pattern hint. (Optional)"
|
||||
},
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "تمكين التحقق من صحة regex"
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
{
|
||||
"AUDIT_LOGS": {
|
||||
"HEADER": "سجلات التدقيق",
|
||||
"HEADER_BTN_TXT": "إضافة سجلات التدقيق",
|
||||
"LOADING": "جارٍ جلب سجلات التدقيق",
|
||||
"HEADER": "Audit Logs",
|
||||
"HEADER_BTN_TXT": "Add Audit Logs",
|
||||
"LOADING": "Fetching Audit Logs",
|
||||
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام",
|
||||
"SIDEBAR_TXT": "<p><b>سجلات التدقيق</b> </p><p> سجلات التدقيق هي مسارات للأحداث والإجراءات في نظام Chatwoot. </p>",
|
||||
"SIDEBAR_TXT": "<p><b>Audit Logs</b> </p><p> Audit Logs are trails for events and actions in a Chatwoot System. </p>",
|
||||
"LIST": {
|
||||
"404": "لا توجد سجلات تدقيق متاحة في هذا الحساب.",
|
||||
"TITLE": "إدارة سجلات التدقيق",
|
||||
"DESC": "سجلات التدقيق هي مسارات للأحداث والإجراءات في نظام Chatwoot.",
|
||||
"404": "There are no Audit Logs available in this account.",
|
||||
"TITLE": "Manage Audit Logs",
|
||||
"DESC": "Audit Logs are trails for events and actions in a Chatwoot System.",
|
||||
"TABLE_HEADER": [
|
||||
"المستخدم",
|
||||
"الإجراء",
|
||||
"User",
|
||||
"Action",
|
||||
"عنوان IP"
|
||||
]
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "تم استرجاع سجلات التدقيق بنجاح",
|
||||
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
|
||||
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
|
||||
},
|
||||
"DEFAULT_USER": "النظام",
|
||||
"DEFAULT_USER": "System",
|
||||
"AUTOMATION_RULE": {
|
||||
"ADD": "%{agentName} أنشأ قاعدة أتمتة جديدة (#%{id})",
|
||||
"EDIT": "%{agentName} قام بتحديث قاعدة أتمتة (#%{id})",
|
||||
"DELETE": "%{agentName} حذف قاعدة أتمتة (#%{id})"
|
||||
"ADD": "%{agentName} created a new automation rule (#%{id})",
|
||||
"EDIT": "%{agentName} updated an automation rule (#%{id})",
|
||||
"DELETE": "%{agentName} deleted an automation rule (#%{id})"
|
||||
},
|
||||
"ACCOUNT_USER": {
|
||||
"ADD": "%{agentName} دعا %{invitee} إلى الحساب كـ %{role}",
|
||||
"ADD": "%{agentName} invited %{invitee} to the account as an %{role}",
|
||||
"EDIT": {
|
||||
"SELF": "%{agentName} غير %{attributes} الخاصة به إلى %{values}",
|
||||
"OTHER": "%{agentName} غير %{attributes} لـ %{user} إلى %{values}"
|
||||
"SELF": "%{agentName} changed their %{attributes} to %{values}",
|
||||
"OTHER": "%{agentName} changed %{attributes} of %{user} to %{values}"
|
||||
}
|
||||
},
|
||||
"INBOX": {
|
||||
"ADD": "%{agentName} أنشأ صندوق وارد جديد (#%{id})",
|
||||
"EDIT": "%{agentName} قام بتحديث صندوق الوارد (#%{id})",
|
||||
"DELETE": "%{agentName} حذف صندوق الوارد (#%{id})"
|
||||
"ADD": "%{agentName} created a new inbox (#%{id})",
|
||||
"EDIT": "%{agentName} updated an inbox (#%{id})",
|
||||
"DELETE": "%{agentName} deleted an inbox (#%{id})"
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"ADD": "%{agentName} أنشأ Webhook جديد (#%{id})",
|
||||
"EDIT": "%{agentName} قام بتحديث Webhook (#%{id})",
|
||||
"DELETE": "%{agentName} حذف Webhook (#%{id})"
|
||||
"ADD": "%{agentName} created a new webhook (#%{id})",
|
||||
"EDIT": "%{agentName} updated a webhook (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a webhook (#%{id})"
|
||||
},
|
||||
"USER_ACTION": {
|
||||
"SIGN_IN": "%{agentName} قام بتسجيل الدخول",
|
||||
"SIGN_OUT": "%{agentName} قام بتسجيل الخروج"
|
||||
"SIGN_IN": "%{agentName} signed in",
|
||||
"SIGN_OUT": "%{agentName} signed out"
|
||||
},
|
||||
"TEAM": {
|
||||
"ADD": "%{agentName} أنشأ فريق جديد (#%{id})",
|
||||
"EDIT": "%{agentName} قام بتحديث الفريق (#%{id})",
|
||||
"DELETE": "%{agentName} حذف الفريق (#%{id})"
|
||||
"ADD": "%{agentName} created a new team (#%{id})",
|
||||
"EDIT": "%{agentName} updated a team (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a team (#%{id})"
|
||||
},
|
||||
"MACRO": {
|
||||
"ADD": "%{agentName} أنشأ ماكرو جديد (#%{id})",
|
||||
"EDIT": "%{agentName} قام بتحديث ماكرو (#%{id})",
|
||||
"DELETE": "%{agentName} حذف ماكرو (#%{id})"
|
||||
"ADD": "%{agentName} created a new macro (#%{id})",
|
||||
"EDIT": "%{agentName} updated a macro (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
||||
},
|
||||
"INBOX_MEMBER": {
|
||||
"ADD": "%{agentName} أضاف %{user} إلى صندوق الوارد (#%{inbox_id})",
|
||||
"REMOVE": "%{agentName} أزال %{user} من صندوق الوارد (#%{inbox_id})"
|
||||
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
|
||||
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
|
||||
},
|
||||
"TEAM_MEMBER": {
|
||||
"ADD": "%{agentName} أضاف %{user} إلى الفريق (#%{team_id})",
|
||||
"REMOVE": "%{agentName} أزال %{user} من الفريق (#%{team_id})"
|
||||
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
|
||||
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
|
||||
},
|
||||
"ACCOUNT": {
|
||||
"EDIT": "%{agentName} قام بتحديث إعدادات الحساب (#%{id})"
|
||||
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,23 +10,23 @@
|
||||
"ASSIGN_AGENT_TOOLTIP": "تعيين وكيل",
|
||||
"ASSIGN_TEAM_TOOLTIP": "تعيين فريق",
|
||||
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح.",
|
||||
"ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى.",
|
||||
"ASSIGN_FAILED": "Failed to assign conversations. Please try again.",
|
||||
"RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح.",
|
||||
"RESOLVE_FAILED": "فشل في حل المحادثات، يرجى المحاولة مرة أخرى.",
|
||||
"RESOLVE_FAILED": "Failed to resolve conversations. Please try again.",
|
||||
"ALL_CONVERSATIONS_SELECTED_ALERT": "المحادثات المرئية في هذه الصفحة هي المحددة فقط.",
|
||||
"AGENT_LIST_LOADING": "جاري تحميل الوكلاء",
|
||||
"UPDATE": {
|
||||
"CHANGE_STATUS": "تغيير الحالة",
|
||||
"SNOOZE_UNTIL": "غفوة",
|
||||
"UPDATE_SUCCESFUL": "تم تحديث حالة المحادثة بنجاح.",
|
||||
"UPDATE_FAILED": "فشل تحديث المحادثات، الرجاء المحاولة مرة أخرى."
|
||||
"UPDATE_FAILED": "Failed to update conversations. Please try again."
|
||||
},
|
||||
"LABELS": {
|
||||
"ASSIGN_LABELS": "إضافة وسم",
|
||||
"ASSIGN_LABELS": "Assign labels",
|
||||
"NO_LABELS_FOUND": "لم يتم العثور على تسميات لـ",
|
||||
"ASSIGN_SELECTED_LABELS": "تعيين التسميات المحددة",
|
||||
"ASSIGN_SUCCESFUL": "تم تعيين التسميات بنجاح.",
|
||||
"ASSIGN_FAILED": "فشل في تعيين التسميات ، الرجاء المحاولة مرة أخرى."
|
||||
"ASSIGN_FAILED": "Failed to assign labels. Please try again."
|
||||
},
|
||||
"TEAMS": {
|
||||
"TEAM_SELECT_LABEL": "اختيار فريق",
|
||||
@@ -34,7 +34,7 @@
|
||||
"NO_TEAMS_AVAILABLE": "لا توجد فرق مضافة إلى هذا الحساب حتى الآن.",
|
||||
"ASSIGN_SELECTED_TEAMS": "تعيين فريق محدد.",
|
||||
"ASSIGN_SUCCESFUL": "تم تعيين الفرق بنجاح.",
|
||||
"ASSIGN_FAILED": "فشل تعيين الفريق، الرجاء المحاولة مرة أخرى."
|
||||
"ASSIGN_FAILED": "Failed to assign team. Please try again."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
"HEADER_BTN_TXT": "إضافة رد جاهز",
|
||||
"LOADING": "جاري جلب الردود الجاهزة...",
|
||||
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام.",
|
||||
"SIDEBAR_TXT": "<p><b>الردود المسبقة</b> </p></p><p> الردود المسبقة هي قوالب رد مكتوبة مسبقاً تساعدك على الرد بسرعة على محادثة. لإدراج استجابة مسبقة أثناء الدردشة، يمكن للوكلاء كتابة رمز قصير يسبقه حرف '/'. </p><p> يمكنك إدارة ردودك المعلبة من هذه الصفحة أو إنشاء استجابات جديدة باستخدام زر \"إضافة استجابة مسبقة\".</p><p>افتح دليل الردود المسبقة <a target=\"_blank\" href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677501325-how-to-create-saved-reply-templates-with-canned-responses\"></a> في علامة تبويب أخرى للحصول على يد المساعدة.</p><p>أيضا، تحقق من كل المكتبة <a href=\"https://www.chatwoot.com/tools/canned-responses-library\" target=\"_blank\">الردود المسبقة</a>.</p>",
|
||||
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are pre-written reply templates that help you quickly respond to a conversation. To insert a canned response during a chat, agents can type a short code preceded by a '/' character. </p><p> You can manage your canned responses from this page or create new ones using the \"Add canned response\" button.</p><p>Open the <a target=\"_blank\" href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677501325-how-to-create-saved-reply-templates-with-canned-responses\">Canned Responses handbook</a> in another tab for a helping hand.</p><p>Also, check out the all-new <a href=\"https://www.chatwoot.com/tools/canned-responses-library\" target=\"_blank\">Canned Responses Library</a>.</p>",
|
||||
"LIST": {
|
||||
"404": "لا توجد ردود جاهزة متوفرة في هذا الحساب.",
|
||||
"TITLE": "إدارة الردود الجاهزة",
|
||||
"DESC": "الردود الجاهزة هي قوالب رسائل معدة مسبقاً يمكن استخدامها لتسريع كتابة الردود في المحادثات.",
|
||||
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
|
||||
"TABLE_HEADER": [
|
||||
"كود مختصر",
|
||||
"المحتوى",
|
||||
@@ -17,7 +17,7 @@
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "إضافة رد جاهز",
|
||||
"DESC": "الردود الجاهزة هي قوالب رسائل معدة مسبقاً يمكن استخدامها لتسريع كتابة الردود في المحادثات.",
|
||||
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
|
||||
"CANCEL_BUTTON_TEXT": "إلغاء",
|
||||
"FORM": {
|
||||
"SHORT_CODE": {
|
||||
|
||||
@@ -59,22 +59,22 @@
|
||||
"TEXT": "اخر نشاط: الأحدث أولا"
|
||||
},
|
||||
"created_at_desc": {
|
||||
"TEXT": "تم إنشاؤها في: الأحدث أولاً"
|
||||
"TEXT": "Created at: Newest first"
|
||||
},
|
||||
"created_at_asc": {
|
||||
"TEXT": "تم إنشاؤها في: أقدم أولاً"
|
||||
"TEXT": "Created at: Oldest first"
|
||||
},
|
||||
"priority_desc": {
|
||||
"TEXT": "الأولوية: الأعلى أولا"
|
||||
"TEXT": "Priority: Highest first"
|
||||
},
|
||||
"priority_asc": {
|
||||
"TEXT": "الأولوية: الأقل أولاً"
|
||||
"TEXT": "Priority: Lowest first"
|
||||
},
|
||||
"waiting_since_asc": {
|
||||
"TEXT": "الرد المعلق: الأطول أولا"
|
||||
"TEXT": "Pending Response: Longest first"
|
||||
},
|
||||
"waiting_since_desc": {
|
||||
"TEXT": "الرد المعلق: أقصر أولاً"
|
||||
"TEXT": "Pending Response: Shortest first"
|
||||
}
|
||||
},
|
||||
"ATTACHMENTS": {
|
||||
@@ -110,7 +110,7 @@
|
||||
"LABEL": "تم إنشاؤها في"
|
||||
},
|
||||
"LAST_USER_MESSAGE_AT": {
|
||||
"NAME": "آخر رسالة للمستخدم في",
|
||||
"NAME": "Last user message at",
|
||||
"LABEL": "أخر رسالة"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,14 +34,14 @@
|
||||
"TITLE": "اضافة تصنيف جديد",
|
||||
"PLACEHOLDER": "ابحث عن تصنيفات",
|
||||
"NO_RESULT": "لم يتم العثور على تصنيفات",
|
||||
"CREATE_LABEL": "إنشاء تسمية جديدة"
|
||||
"CREATE_LABEL": "Create new label"
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACT": "دمج جهة الاتصال",
|
||||
"CONTACT_ACTIONS": "إجراءات جهات الاتصال",
|
||||
"MUTE_CONTACT": "احجب جهة الاتصال",
|
||||
"UNMUTE_CONTACT": "إلغاء حجب جهة الاتصال",
|
||||
"MUTED_SUCCESS": "تم حظر جهة الاتصال هذه بنجاح، لن يتم إعلامك بأي محادثات في المستقبل.",
|
||||
"MUTED_SUCCESS": "This contact is blocked successfully. You will not be notified of any future conversations.",
|
||||
"UNMUTED_SUCCESS": "تم إلغاء حجب جهة الاتصال بنجاح.",
|
||||
"SEND_TRANSCRIPT": "إرسال النص",
|
||||
"EDIT_LABEL": "تعديل",
|
||||
@@ -72,7 +72,7 @@
|
||||
"SUBMIT": "استيراد",
|
||||
"CANCEL": "إلغاء"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "سيتم إعلامك عبر البريد الإلكتروني عند اكتمال الاستيراد.",
|
||||
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
|
||||
"ERROR_MESSAGE": "حدث خطأ، الرجاء المحاولة مرة أخرى"
|
||||
},
|
||||
"EXPORT_CONTACTS": {
|
||||
@@ -83,8 +83,8 @@
|
||||
"ERROR_MESSAGE": "حدث خطأ، الرجاء المحاولة مرة أخرى",
|
||||
"CONFIRM": {
|
||||
"TITLE": "تصدير جهات الاتصال",
|
||||
"MESSAGE": "هل أنت متأكد من أنك تريد تصدير جميع جهات الاتصال؟",
|
||||
"FILTERED_MESSAGE": "هل أنت متأكد من أنك تريد تصدير جميع جهات الاتصال التي تم تصفيتها؟",
|
||||
"MESSAGE": "Are you sure you want to export all contacts?",
|
||||
"FILTERED_MESSAGE": "Are you sure you want to export all the filtered contacts?",
|
||||
"YES": "نعم, قم بالتصدير",
|
||||
"NO": "نعم, إلغاء"
|
||||
}
|
||||
@@ -138,7 +138,7 @@
|
||||
"LABEL": "رقم الهاتف",
|
||||
"HELP": "يجب ان يحتوى رقم الهاتف على كود دولتك تسبقها علامة +\nمثال: +20101243567",
|
||||
"ERROR": "يجب ان تكون خانة رقم الهاتف إما فارغة او مكتملة مع رمز الدولة",
|
||||
"DIAL_CODE_ERROR": "الرجاء تحديد رمز الاتصال من القائمة",
|
||||
"DIAL_CODE_ERROR": "Please select a dial code from the list",
|
||||
"DUPLICATE": "رقم الهاتف هذا مستخدم لجهة اتصال أخرى."
|
||||
},
|
||||
"LOCATION": {
|
||||
@@ -213,8 +213,8 @@
|
||||
"ERROR": "لا يمكن أن تكون الرسالة فارغة"
|
||||
},
|
||||
"ATTACHMENTS": {
|
||||
"SELECT": "اختر الملفات",
|
||||
"HELP_TEXT": "قم بسحب وإسقاط الملفات هنا أو اختر ملفات لإرفاقها"
|
||||
"SELECT": "Choose files",
|
||||
"HELP_TEXT": "Drag and drop files here or choose files to attach"
|
||||
},
|
||||
"SUBMIT": "إرسال الرسالة",
|
||||
"CANCEL": "إلغاء",
|
||||
@@ -231,7 +231,7 @@
|
||||
"FILTER_CONTACTS": "فلترة",
|
||||
"FILTER_CONTACTS_SAVE": "حفظ الفلتر",
|
||||
"FILTER_CONTACTS_DELETE": "حذف الفلتر",
|
||||
"FILTER_CONTACTS_EDIT": "تحرير الجزء",
|
||||
"FILTER_CONTACTS_EDIT": "Edit segment",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "جاري تحميل جهات الاتصال...",
|
||||
"404": "لا توجد جهات اتصال تطابق بحثك 🔍",
|
||||
@@ -298,8 +298,8 @@
|
||||
"BUTTON": "إضافة سمة خاصة",
|
||||
"NOT_AVAILABLE": "لا توجد سمات مخصصة متاحة لجهة الاتصال هذه.",
|
||||
"COPY_SUCCESSFUL": "تم النسخ إلى الحافظة بنجاح",
|
||||
"SHOW_MORE": "إظهار كافة السمات",
|
||||
"SHOW_LESS": "عرض سمات أقل",
|
||||
"SHOW_MORE": "Show all attributes",
|
||||
"SHOW_LESS": "Show less attributes",
|
||||
"ACTIONS": {
|
||||
"COPY": "نسخ السمة",
|
||||
"DELETE": "حذف السمة",
|
||||
@@ -350,7 +350,7 @@
|
||||
"VALIDATIONS": {
|
||||
"REQUIRED": "القيمة الصالحة مطلوبة",
|
||||
"INVALID_URL": "عنوان URL غير صالح",
|
||||
"INVALID_INPUT": "مدخلات غير صالح"
|
||||
"INVALID_INPUT": "Invalid Input"
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACTS": {
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
"CONTACTS_FILTER": {
|
||||
"TITLE": "تصفية جهات الاتصال",
|
||||
"SUBTITLE": "إضافة فلاتر أدناه واضغط على 'إرسال' لتصفية جهات الاتصال.",
|
||||
"EDIT_CUSTOM_SEGMENT": "تحرير الجزء",
|
||||
"CUSTOM_VIEWS_SUBTITLE": "أضف أو أزل الفلاتر وقم بتحديث المجلد الخاص بك.",
|
||||
"EDIT_CUSTOM_SEGMENT": "Edit Segment",
|
||||
"CUSTOM_VIEWS_SUBTITLE": "Add or remove filters and update your segment.",
|
||||
"ADD_NEW_FILTER": "إضافة فلتر",
|
||||
"CLEAR_ALL_FILTERS": "مسح جميع الفلاتر",
|
||||
"FILTER_DELETE_ERROR": "يجب ان يكون لديك فلتر واحد على الاقل",
|
||||
"SUBMIT_BUTTON_LABEL": "إرسال",
|
||||
"UPDATE_BUTTON_LABEL": "تحديث الجزء",
|
||||
"UPDATE_BUTTON_LABEL": "Update Segment",
|
||||
"CANCEL_BUTTON_LABEL": "إلغاء",
|
||||
"CLEAR_BUTTON_LABEL": "مسح الفلاتر",
|
||||
"EMPTY_VALUE_ERROR": "القيمة مطلوبة",
|
||||
"SEGMENT_LABEL": "اسم القسم",
|
||||
"SEGMENT_QUERY_LABEL": "استعلام الجزء",
|
||||
"SEGMENT_LABEL": "Segment Name",
|
||||
"SEGMENT_QUERY_LABEL": "Segment Query",
|
||||
"TOOLTIP_LABEL": "تصفية جهات الاتصال",
|
||||
"QUERY_DROPDOWN_LABELS": {
|
||||
"AND": "و",
|
||||
@@ -45,7 +45,7 @@
|
||||
"CREATED_AT": "تم إنشاؤها في",
|
||||
"LAST_ACTIVITY": "آخر نشاط",
|
||||
"REFERER_LINK": "رابط المرجع",
|
||||
"BLOCKED": "محظور"
|
||||
"BLOCKED": "Blocked"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "الفلاتر القياسية",
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
"NO_INBOX_AGENT": "يبدو أنه لم يتم إسنادك لأي قنوات تواصل بعد. الرجاء التواصل مع المدير لإضافتك لصناديق الوارد الخاصة بقنوات التواصل",
|
||||
"SEARCH_MESSAGES": "البحث عن رسائل في المحادثات",
|
||||
"EMPTY_STATE": {
|
||||
"CMD_BAR": "لفتح قائمة الأوامر",
|
||||
"KEYBOARD_SHORTCUTS": "لعرض اختصارات لوحة المفاتيح"
|
||||
"CMD_BAR": "to open command menu",
|
||||
"KEYBOARD_SHORTCUTS": "to view keyboard shortcuts"
|
||||
},
|
||||
"SEARCH": {
|
||||
"TITLE": "البحث في الرسائل",
|
||||
@@ -40,16 +40,16 @@
|
||||
"UNKNOWN_FILE_TYPE": "ملف غير معروف",
|
||||
"SAVE_CONTACT": "حفظ",
|
||||
"UPLOADING_ATTACHMENTS": "جاري تحميل المرفقات...",
|
||||
"REPLIED_TO_STORY": "رد على قصتك",
|
||||
"UNSUPPORTED_MESSAGE": "هذه الرسالة غير مدعومة.",
|
||||
"UNSUPPORTED_MESSAGE_FACEBOOK": "هذه الرسالة غير مدعومة، يمكنك مشاهدة هذه الرسالة على تطبيق فيسبوك (Messenger).",
|
||||
"UNSUPPORTED_MESSAGE_INSTAGRAM": "هذه الرسالة غير مدعومة، يمكنك عرض هذه الرسالة على تطبيق Instagram.",
|
||||
"REPLIED_TO_STORY": "Replied to your story",
|
||||
"UNSUPPORTED_MESSAGE": "This message is unsupported.",
|
||||
"UNSUPPORTED_MESSAGE_FACEBOOK": "This message is unsupported. You can view this message on the Facebook Messenger app.",
|
||||
"UNSUPPORTED_MESSAGE_INSTAGRAM": "This message is unsupported. You can view this message on the Instagram app.",
|
||||
"SUCCESS_DELETE_MESSAGE": "تم حذف الرسالة بنجاح",
|
||||
"FAIL_DELETE_MESSSAGE": "تعذر حذف الرسالة! حاول مرة أخرى",
|
||||
"NO_RESPONSE": "لا توجد استجابة",
|
||||
"RATING_TITLE": "التقييم",
|
||||
"FEEDBACK_TITLE": "الملاحظات",
|
||||
"REPLY_MESSAGE_NOT_FOUND": "الرسالة غير متوفرة",
|
||||
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
|
||||
"CARD": {
|
||||
"SHOW_LABELS": "إظهار السمات",
|
||||
"HIDE_LABELS": "إخفاء السمات"
|
||||
@@ -61,7 +61,7 @@
|
||||
"OPEN": "المزيد",
|
||||
"CLOSE": "أغلق",
|
||||
"DETAILS": "التفاصيل",
|
||||
"SNOOZED_UNTIL": "غفوة حتى",
|
||||
"SNOOZED_UNTIL": "Snoozed until",
|
||||
"SNOOZED_UNTIL_TOMORROW": "غفوة حتى الغد",
|
||||
"SNOOZED_UNTIL_NEXT_WEEK": "غفوة حتى الأسبوع القادم",
|
||||
"SNOOZED_UNTIL_NEXT_REPLY": "غفوة حتى الرد التالي",
|
||||
@@ -69,8 +69,8 @@
|
||||
"FRT": "FRT {status}",
|
||||
"NRT": "NRT {status}",
|
||||
"RT": "RT {status}",
|
||||
"MISSED": "مفقود",
|
||||
"DUE": "مستحق"
|
||||
"MISSED": "missed",
|
||||
"DUE": "due"
|
||||
}
|
||||
},
|
||||
"RESOLVE_DROPDOWN": {
|
||||
@@ -92,17 +92,17 @@
|
||||
"TITLE": "الأولوية",
|
||||
"OPTIONS": {
|
||||
"NONE": "لا شيء",
|
||||
"URGENT": "عاجل",
|
||||
"HIGH": "عالية",
|
||||
"MEDIUM": "متوسطة",
|
||||
"LOW": "منخفضة"
|
||||
"URGENT": "Urgent",
|
||||
"HIGH": "High",
|
||||
"MEDIUM": "Medium",
|
||||
"LOW": "Low"
|
||||
},
|
||||
"CHANGE_PRIORITY": {
|
||||
"SELECT_PLACEHOLDER": "لا شيء",
|
||||
"INPUT_PLACEHOLDER": "تحديد الأولوية",
|
||||
"INPUT_PLACEHOLDER": "Select priority",
|
||||
"NO_RESULTS": "لم يتم العثور على النتائج",
|
||||
"SUCCESSFUL": "تغيير أولوية معرف المحادثة %{conversationId} إلى %{priority}",
|
||||
"FAILED": "تعذر تغيير الأولوية، الرجاء المحاولة مرة أخرى."
|
||||
"SUCCESSFUL": "Changed priority of conversation id %{conversationId} to %{priority}",
|
||||
"FAILED": "Couldn't change priority. Please try again."
|
||||
}
|
||||
},
|
||||
"CARD_CONTEXT_MENU": {
|
||||
@@ -150,8 +150,8 @@
|
||||
"SEND": "إرسال",
|
||||
"CREATE": "إضافة ملاحظة",
|
||||
"INSERT_READ_MORE": "اقرأ المزيد",
|
||||
"DISMISS_REPLY": "تجاهل الرد",
|
||||
"REPLYING_TO": "الرد على:",
|
||||
"DISMISS_REPLY": "Dismiss reply",
|
||||
"REPLYING_TO": "Replying to:",
|
||||
"TIP_FORMAT_ICON": "عرض محرر النصوص",
|
||||
"TIP_EMOJI_ICON": "إظهار قائمة الرموز التعبيرية",
|
||||
"TIP_ATTACH_ICON": "إرفاق الملفات",
|
||||
@@ -163,7 +163,7 @@
|
||||
"STOP_AUDIO_RECORDING": "إيقاف التسجيل الصوتي",
|
||||
"": "",
|
||||
"EMAIL_HEAD": {
|
||||
"TO": "إلى",
|
||||
"TO": "TO",
|
||||
"ADD_BCC": "إضافة bcc",
|
||||
"CC": {
|
||||
"LABEL": "CC",
|
||||
@@ -206,15 +206,15 @@
|
||||
},
|
||||
"CONTEXT_MENU": {
|
||||
"COPY": "نسخ",
|
||||
"REPLY_TO": "الرد على هذه الرسالة",
|
||||
"REPLY_TO": "Reply to this message",
|
||||
"DELETE": "حذف",
|
||||
"CREATE_A_CANNED_RESPONSE": "إضافة إلى الردود السريعة",
|
||||
"TRANSLATE": "ترجم",
|
||||
"COPY_PERMALINK": "نسخ الرابط إلى الرسالة",
|
||||
"LINK_COPIED": "تم نسخ عنوان URL للرسالة إلى الحافظة",
|
||||
"COPY_PERMALINK": "Copy link to the message",
|
||||
"LINK_COPIED": "Message URL copied to the clipboard",
|
||||
"DELETE_CONFIRMATION": {
|
||||
"TITLE": "هل أنت متأكد من أنك تريد حذف هذه الرسالة؟",
|
||||
"MESSAGE": "لا يمكنك التراجع عن هذا الإجراء",
|
||||
"TITLE": "Are you sure you want to delete this message?",
|
||||
"MESSAGE": "You cannot undo this action",
|
||||
"DELETE": "حذف",
|
||||
"CANCEL": "إلغاء"
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"DATE_PICKER": {
|
||||
"APPLY_BUTTON": "تطبيق",
|
||||
"CLEAR_BUTTON": "مسح",
|
||||
"CLEAR_BUTTON": "Clear",
|
||||
"DATE_RANGE_INPUT": {
|
||||
"START": "تاريخ البدء",
|
||||
"END": "تاريخ الانتهاء"
|
||||
"START": "Start Date",
|
||||
"END": "End Date"
|
||||
},
|
||||
"DATE_RANGE_OPTIONS": {
|
||||
"TITLE": "نطاق التاريخ",
|
||||
"TITLE": "DATE RANGE",
|
||||
"LAST_7_DAYS": "آخر 7 أيام",
|
||||
"LAST_30_DAYS": "آخر 30 يوماً",
|
||||
"LAST_3_MONTHS": "آخر 3 أشهر",
|
||||
|
||||
@@ -50,8 +50,8 @@
|
||||
},
|
||||
"UPDATE_CHATWOOT": "يتوفر تحديث %{latestChatwootVersion} لـ Chatwoot. الرجاء التحديث.",
|
||||
"LEARN_MORE": "اعرف المزيد",
|
||||
"PAYMENT_PENDING": "الدفعة الخاصة بك معلقة. الرجاء تحديث معلومات الدفع الخاصة بك للاستمرار في استخدام Chatwoot",
|
||||
"LIMITS_UPGRADE": "لقد تجاوز حسابك حدود الاستخدام، يرجى ترقية خطتك للاستمرار في استخدام Chatwoot",
|
||||
"PAYMENT_PENDING": "Your payment is pending. Please update your payment information to continue using Chatwoot",
|
||||
"LIMITS_UPGRADE": "Your account has exceeded the usage limits, please upgrade your plan to continue using Chatwoot",
|
||||
"OPEN_BILLING": "فتح الفواتير"
|
||||
},
|
||||
"FORMS": {
|
||||
@@ -88,16 +88,14 @@
|
||||
"assigned_conversation_new_message": "رسالة جديدة",
|
||||
"participating_conversation_new_message": "رسالة جديدة",
|
||||
"conversation_mention": "إشارة",
|
||||
"sla_missed_first_response": "فشل اتفاقية مستوى الخدمة",
|
||||
"sla_missed_next_response": "فشل اتفاقية مستوى الخدمة",
|
||||
"sla_missed_resolution": "فشل اتفاقية مستوى الخدمة"
|
||||
"sla_missed_first_response": "SLA Missed",
|
||||
"sla_missed_next_response": "SLA Missed",
|
||||
"sla_missed_resolution": "SLA Missed"
|
||||
}
|
||||
},
|
||||
"NETWORK": {
|
||||
"NOTIFICATION": {
|
||||
"OFFLINE": "غير متصل",
|
||||
"RECONNECTING": "Reconnecting...",
|
||||
"RECONNECT_SUCCESS": "Reconnected"
|
||||
"OFFLINE": "غير متصل"
|
||||
},
|
||||
"BUTTON": {
|
||||
"REFRESH": "تحديث"
|
||||
@@ -109,7 +107,7 @@
|
||||
"GENERAL": "عام",
|
||||
"REPORTS": "التقارير",
|
||||
"CONVERSATION": "المحادثات",
|
||||
"BULK_ACTIONS": "الإجراءات الشاملة",
|
||||
"BULK_ACTIONS": "Bulk Actions",
|
||||
"CHANGE_ASSIGNEE": "تغيير المحال إليه",
|
||||
"CHANGE_PRIORITY": "تغيير الأولوية",
|
||||
"CHANGE_TEAM": "تغيير الفريق",
|
||||
@@ -117,7 +115,7 @@
|
||||
"ADD_LABEL": "إضافة تسمية إلى المحادثة",
|
||||
"REMOVE_LABEL": "إزالة التسمية من المحادثة",
|
||||
"SETTINGS": "الإعدادات",
|
||||
"AI_ASSIST": "مساعدة AI",
|
||||
"AI_ASSIST": "AI Assist",
|
||||
"APPEARANCE": "مظهر",
|
||||
"SNOOZE_NOTIFICATION": "تأجيل التنبيهات"
|
||||
},
|
||||
@@ -141,7 +139,7 @@
|
||||
"GO_TO_NOTIFICATIONS": "الذهاب إلى الإشعارات",
|
||||
"ADD_LABELS_TO_CONVERSATION": "إضافة تسمية إلى المحادثة",
|
||||
"ASSIGN_AN_AGENT": "تعيين وكيل",
|
||||
"AI_ASSIST": "مساعدة AI",
|
||||
"AI_ASSIST": "AI Assist",
|
||||
"ASSIGN_PRIORITY": "تعيين الأولوية",
|
||||
"ASSIGN_A_TEAM": "تعيين فريق",
|
||||
"MUTE_CONVERSATION": "كتم المحادثة",
|
||||
@@ -156,9 +154,9 @@
|
||||
"UNTIL_TOMORROW": "حتى الغد",
|
||||
"UNTIL_NEXT_MONTH": "حتي الشهر القادم",
|
||||
"AN_HOUR_FROM_NOW": "حتي ساعة من الأن",
|
||||
"UNTIL_CUSTOM_TIME": "مخصص...",
|
||||
"CUSTOM": "مخصص...",
|
||||
"CHANGE_APPEARANCE": "تغيير المظهر",
|
||||
"LIGHT_MODE": "فاتح",
|
||||
"LIGHT_MODE": "Light",
|
||||
"DARK_MODE": "مظلم",
|
||||
"SYSTEM_MODE": "نظام",
|
||||
"SNOOZE_NOTIFICATION": "تأجيل التنبيهات"
|
||||
|
||||
@@ -456,7 +456,7 @@
|
||||
"BACK_RESULTS": "Back to results"
|
||||
},
|
||||
"UPGRADE_PAGE": {
|
||||
"TITLE": "مركز المساعدة",
|
||||
"TITLE": "Help Center",
|
||||
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
|
||||
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
|
||||
"BUTTON": {
|
||||
|
||||
@@ -2,63 +2,63 @@
|
||||
"INBOX": {
|
||||
"LIST": {
|
||||
"TITLE": "صندوق الوارد",
|
||||
"DISPLAY_DROPDOWN": "عرض",
|
||||
"LOADING": "جارٍ جلب الإشعارات",
|
||||
"404": "لا توجد إشعارات نشطة في هذه المجموعة.",
|
||||
"NO_NOTIFICATIONS": "لا توجد إشعارات",
|
||||
"NOTE": "الإشعارات من جميع صناديق الوارد المشترك بها",
|
||||
"NO_MESSAGES_AVAILABLE": "عذرًا! لا يمكن جلب الرسائل",
|
||||
"SNOOZED_UNTIL": "غفوة حتى",
|
||||
"DISPLAY_DROPDOWN": "Display",
|
||||
"LOADING": "Fetching notifications",
|
||||
"404": "There are no active notifications in this group.",
|
||||
"NO_NOTIFICATIONS": "No notifications",
|
||||
"NOTE": "Notifications from all subscribed inboxes",
|
||||
"NO_MESSAGES_AVAILABLE": "Oops! Not able to fetch messages",
|
||||
"SNOOZED_UNTIL": "Snoozed until",
|
||||
"SNOOZED_UNTIL_TOMORROW": "غفوة حتى الغد",
|
||||
"SNOOZED_UNTIL_NEXT_WEEK": "غفوة حتى الأسبوع القادم"
|
||||
},
|
||||
"ACTION_HEADER": {
|
||||
"SNOOZE": "غفوة الإشعار",
|
||||
"DELETE": "حذف الإشعار",
|
||||
"SNOOZE": "Snooze notification",
|
||||
"DELETE": "Delete notification",
|
||||
"BACK": "العودة"
|
||||
},
|
||||
"TYPES": {
|
||||
"CONVERSATION_MENTION": "تم ذكرك في محادثة",
|
||||
"CONVERSATION_CREATION": "تم إنشاء محادثة جديدة",
|
||||
"CONVERSATION_ASSIGNMENT": "تم تعيين محادثة لك",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "رسالة جديدة في محادثة معينة لك",
|
||||
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "رسالة جديدة في محادثة تشارك فيها",
|
||||
"SLA_MISSED_FIRST_RESPONSE": "تم تجاوز الهدف الأول للرد للمحادثة",
|
||||
"SLA_MISSED_NEXT_RESPONSE": "تم تجاوز الهدف التالي للرد للمحادثة",
|
||||
"SLA_MISSED_RESOLUTION": "تم تجاوز الهدف النهائي للحل للمحادثة"
|
||||
"CONVERSATION_MENTION": "You have been mentioned in a conversation",
|
||||
"CONVERSATION_CREATION": "New conversation created",
|
||||
"CONVERSATION_ASSIGNMENT": "A conversation has been assigned to you",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "New message in an assigned conversation",
|
||||
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "New message in a conversation you are participating in",
|
||||
"SLA_MISSED_FIRST_RESPONSE": "SLA target first response missed for conversation",
|
||||
"SLA_MISSED_NEXT_RESPONSE": "SLA target next response missed for conversation",
|
||||
"SLA_MISSED_RESOLUTION": "SLA target resolution missed for conversation"
|
||||
},
|
||||
"MENU_ITEM": {
|
||||
"MARK_AS_READ": "وضع علامة كمقروء",
|
||||
"MARK_AS_READ": "Mark as read",
|
||||
"MARK_AS_UNREAD": "وضع علامة كغير مقروء",
|
||||
"SNOOZE": "غفوة",
|
||||
"DELETE": "حذف",
|
||||
"MARK_ALL_READ": "تحديد الكل كمقروء",
|
||||
"DELETE_ALL": "حذف الكل",
|
||||
"DELETE_ALL_READ": "حذف كل المقروء"
|
||||
"DELETE_ALL": "Delete all",
|
||||
"DELETE_ALL_READ": "Delete all read"
|
||||
},
|
||||
"DISPLAY_MENU": {
|
||||
"SORT": "فرز",
|
||||
"DISPLAY": "عرض :",
|
||||
"SORT": "Sort",
|
||||
"DISPLAY": "Display :",
|
||||
"SORT_OPTIONS": {
|
||||
"NEWEST": "الأحدث",
|
||||
"OLDEST": "الأقدم",
|
||||
"NEWEST": "Newest",
|
||||
"OLDEST": "Oldest",
|
||||
"PRIORITY": "الأولوية"
|
||||
},
|
||||
"DISPLAY_OPTIONS": {
|
||||
"SNOOZED": "غفوة",
|
||||
"READ": "قراءة",
|
||||
"READ": "قرائة",
|
||||
"LABELS": "الوسوم",
|
||||
"CONVERSATION_ID": "معرف المحادثة"
|
||||
"CONVERSATION_ID": "Conversation ID"
|
||||
}
|
||||
},
|
||||
"ALERTS": {
|
||||
"MARK_AS_READ": "تم وضع علامة الإشعار كمقروء",
|
||||
"MARK_AS_UNREAD": "تم وضع علامة الإشعار كغير مقروء",
|
||||
"SNOOZE": "تم تأجيل الإشعار",
|
||||
"DELETE": "تم حذف الإشعار",
|
||||
"MARK_ALL_READ": "تم وضع علامة كل الإشعارات كمقروء",
|
||||
"DELETE_ALL": "تم حذف كل الإشعارات",
|
||||
"DELETE_ALL_READ": "تم حذف كل الإشعارات المقروءة"
|
||||
"MARK_AS_READ": "Notification marked as read",
|
||||
"MARK_AS_UNREAD": "Notification marked as unread",
|
||||
"SNOOZE": "Notification snoozed",
|
||||
"DELETE": "Notification deleted",
|
||||
"MARK_ALL_READ": "All notifications marked as read",
|
||||
"DELETE_ALL": "All notifications deleted",
|
||||
"DELETE_ALL_READ": "All read notifications deleted"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
"INBOX_MGMT": {
|
||||
"HEADER": "قنوات التواصل",
|
||||
"SIDEBAR_TXT": "<p><b>قنوات التواصل</b></p> <p> عند ربطك لموقع ويب أو صفحة فيسبوك إلى Chatwoot، يتم تسميتها <b>قناة تواصل</b>. يمكنك إنشاء قنوات تواصل غير محدودة من مختلف الأنواع في حساب Chatwoot الخاص بك. </p><p> انقر فوق <b>إضافة قناة تواصل</b> لربط موقع الويب أو صفحة فيسبوك الخاصة بك. </p><p> من لوحة الإدارة، يمكنك رؤية جميع المحادثات من جميع صناديق الوارد الخاصة بك والرد عليها من مكان موّحد عبر الضغط على علامة التبويب \"المحادثات\". </p><p> يمكنك أيضًا مشاهدة المحادثات الخاصة بصندوق وارد معين بالنقر على اسم صندوق الوارد على الجزء الجانبي من لوحة الإدارة. </p>",
|
||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||
"LIST": {
|
||||
"404": "لا توجد صناديق وارد لقنوات تواصل مرتبطة بهذا الحساب."
|
||||
},
|
||||
@@ -366,7 +364,6 @@
|
||||
"TITLE": "Microsoft Email",
|
||||
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"SIGN_IN": "Sign in with Microsoft",
|
||||
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
|
||||
}
|
||||
@@ -521,7 +518,7 @@
|
||||
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
|
||||
},
|
||||
"HELP_CENTER": {
|
||||
"LABEL": "مركز المساعدة",
|
||||
"LABEL": "Help Center",
|
||||
"PLACEHOLDER": "Select Help Center",
|
||||
"SELECT_PLACEHOLDER": "Select Help Center",
|
||||
"REMOVE": "Remove Help Center",
|
||||
|
||||
@@ -209,87 +209,6 @@
|
||||
"API_SUCCESS": "تم حذف تطبيق لوحة التحكم بنجاح",
|
||||
"API_ERROR": "لم نتمكن من حذف التطبيق. الرجاء المحاولة مرة أخرى لاحقاً"
|
||||
}
|
||||
},
|
||||
"LINEAR": {
|
||||
"ADD_OR_LINK_BUTTON": "Create/Link Linear Issue",
|
||||
"LOADING": "Fetching linear issues...",
|
||||
"LOADING_ERROR": "There was an error fetching the linear issues, please try again",
|
||||
"CREATE": "إنشاء",
|
||||
"LINK": {
|
||||
"SEARCH": "Search issues",
|
||||
"SELECT": "Select issue",
|
||||
"TITLE": "الرابط",
|
||||
"EMPTY_LIST": "No linear issues found",
|
||||
"LOADING": "جار التحميل",
|
||||
"ERROR": "There was an error fetching the linear issues, please try again",
|
||||
"LINK_SUCCESS": "Issue linked successfully",
|
||||
"LINK_ERROR": "There was an error linking the issue, please try again",
|
||||
"LINK_TITLE": "Conversation (#%{conversationId}) with %{name}"
|
||||
},
|
||||
"ADD_OR_LINK": {
|
||||
"TITLE": "Create/link linear issue",
|
||||
"DESCRIPTION": "Create Linear issues from conversations, or link existing ones for seamless tracking.",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "العنوان",
|
||||
"PLACEHOLDER": "Enter title",
|
||||
"REQUIRED_ERROR": "العنوان مطلوب"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "الوصف",
|
||||
"PLACEHOLDER": "Enter description"
|
||||
},
|
||||
"TEAM": {
|
||||
"LABEL": "الفريق",
|
||||
"PLACEHOLDER": "اختيار فريق",
|
||||
"SEARCH": "Search team",
|
||||
"REQUIRED_ERROR": "Team is required"
|
||||
},
|
||||
"ASSIGNEE": {
|
||||
"LABEL": "Assignee",
|
||||
"PLACEHOLDER": "Select assignee",
|
||||
"SEARCH": "Search assignee"
|
||||
},
|
||||
"PRIORITY": {
|
||||
"LABEL": "الأولوية",
|
||||
"PLACEHOLDER": "تحديد الأولوية",
|
||||
"SEARCH": "Search priority"
|
||||
},
|
||||
"LABEL": {
|
||||
"LABEL": "الوسم",
|
||||
"PLACEHOLDER": "Select label",
|
||||
"SEARCH": "Search label"
|
||||
},
|
||||
"STATUS": {
|
||||
"LABEL": "الحالة",
|
||||
"PLACEHOLDER": "Select status",
|
||||
"SEARCH": "Search status"
|
||||
},
|
||||
"PROJECT": {
|
||||
"LABEL": "Project",
|
||||
"PLACEHOLDER": "Select project",
|
||||
"SEARCH": "Search project"
|
||||
}
|
||||
},
|
||||
"CREATE": "إنشاء",
|
||||
"CANCEL": "إلغاء",
|
||||
"CREATE_SUCCESS": "Issue created successfully",
|
||||
"CREATE_ERROR": "There was an error creating the issue, please try again",
|
||||
"LOADING_TEAM_ERROR": "There was an error fetching the teams, please try again",
|
||||
"LOADING_TEAM_ENTITIES_ERROR": "There was an error fetching the team entities, please try again"
|
||||
},
|
||||
"ISSUE": {
|
||||
"STATUS": "الحالة",
|
||||
"PRIORITY": "الأولوية",
|
||||
"ASSIGNEE": "Assignee",
|
||||
"LABELS": "الوسوم",
|
||||
"CREATED_AT": "Created at %{createdAt}"
|
||||
},
|
||||
"UNLINK": {
|
||||
"TITLE": "Unlink",
|
||||
"SUCCESS": "Issue unlinked successfully",
|
||||
"ERROR": "There was an error unlinking the issue, please try again"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,7 +517,7 @@
|
||||
"DOWNLOAD_SLA_REPORTS": "Download SLA reports",
|
||||
"DOWNLOAD_FAILED": "Failed to download SLA Reports",
|
||||
"DROPDOWN": {
|
||||
"ADD_FIlTER": "إضافة فلتر",
|
||||
"ADD_FIlTER": "Add filter",
|
||||
"CLEAR_ALL": "Clear all",
|
||||
"CLEAR_FILTER": "Clear filter",
|
||||
"EMPTY_LIST": "لم يتم العثور على النتائج",
|
||||
|
||||
@@ -278,9 +278,9 @@
|
||||
"SLA": "SLA",
|
||||
"BETA": "تجريبي",
|
||||
"REPORTS_OVERVIEW": "نظرة عامة",
|
||||
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
|
||||
"FACEBOOK_REAUTHORIZE": "انتهت صلاحية اتصال الفيسبوك الخاص بك، يرجى إعادة الاتصال بصفحة الفيسبوك الخاصة بك لمواصلة الخدمات",
|
||||
"HELP_CENTER": {
|
||||
"TITLE": "مركز المساعدة",
|
||||
"TITLE": "Help Center",
|
||||
"ALL_ARTICLES": "جميع المقالات",
|
||||
"MY_ARTICLES": "مقالاتي",
|
||||
"DRAFT": "مسودة",
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"NONE": "Нито един",
|
||||
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
|
||||
"ASSIGN_SELECTED_TEAMS": "Assign selected team.",
|
||||
"ASSIGN_SUCCESFUL": "Teams assigned successfully.",
|
||||
"ASSIGN_SUCCESFUL": "Teams assiged successfully.",
|
||||
"ASSIGN_FAILED": "Failed to assign team. Please try again."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +95,7 @@
|
||||
},
|
||||
"NETWORK": {
|
||||
"NOTIFICATION": {
|
||||
"OFFLINE": "Offline",
|
||||
"RECONNECTING": "Reconnecting...",
|
||||
"RECONNECT_SUCCESS": "Reconnected"
|
||||
"OFFLINE": "Offline"
|
||||
},
|
||||
"BUTTON": {
|
||||
"REFRESH": "Refresh"
|
||||
@@ -156,7 +154,7 @@
|
||||
"UNTIL_TOMORROW": "Until tomorrow",
|
||||
"UNTIL_NEXT_MONTH": "Until next month",
|
||||
"AN_HOUR_FROM_NOW": "Until an hour from now",
|
||||
"UNTIL_CUSTOM_TIME": "Custom...",
|
||||
"CUSTOM": "Custom...",
|
||||
"CHANGE_APPEARANCE": "Change Appearance",
|
||||
"LIGHT_MODE": "Light",
|
||||
"DARK_MODE": "Dark",
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
"INBOX_MGMT": {
|
||||
"HEADER": "Inboxes",
|
||||
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>",
|
||||
"RECONNECTION_REQUIRED": "Your inbox is disconnected. You won't receive new messages until you reauthorize it.",
|
||||
"CLICK_TO_RECONNECT": "Click here to reconnect.",
|
||||
"LIST": {
|
||||
"404": "There are no inboxes attached to this account."
|
||||
},
|
||||
@@ -366,7 +364,6 @@
|
||||
"TITLE": "Microsoft Email",
|
||||
"DESCRIPTION": "Click on the Sign in with Microsoft button to get started. You will redirected to the email sign in page. Once you accept the requested permissions, you would be redirected back to the inbox creation step.",
|
||||
"EMAIL_PLACEHOLDER": "Enter email address",
|
||||
"SIGN_IN": "Sign in with Microsoft",
|
||||
"HELP": "To add your Microsoft account as a channel, you need to authenticate your Microsoft account by clicking on 'Sign in with Microsoft' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Microsoft, please try again"
|
||||
}
|
||||
|
||||
@@ -209,87 +209,6 @@
|
||||
"API_SUCCESS": "Dashboard app deleted successfully",
|
||||
"API_ERROR": "We couldn't delete the app. Please try again later"
|
||||
}
|
||||
},
|
||||
"LINEAR": {
|
||||
"ADD_OR_LINK_BUTTON": "Create/Link Linear Issue",
|
||||
"LOADING": "Fetching linear issues...",
|
||||
"LOADING_ERROR": "There was an error fetching the linear issues, please try again",
|
||||
"CREATE": "Създаване",
|
||||
"LINK": {
|
||||
"SEARCH": "Search issues",
|
||||
"SELECT": "Select issue",
|
||||
"TITLE": "Link",
|
||||
"EMPTY_LIST": "No linear issues found",
|
||||
"LOADING": "Loading",
|
||||
"ERROR": "There was an error fetching the linear issues, please try again",
|
||||
"LINK_SUCCESS": "Issue linked successfully",
|
||||
"LINK_ERROR": "There was an error linking the issue, please try again",
|
||||
"LINK_TITLE": "Conversation (#%{conversationId}) with %{name}"
|
||||
},
|
||||
"ADD_OR_LINK": {
|
||||
"TITLE": "Create/link linear issue",
|
||||
"DESCRIPTION": "Create Linear issues from conversations, or link existing ones for seamless tracking.",
|
||||
"FORM": {
|
||||
"TITLE": {
|
||||
"LABEL": "Title",
|
||||
"PLACEHOLDER": "Enter title",
|
||||
"REQUIRED_ERROR": "Title is required"
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Описание",
|
||||
"PLACEHOLDER": "Enter description"
|
||||
},
|
||||
"TEAM": {
|
||||
"LABEL": "Team",
|
||||
"PLACEHOLDER": "Изберете екип",
|
||||
"SEARCH": "Search team",
|
||||
"REQUIRED_ERROR": "Team is required"
|
||||
},
|
||||
"ASSIGNEE": {
|
||||
"LABEL": "Assignee",
|
||||
"PLACEHOLDER": "Select assignee",
|
||||
"SEARCH": "Search assignee"
|
||||
},
|
||||
"PRIORITY": {
|
||||
"LABEL": "Priority",
|
||||
"PLACEHOLDER": "Select priority",
|
||||
"SEARCH": "Search priority"
|
||||
},
|
||||
"LABEL": {
|
||||
"LABEL": "Label",
|
||||
"PLACEHOLDER": "Select label",
|
||||
"SEARCH": "Search label"
|
||||
},
|
||||
"STATUS": {
|
||||
"LABEL": "Статус",
|
||||
"PLACEHOLDER": "Select status",
|
||||
"SEARCH": "Search status"
|
||||
},
|
||||
"PROJECT": {
|
||||
"LABEL": "Project",
|
||||
"PLACEHOLDER": "Select project",
|
||||
"SEARCH": "Search project"
|
||||
}
|
||||
},
|
||||
"CREATE": "Създаване",
|
||||
"CANCEL": "Отмени",
|
||||
"CREATE_SUCCESS": "Issue created successfully",
|
||||
"CREATE_ERROR": "There was an error creating the issue, please try again",
|
||||
"LOADING_TEAM_ERROR": "There was an error fetching the teams, please try again",
|
||||
"LOADING_TEAM_ENTITIES_ERROR": "There was an error fetching the team entities, please try again"
|
||||
},
|
||||
"ISSUE": {
|
||||
"STATUS": "Статус",
|
||||
"PRIORITY": "Priority",
|
||||
"ASSIGNEE": "Assignee",
|
||||
"LABELS": "Етикети",
|
||||
"CREATED_AT": "Created at %{createdAt}"
|
||||
},
|
||||
"UNLINK": {
|
||||
"TITLE": "Unlink",
|
||||
"SUCCESS": "Issue unlinked successfully",
|
||||
"ERROR": "There was an error unlinking the issue, please try again"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@
|
||||
"SLA": "SLA",
|
||||
"BETA": "Beta",
|
||||
"REPORTS_OVERVIEW": "Overview",
|
||||
"REAUTHORIZE": "Your inbox connection has expired, please reconnect\n to continue receiving and sending messages",
|
||||
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
|
||||
"HELP_CENTER": {
|
||||
"TITLE": "Help Center",
|
||||
"ALL_ARTICLES": "All Articles",
|
||||
|
||||
@@ -2,67 +2,67 @@
|
||||
"AGENT_BOTS": {
|
||||
"HEADER": "Bots",
|
||||
"LOADING_EDITOR": "S'està carregant l'editor...",
|
||||
"HEADER_BTN_TXT": "Afegir configuració del bot",
|
||||
"SIDEBAR_TXT": "<p><b>Bots d'Agent</b> <p>Bots d'Agent són com els membres més fabulosos del teu equip. Poden gestionar les coses petites, de manera que us podeu centrar en les coses importants. Prova-ls.</p> <p> Pots gestionar els teus bots des d'aquesta pàgina o crear-ne de nous mitjançant el botó \"Afegeix una configuració de bot\".</p> <p> Obre el <a href=\"https:/ /www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677497472-how-to-use-agent-bots\" target=\"blank\">Manual de robots d'agent</a> en una altra pestanya per a un cop de mà.</p>",
|
||||
"HEADER_BTN_TXT": "Add bot configuration",
|
||||
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.</p> <p> You can manage your bots from this page or create new ones using the 'Add bot configuraton' button.</p> <p> Open the <a href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677497472-how-to-use-agent-bots\" target=\"blank\">Agent bots handbook</a> in another tab for a helping hand.</p>",
|
||||
"CSML_BOT_EDITOR": {
|
||||
"NAME": {
|
||||
"LABEL": "Nom del bot",
|
||||
"PLACEHOLDER": "Anomena el teu bot.",
|
||||
"ERROR": "El nom del bot és obligatori."
|
||||
"LABEL": "Bot name",
|
||||
"PLACEHOLDER": "Name your bot.",
|
||||
"ERROR": "Bot name is required."
|
||||
},
|
||||
"DESCRIPTION": {
|
||||
"LABEL": "Descripció del bot",
|
||||
"PLACEHOLDER": "Què fa aquest bot?"
|
||||
"LABEL": "Bot description",
|
||||
"PLACEHOLDER": "What does this bot do?"
|
||||
},
|
||||
"BOT_CONFIG": {
|
||||
"ERROR": "Introdueix la configuració del bot CSML més amunt.",
|
||||
"API_ERROR": "La vostra configuració CSML no és vàlida. Arregla-ho i torna-ho a provar."
|
||||
"ERROR": "Please enter your CSML bot configuration above.",
|
||||
"API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
|
||||
},
|
||||
"SUBMIT": "Valida i desa"
|
||||
"SUBMIT": "Validate and save"
|
||||
},
|
||||
"BOT_CONFIGURATION": {
|
||||
"TITLE": "Selecciona un bot d'agent",
|
||||
"DESC": "Assigna un bot d'agent a la teva safata d'entrada. Poden gestionar les converses inicials i transferir-les a un agent en directe quan sigui necessari.",
|
||||
"TITLE": "Select an agent bot",
|
||||
"DESC": "Assign an Agent Bot to your inbox. They can handle initial conversations and transfer them to a live agent when necessary.",
|
||||
"SUBMIT": "Actualitza",
|
||||
"DISCONNECT": "Desconnecta el bot",
|
||||
"SUCCESS_MESSAGE": "S'ha actualitzat correctament el bot de l'agent.",
|
||||
"DISCONNECTED_SUCCESS_MESSAGE": "S'ha desconnectat correctament el bot de l'agent.",
|
||||
"ERROR_MESSAGE": "No s'ha pogut actualitzar el bot de l'agent. Torneu-ho a provar.",
|
||||
"DISCONNECTED_ERROR_MESSAGE": "No s'ha pogut desconnectar el bot de l'agent. Torneu-ho a provar.",
|
||||
"SELECT_PLACEHOLDER": "Selecciona el bot"
|
||||
"DISCONNECT": "Disconnect bot",
|
||||
"SUCCESS_MESSAGE": "Successfully updated the agent bot.",
|
||||
"DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot.",
|
||||
"ERROR_MESSAGE": "Could not update the agent bot. Please try again.",
|
||||
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot. Please try again.",
|
||||
"SELECT_PLACEHOLDER": "Select bot"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Configura el nou bot",
|
||||
"TITLE": "Configure new bot",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel·la",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Bot afegit correctament.",
|
||||
"ERROR_MESSAGE": "No s'ha pogut afegir el bot. Torneu-ho a provar més endavant."
|
||||
"SUCCESS_MESSAGE": "Bot added successfully.",
|
||||
"ERROR_MESSAGE": "Could not add bot. Please try again later."
|
||||
}
|
||||
},
|
||||
"LIST": {
|
||||
"404": "No s'han trobat bots. Pots crear un bot fent clic al botó \"Configura un bot nou\" ↗",
|
||||
"LOADING": "S'estan obtenint bots...",
|
||||
"TYPE": "Tipus de bot"
|
||||
"404": "No bots found. You can create a bot by clicking the 'Configure new bot' button ↗",
|
||||
"LOADING": "Fetching bots...",
|
||||
"TYPE": "Bot type"
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Esborrar",
|
||||
"TITLE": "Suprimeix el bot",
|
||||
"TITLE": "Delete bot",
|
||||
"SUBMIT": "Esborrar",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel·la",
|
||||
"DESCRIPTION": "Estàs segur que vols suprimir aquest bot? Aquesta acció és irreversible.",
|
||||
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "S'ha esborrat el bot correctament.",
|
||||
"ERROR_MESSAGE": "No s'ha pogut eliminar el bot. Torneu-ho a provar més endavant."
|
||||
"SUCCESS_MESSAGE": "Bot deleted successfully.",
|
||||
"ERROR_MESSAGE": "Could not delete bot. Please try again."
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"BUTTON_TEXT": "Edita",
|
||||
"LOADING": "S'estan obtenint bots...",
|
||||
"TITLE": "Edita el bot",
|
||||
"LOADING": "Fetching bots...",
|
||||
"TITLE": "Edit bot",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel·la",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Bot actualitzat correctament.",
|
||||
"ERROR_MESSAGE": "No s'ha pogut actualitzar el bot. Torneu-ho a provar."
|
||||
"SUCCESS_MESSAGE": "Bot updated successfully.",
|
||||
"ERROR_MESSAGE": "Could not update bot. Please try again."
|
||||
}
|
||||
},
|
||||
"TYPES": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user