Compare commits

..
Author SHA1 Message Date
Shivam Mishra ff6c9dcc67 feat: update message 2024-07-26 14:20:52 +05:30
Shivam Mishra 63b9b9fcc1 feat: add console.warn for action cable ignore 2024-07-26 14:20:45 +05:30
Pranav 924d3ef2df Merge branch 'develop' into deploy-apr-15 2024-07-10 20:29:13 -07:00
Pranav 4ef06530e5 Merge branch 'develop' into deploy-apr-15 2024-07-10 20:28:53 -07:00
Pranav 3f10e89dba Merge branch 'develop' into deploy-apr-15 2024-07-08 19:47:10 -07:00
Pranav 184325dcae Remove channel auth error 2024-05-23 11:18:19 -07:00
Pranav ce2a4f5c34 Fix minor issues 2024-05-22 21:01:26 -07:00
Pranav 7c2f4ec334 Merge branch 'develop' into deploy-apr-15 2024-05-22 18:18:55 -07:00
Pranav a69894e0fc Reset data 2024-05-15 20:52:08 -07:00
Pranav 6f36a02519 Fix missing short summary 2024-05-15 20:45:11 -07:00
Pranav 54e697ec51 Fix report average 2024-05-15 20:43:06 -07:00
Pranav ee044e8602 Fix timezone issues 2024-05-15 20:42:55 -07:00
Pranav 3b7421221c new_report -> conversation_report 2024-05-15 20:42:43 -07:00
Pranav bc9327c09f Revert changes unrelated to performance 2024-05-15 20:42:32 -07:00
Pranav ac71dacc61 Update records 2024-05-15 20:41:44 -07:00
Pranav 072c607062 Update 2024-05-15 20:41:08 -07:00
Pranav 4b67066027 Add range filter 2024-05-15 20:38:01 -07:00
Pranav 08834b6016 Update report controller to use the new reports builder 2024-05-15 20:37:54 -07:00
PranavandPranav cbdc5b1ab0 chore: Temporarily disable the disconnect logic for inboxes (#9321)
* chore: Temporarily disable the disconnect logic for inboxes

* Remove unused spec
2024-05-09 19:01:49 -07:00
Pranav 25c6adbd07 Update the duplicate count event 2024-05-09 19:00:23 -07:00
Pranav d552b41fac fix: Terminate IMAP connection after the job is complete (#9225) 2024-04-15 17:43:18 -07:00
Pranav a315321c4f Fix the issues with dropdown 2024-04-15 17:42:12 -07:00
Pranav 2e768e2042 inbox view 2024-02-13 19:12:56 -08:00
Pranav bd481ad314 Update title 2024-02-13 18:39:59 -08:00
Pranav b90737ee5d capitalize 2024-02-13 18:36:50 -08:00
Pranav 6e57e76358 update copy 2024-02-13 18:31:39 -08:00
Pranav 75998945a5 Add agent reports 2024-02-13 18:31:07 -08:00
Pranav cc80e4122a update team reports 2024-02-13 18:16:52 -08:00
Pranav 8aa0a347d6 commit 2 2024-02-13 17:32:57 -08:00
Pranav 2a34ceb66b Commit 2024-02-13 17:32:31 -08:00
Pranav db30a05b19 Update heatmap report 2024-02-13 15:23:46 -08:00
Pranav 73fcb91ca3 Update filter properly 2024-02-13 14:49:49 -08:00
Pranav bcd5633d2a Filter for live conversations 2024-02-13 14:36:04 -08:00
Pranav 1a272e7a18 Remove CSV Safe 2024-02-13 12:32:02 -08:00
Pranav 7d4374a3bf Added a section for group by teams 2024-02-13 12:26:48 -08:00
Pranav 2617d9b2b7 Merge branch 'develop' into report-rewrite 2024-02-13 10:13:10 -08:00
Pranav 375d216d12 Re-write live-conversations API 2024-02-10 18:41:11 -08:00
761 changed files with 6856 additions and 16198 deletions
-2
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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();
+2 -2
View File
@@ -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;
});
},
+3 -2
View File
@@ -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
@@ -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);
}
@@ -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;
@@ -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;
}
@@ -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
);
});
});
});
+3 -12
View File
@@ -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