Merge branch 'release/1.21.0'
This commit is contained in:
commit
c831bee0e3
703 changed files with 22272 additions and 2715 deletions
3
.bundler-audit.yml
Normal file
3
.bundler-audit.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
ignore:
|
||||
- CVE-2021-41098 # https://github.com/chatwoot/chatwoot/issues/3097 (update once azure blob storage is updated)
|
|
@ -14,6 +14,10 @@ plugins:
|
|||
checks:
|
||||
similar-code:
|
||||
enabled: false
|
||||
method-count:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 25
|
||||
exclude_patterns:
|
||||
- "spec/"
|
||||
- "**/specs/"
|
||||
|
|
|
@ -100,6 +100,9 @@ FB_VERIFY_TOKEN=
|
|||
FB_APP_SECRET=
|
||||
FB_APP_ID=
|
||||
|
||||
# https://developers.facebook.com/docs/messenger-platform/instagram/get-started#app-dashboard
|
||||
IG_VERIFY_TOKEN=
|
||||
|
||||
# Twitter
|
||||
# documentation: https://www.chatwoot.com/docs/twitter-app-setup
|
||||
TWITTER_APP_ID=
|
||||
|
@ -113,7 +116,7 @@ SLACK_CLIENT_SECRET=
|
|||
|
||||
### Change this env variable only if you are using a custom build mobile app
|
||||
## Mobile app env variables
|
||||
IOS_APP_ID=6C953F3RX2.com.chatwoot.app
|
||||
IOS_APP_ID=L7YLMN4634.com.chatwoot.app
|
||||
ANDROID_BUNDLE_ID=com.chatwoot.app
|
||||
|
||||
# https://developers.google.com/android/guides/client-auth (use keytool to print the fingerprint in the first section)
|
||||
|
@ -166,7 +169,7 @@ USE_INBOX_AVATAR_FOR_BOT=true
|
|||
|
||||
## Rack Attack configuration
|
||||
## To prevent and throttle abusive requests
|
||||
# ENABLE_RACK_ATTACK=false
|
||||
# ENABLE_RACK_ATTACK=true
|
||||
|
||||
|
||||
## Running chatwoot as an API only server
|
||||
|
|
11
Gemfile
11
Gemfile
|
@ -56,7 +56,6 @@ gem 'activerecord-import'
|
|||
gem 'dotenv-rails'
|
||||
gem 'foreman'
|
||||
gem 'puma'
|
||||
gem 'rack-timeout'
|
||||
gem 'webpacker', '~> 5.x'
|
||||
# metrics on heroku
|
||||
gem 'barnes'
|
||||
|
@ -122,6 +121,11 @@ gem 'hairtrigger'
|
|||
|
||||
gem 'procore-sift'
|
||||
|
||||
group :production, :staging do
|
||||
# we dont want request timing out in development while using byebug
|
||||
gem 'rack-timeout'
|
||||
end
|
||||
|
||||
group :development do
|
||||
gem 'annotate'
|
||||
gem 'bullet'
|
||||
|
@ -143,6 +147,11 @@ group :test do
|
|||
end
|
||||
|
||||
group :development, :test do
|
||||
# TODO: is this needed ?
|
||||
# errors thrown by devise password gem
|
||||
gem 'flay'
|
||||
gem 'rspec'
|
||||
# for error thrown by devise password gem
|
||||
gem 'active_record_query_trace'
|
||||
gem 'bundle-audit', require: false
|
||||
gem 'byebug', platform: :mri
|
||||
|
|
175
Gemfile.lock
175
Gemfile.lock
|
@ -90,21 +90,21 @@ GEM
|
|||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.2)
|
||||
attr_extras (6.2.4)
|
||||
aws-eventstream (1.1.1)
|
||||
aws-partitions (1.482.0)
|
||||
aws-sdk-core (3.119.0)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.513.0)
|
||||
aws-sdk-core (3.121.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
aws-sdk-kms (1.46.0)
|
||||
aws-sdk-core (~> 3, >= 3.119.0)
|
||||
aws-sdk-kms (1.49.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.98.0)
|
||||
aws-sdk-core (~> 3, >= 3.119.0)
|
||||
aws-sdk-s3 (1.103.0)
|
||||
aws-sdk-core (~> 3, >= 3.120.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.4)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (1.4.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-storage-blob (2.0.1)
|
||||
azure-storage-common (~> 2.0)
|
||||
|
@ -119,28 +119,28 @@ GEM
|
|||
statsd-ruby (~> 1.1)
|
||||
bcrypt (3.1.16)
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.7.7)
|
||||
bootsnap (1.9.1)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (5.1.1)
|
||||
browser (5.3.1)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.4)
|
||||
bullet (6.1.5)
|
||||
activesupport (>= 3.0.0)
|
||||
uniform_notifier (~> 1.11)
|
||||
bundle-audit (0.1.0)
|
||||
bundler-audit
|
||||
bundler-audit (0.8.0)
|
||||
bundler-audit (0.9.0.1)
|
||||
bundler (>= 1.2.0, < 3)
|
||||
thor (~> 1.0)
|
||||
byebug (11.1.3)
|
||||
coderay (1.1.3)
|
||||
commonmarker (0.22.0)
|
||||
commonmarker (0.23.2)
|
||||
concurrent-ruby (1.1.9)
|
||||
connection_pool (2.2.5)
|
||||
crack (0.4.5)
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
cypress-on-rails (1.10.1)
|
||||
cypress-on-rails (1.11.0)
|
||||
rack
|
||||
database_cleaner (2.0.1)
|
||||
database_cleaner-active_record (~> 2.0.0)
|
||||
|
@ -150,7 +150,7 @@ GEM
|
|||
database_cleaner-core (2.0.1)
|
||||
datetime_picker_rails (0.0.7)
|
||||
momentjs-rails (>= 2.8.1)
|
||||
ddtrace (0.51.1)
|
||||
ddtrace (0.53.0)
|
||||
ffi (~> 1.0)
|
||||
msgpack
|
||||
declarative (0.0.20)
|
||||
|
@ -174,12 +174,13 @@ GEM
|
|||
dotenv-rails (2.7.6)
|
||||
dotenv (= 2.7.6)
|
||||
railties (>= 3.2)
|
||||
down (5.2.3)
|
||||
down (5.2.4)
|
||||
addressable (~> 2.8)
|
||||
ecma-re-validator (0.3.0)
|
||||
regexp_parser (~> 2.0)
|
||||
erubi (1.10.0)
|
||||
et-orbi (1.2.4)
|
||||
erubis (2.7.0)
|
||||
et-orbi (1.2.5)
|
||||
tzinfo
|
||||
execjs (2.8.1)
|
||||
facebook-messenger (2.0.1)
|
||||
|
@ -190,7 +191,7 @@ GEM
|
|||
factory_bot_rails (6.2.0)
|
||||
factory_bot (~> 6.2.0)
|
||||
railties (>= 5.0.0)
|
||||
faker (2.18.0)
|
||||
faker (2.19.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
|
@ -198,10 +199,15 @@ GEM
|
|||
faraday (~> 1.0)
|
||||
fcm (1.0.3)
|
||||
faraday (~> 1)
|
||||
ffi (1.15.3)
|
||||
ffi (1.15.4)
|
||||
flag_shih_tzu (0.3.23)
|
||||
flay (2.12.1)
|
||||
erubis (~> 2.7.0)
|
||||
path_expander (~> 1.0)
|
||||
ruby_parser (~> 3.0)
|
||||
sexp_processor (~> 4.0)
|
||||
foreman (0.87.2)
|
||||
fugit (1.5.0)
|
||||
fugit (1.5.2)
|
||||
et-orbi (~> 1.1, >= 1.1.8)
|
||||
raabro (~> 1.4)
|
||||
gapic-common (0.3.4)
|
||||
|
@ -210,7 +216,7 @@ GEM
|
|||
googleapis-common-protos-types (>= 1.0.4, < 2.0)
|
||||
googleauth (~> 0.9)
|
||||
grpc (~> 1.25)
|
||||
geocoder (1.6.7)
|
||||
geocoder (1.7.0)
|
||||
gli (2.20.1)
|
||||
globalid (0.5.2)
|
||||
activesupport (>= 5.0)
|
||||
|
@ -223,9 +229,9 @@ GEM
|
|||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
webrick
|
||||
google-apis-iamcredentials_v1 (0.6.0)
|
||||
google-apis-iamcredentials_v1 (0.7.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-apis-storage_v1 (0.6.0)
|
||||
google-apis-storage_v1 (0.8.0)
|
||||
google-apis-core (>= 0.4, < 2.a)
|
||||
google-cloud-core (1.6.0)
|
||||
google-cloud-env (~> 1.0)
|
||||
|
@ -238,7 +244,7 @@ GEM
|
|||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.5.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.1.0)
|
||||
google-cloud-errors (1.2.0)
|
||||
google-cloud-storage (1.34.1)
|
||||
addressable (~> 2.5)
|
||||
digest-crc (~> 0.4)
|
||||
|
@ -247,28 +253,32 @@ GEM
|
|||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
google-protobuf (3.17.3-universal-darwin)
|
||||
google-protobuf (3.17.3-x86_64-linux)
|
||||
googleapis-common-protos (1.3.11)
|
||||
google-protobuf (3.18.1)
|
||||
google-protobuf (3.18.1-universal-darwin)
|
||||
google-protobuf (3.18.1-x86_64-linux)
|
||||
googleapis-common-protos (1.3.12)
|
||||
google-protobuf (~> 3.14)
|
||||
googleapis-common-protos-types (>= 1.0.6, < 2.0)
|
||||
googleapis-common-protos-types (~> 1.2)
|
||||
grpc (~> 1.27)
|
||||
googleapis-common-protos-types (1.1.0)
|
||||
googleapis-common-protos-types (1.2.0)
|
||||
google-protobuf (~> 3.14)
|
||||
googleauth (0.17.0)
|
||||
googleauth (0.17.1)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.14)
|
||||
signet (~> 0.15)
|
||||
groupdate (5.2.2)
|
||||
activesupport (>= 5)
|
||||
grpc (1.38.0-universal-darwin)
|
||||
google-protobuf (~> 3.15)
|
||||
grpc (1.41.0)
|
||||
google-protobuf (~> 3.17)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.38.0-x86_64-linux)
|
||||
google-protobuf (~> 3.15)
|
||||
grpc (1.41.0-universal-darwin)
|
||||
google-protobuf (~> 3.17)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
grpc (1.41.0-x86_64-linux)
|
||||
google-protobuf (~> 3.17)
|
||||
googleapis-common-protos-types (~> 1.0)
|
||||
haikunator (1.1.1)
|
||||
hairtrigger (0.2.24)
|
||||
|
@ -282,7 +292,7 @@ GEM
|
|||
http-accept (1.7.0)
|
||||
http-cookie (1.0.4)
|
||||
domain_name (~> 0.5)
|
||||
httparty (0.18.1)
|
||||
httparty (0.20.0)
|
||||
mime-types (~> 3.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
|
@ -306,7 +316,7 @@ GEM
|
|||
hana (~> 1.3)
|
||||
regexp_parser (~> 2.0)
|
||||
uri_template (~> 0.7)
|
||||
jwt (2.2.3)
|
||||
jwt (2.3.0)
|
||||
kaminari (1.2.1)
|
||||
activesupport (>= 4.1.0)
|
||||
kaminari-actionview (= 1.2.1)
|
||||
|
@ -327,9 +337,9 @@ GEM
|
|||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
line-bot-api (1.21.0)
|
||||
liquid (5.0.1)
|
||||
listen (3.6.0)
|
||||
line-bot-api (1.22.0)
|
||||
liquid (5.1.0)
|
||||
listen (3.7.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.12.0)
|
||||
|
@ -337,17 +347,18 @@ GEM
|
|||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
marcel (1.0.1)
|
||||
marcel (1.0.2)
|
||||
maxminddb (0.1.22)
|
||||
memoist (0.16.2)
|
||||
method_source (1.0.0)
|
||||
mime-types (3.3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2021.0704)
|
||||
mime-types-data (3.2021.0901)
|
||||
mini_magick (4.11.0)
|
||||
mini_mime (1.1.1)
|
||||
mini_mime (1.1.2)
|
||||
mini_portile2 (2.5.3)
|
||||
minitest (5.14.4)
|
||||
mock_redis (0.28.0)
|
||||
mock_redis (0.29.0)
|
||||
ruby2_keywords
|
||||
momentjs-rails (2.20.1)
|
||||
railties (>= 3.1)
|
||||
|
@ -358,8 +369,11 @@ GEM
|
|||
net-http-persistent (4.0.1)
|
||||
connection_pool (~> 2.2)
|
||||
netrc (0.11.0)
|
||||
newrelic_rpm (7.2.0)
|
||||
newrelic_rpm (8.0.0)
|
||||
nio4r (2.5.8)
|
||||
nokogiri (1.11.7)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.11.7-x86_64-darwin)
|
||||
|
@ -369,9 +383,10 @@ GEM
|
|||
oauth (0.5.6)
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.1)
|
||||
parallel (1.20.1)
|
||||
parallel (1.21.0)
|
||||
parser (3.0.2.0)
|
||||
ast (~> 2.4.1)
|
||||
path_expander (1.1.0)
|
||||
pg (1.2.3)
|
||||
procore-sift (0.16.0)
|
||||
rails (> 4.2.0)
|
||||
|
@ -381,9 +396,9 @@ GEM
|
|||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.6)
|
||||
puma (5.4.0)
|
||||
puma (5.5.1)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
pundit (2.1.1)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.5.2)
|
||||
|
@ -415,7 +430,7 @@ GEM
|
|||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.4.1)
|
||||
rails-html-sanitizer (1.4.2)
|
||||
loofah (~> 2.3)
|
||||
railties (6.1.4.1)
|
||||
actionpack (= 6.1.4.1)
|
||||
|
@ -446,6 +461,10 @@ GEM
|
|||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
rexml (3.2.5)
|
||||
rspec (3.10.0)
|
||||
rspec-core (~> 3.10.0)
|
||||
rspec-expectations (~> 3.10.0)
|
||||
rspec-mocks (~> 3.10.0)
|
||||
rspec-core (3.10.1)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-expectations (3.10.1)
|
||||
|
@ -454,7 +473,7 @@ GEM
|
|||
rspec-mocks (3.10.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.10.0)
|
||||
rspec-rails (5.0.1)
|
||||
rspec-rails (5.0.2)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
railties (>= 5.2)
|
||||
|
@ -463,35 +482,34 @@ GEM
|
|||
rspec-mocks (~> 3.10)
|
||||
rspec-support (~> 3.10)
|
||||
rspec-support (3.10.2)
|
||||
rubocop (1.18.4)
|
||||
rubocop (1.22.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.0.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml
|
||||
rubocop-ast (>= 1.8.0, < 2.0)
|
||||
rubocop-ast (>= 1.12.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.8.0)
|
||||
rubocop-ast (1.12.0)
|
||||
parser (>= 3.0.1.1)
|
||||
rubocop-performance (1.11.4)
|
||||
rubocop-performance (1.11.5)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
rubocop-rails (2.11.3)
|
||||
rubocop-rails (2.12.3)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-rspec (2.4.0)
|
||||
rubocop (~> 1.0)
|
||||
rubocop-ast (>= 1.1.0)
|
||||
rubocop-rspec (2.5.0)
|
||||
rubocop (~> 1.19)
|
||||
ruby-progressbar (1.11.0)
|
||||
ruby-vips (2.1.2)
|
||||
ruby-vips (2.1.3)
|
||||
ffi (~> 1.12)
|
||||
ruby2_keywords (0.0.5)
|
||||
ruby2ruby (2.4.4)
|
||||
ruby_parser (~> 3.1)
|
||||
sexp_processor (~> 4.6)
|
||||
ruby_parser (3.16.0)
|
||||
ruby_parser (3.17.0)
|
||||
sexp_processor (~> 4.15, >= 4.15.1)
|
||||
sassc (2.4.0)
|
||||
ffi (~> 1.9)
|
||||
|
@ -501,38 +519,38 @@ GEM
|
|||
sprockets (> 3.0)
|
||||
sprockets-rails
|
||||
tilt
|
||||
scout_apm (4.1.1)
|
||||
scout_apm (4.1.2)
|
||||
parser
|
||||
seed_dump (3.3.1)
|
||||
activerecord (>= 4)
|
||||
activesupport (>= 4)
|
||||
selectize-rails (0.12.6)
|
||||
semantic_range (3.0.0)
|
||||
sentry-rails (4.6.4)
|
||||
sentry-rails (4.7.3)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby-core (~> 4.6.0)
|
||||
sentry-ruby (4.6.4)
|
||||
sentry-ruby-core (~> 4.7.0)
|
||||
sentry-ruby (4.7.3)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
faraday (>= 1.0)
|
||||
sentry-ruby-core (= 4.6.4)
|
||||
sentry-ruby-core (4.6.4)
|
||||
sentry-ruby-core (= 4.7.3)
|
||||
sentry-ruby-core (4.7.3)
|
||||
concurrent-ruby
|
||||
faraday
|
||||
sentry-sidekiq (4.6.4)
|
||||
sentry-ruby-core (~> 4.6.0)
|
||||
sentry-sidekiq (4.7.3)
|
||||
sentry-ruby-core (~> 4.7.0)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.15.3)
|
||||
shoulda-matchers (5.0.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (6.2.1)
|
||||
sidekiq (6.2.2)
|
||||
connection_pool (>= 2.2.2)
|
||||
rack (~> 2.0)
|
||||
redis (>= 4.2.0)
|
||||
sidekiq-cron (1.2.0)
|
||||
fugit (~> 1.1)
|
||||
sidekiq (>= 4.2.1)
|
||||
signet (0.15.0)
|
||||
addressable (~> 2.3)
|
||||
signet (0.16.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
|
@ -575,15 +593,15 @@ GEM
|
|||
oauth
|
||||
tzinfo (2.0.4)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2021.1)
|
||||
tzinfo-data (1.2021.3)
|
||||
tzinfo (>= 1.0.0)
|
||||
uber (0.1.0)
|
||||
uglifier (4.2.0)
|
||||
execjs (>= 0.3.0, < 3)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (2.0.0)
|
||||
unf_ext (0.0.8)
|
||||
unicode-display_width (2.1.0)
|
||||
uniform_notifier (1.14.2)
|
||||
uri_template (0.7.0)
|
||||
valid_email2 (4.0.0)
|
||||
|
@ -596,11 +614,11 @@ GEM
|
|||
activemodel (>= 6.0.0)
|
||||
bindex (>= 0.4.0)
|
||||
railties (>= 6.0.0)
|
||||
webmock (3.13.0)
|
||||
addressable (>= 2.3.6)
|
||||
webmock (3.14.0)
|
||||
addressable (>= 2.8.0)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webpacker (5.4.0)
|
||||
webpacker (5.4.3)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
|
@ -617,6 +635,7 @@ GEM
|
|||
|
||||
PLATFORMS
|
||||
arm64-darwin-20
|
||||
ruby
|
||||
x86_64-darwin-18
|
||||
x86_64-darwin-20
|
||||
x86_64-darwin-21
|
||||
|
@ -652,6 +671,7 @@ DEPENDENCIES
|
|||
faker
|
||||
fcm
|
||||
flag_shih_tzu
|
||||
flay
|
||||
foreman
|
||||
geocoder
|
||||
google-cloud-dialogflow
|
||||
|
@ -687,6 +707,7 @@ DEPENDENCIES
|
|||
redis-namespace
|
||||
responders
|
||||
rest-client
|
||||
rspec
|
||||
rspec-rails (~> 5.0.0)
|
||||
rubocop
|
||||
rubocop-performance
|
||||
|
|
|
@ -42,7 +42,7 @@ class ContactMergeAction
|
|||
end
|
||||
|
||||
def merge_and_remove_mergee_contact
|
||||
mergable_attribute_keys = %w[identifier name email phone_number custom_attributes]
|
||||
mergable_attribute_keys = %w[identifier name email phone_number additional_attributes custom_attributes]
|
||||
base_contact_attributes = base_contact.attributes.slice(*mergable_attribute_keys).compact_blank
|
||||
mergee_contact_attributes = mergee_contact.attributes.slice(*mergable_attribute_keys).compact_blank
|
||||
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
# based on this we are showing "not sent from chatwoot" message in frontend
|
||||
# Hence there is no need to set user_id in message for outgoing echo messages.
|
||||
|
||||
class Messages::Facebook::MessageBuilder
|
||||
class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
attr_reader :response
|
||||
|
||||
def initialize(response, inbox, outgoing_echo: false)
|
||||
super()
|
||||
@response = response
|
||||
@inbox = inbox
|
||||
@outgoing_echo = outgoing_echo
|
||||
|
@ -47,30 +48,12 @@ class Messages::Facebook::MessageBuilder
|
|||
|
||||
def build_message
|
||||
@message = conversation.messages.create!(message_params)
|
||||
|
||||
@attachments.each do |attachment|
|
||||
process_attachment(attachment)
|
||||
end
|
||||
end
|
||||
|
||||
def process_attachment(attachment)
|
||||
return if attachment['type'].to_sym == :template
|
||||
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
attachment_file = Down.download(
|
||||
file_url
|
||||
)
|
||||
attachment.file.attach(
|
||||
io: attachment_file,
|
||||
filename: attachment_file.original_filename,
|
||||
content_type: attachment_file.content_type
|
||||
)
|
||||
end
|
||||
|
||||
def ensure_contact_avatar
|
||||
return if contact_params[:remote_avatar_url].blank?
|
||||
return if @contact.avatar.attached?
|
||||
|
@ -89,28 +72,6 @@ class Messages::Facebook::MessageBuilder
|
|||
))
|
||||
end
|
||||
|
||||
def attachment_params(attachment)
|
||||
file_type = attachment['type'].to_sym
|
||||
params = { file_type: file_type, account_id: @message.account_id }
|
||||
|
||||
if [:image, :file, :audio, :video].include? file_type
|
||||
params.merge!(file_type_params(attachment))
|
||||
elsif file_type == :location
|
||||
params.merge!(location_params(attachment))
|
||||
elsif file_type == :fallback
|
||||
params.merge!(fallback_params(attachment))
|
||||
end
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
def file_type_params(attachment)
|
||||
{
|
||||
external_url: attachment['payload']['url'],
|
||||
remote_file_url: attachment['payload']['url']
|
||||
}
|
||||
end
|
||||
|
||||
def location_params(attachment)
|
||||
lat = attachment['payload']['coordinates']['lat']
|
||||
long = attachment['payload']['coordinates']['long']
|
||||
|
@ -167,7 +128,7 @@ class Messages::Facebook::MessageBuilder
|
|||
result = {}
|
||||
# OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user
|
||||
# We don't need to capture this error as we don't care about contact params in case of echo messages
|
||||
Sentry.capture_exception(e) unless outgoing_echo?
|
||||
Sentry.capture_exception(e) unless @outgoing_echo
|
||||
rescue StandardError => e
|
||||
result = {}
|
||||
Sentry.capture_exception(e)
|
||||
|
|
150
app/builders/messages/instagram/message_builder.rb
Normal file
150
app/builders/messages/instagram/message_builder.rb
Normal file
|
@ -0,0 +1,150 @@
|
|||
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
|
||||
# Assumptions
|
||||
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
|
||||
# based on this we are showing "not sent from chatwoot" message in frontend
|
||||
# Hence there is no need to set user_id in message for outgoing echo messages.
|
||||
|
||||
class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||
attr_reader :messaging
|
||||
|
||||
def initialize(messaging, inbox, outgoing_echo: false)
|
||||
super()
|
||||
@messaging = messaging
|
||||
@inbox = inbox
|
||||
@outgoing_echo = outgoing_echo
|
||||
end
|
||||
|
||||
def perform
|
||||
return if @inbox.channel.reauthorization_required?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
build_message
|
||||
end
|
||||
rescue Koala::Facebook::AuthenticationError
|
||||
@inbox.channel.authorization_error!
|
||||
raise
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attachments
|
||||
@messaging[:message][:attachments] || {}
|
||||
end
|
||||
|
||||
def message_type
|
||||
@outgoing_echo ? :outgoing : :incoming
|
||||
end
|
||||
|
||||
def message_source_id
|
||||
@outgoing_echo ? recipient_id : sender_id
|
||||
end
|
||||
|
||||
def sender_id
|
||||
@messaging[:sender][:id]
|
||||
end
|
||||
|
||||
def recipient_id
|
||||
@messaging[:recipient][:id]
|
||||
end
|
||||
|
||||
def message
|
||||
@messaging[:message]
|
||||
end
|
||||
|
||||
def contact
|
||||
@contact ||= @inbox.contact_inboxes.find_by(source_id: message_source_id)&.contact
|
||||
end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
||||
end
|
||||
|
||||
def message_content
|
||||
@messaging[:message][:text]
|
||||
end
|
||||
|
||||
def content_attributes
|
||||
{ message_id: @messaging[:message][:mid] }
|
||||
end
|
||||
|
||||
def build_message
|
||||
return if @outgoing_echo && already_sent_from_chatwoot?
|
||||
|
||||
@message = conversation.messages.create!(message_params)
|
||||
|
||||
attachments.each do |attachment|
|
||||
process_attachment(attachment)
|
||||
end
|
||||
end
|
||||
|
||||
def build_conversation
|
||||
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: message_source_id)
|
||||
Conversation.create!(conversation_params.merge(
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
))
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: contact.id,
|
||||
additional_attributes: {
|
||||
type: 'instagram_direct_message'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: message_type,
|
||||
source_id: message_source_id,
|
||||
content: message_content,
|
||||
content_attributes: content_attributes,
|
||||
sender: @outgoing_echo ? nil : contact
|
||||
}
|
||||
end
|
||||
|
||||
def already_sent_from_chatwoot?
|
||||
cw_message = conversation.messages.where(
|
||||
source_id: nil,
|
||||
message_type: 'outgoing',
|
||||
content: message_content,
|
||||
private: false,
|
||||
status: :sent
|
||||
).first
|
||||
cw_message.update(content_attributes: content_attributes) if cw_message.present?
|
||||
cw_message.present?
|
||||
end
|
||||
|
||||
### Sample response
|
||||
# {
|
||||
# "object": "instagram",
|
||||
# "entry": [
|
||||
# {
|
||||
# "id": "<IGID>",// ig id of the business
|
||||
# "time": 1569262486134,
|
||||
# "messaging": [
|
||||
# {
|
||||
# "sender": {
|
||||
# "id": "<IGSID>"
|
||||
# },
|
||||
# "recipient": {
|
||||
# "id": "<IGID>"
|
||||
# },
|
||||
# "timestamp": 1569262485349,
|
||||
# "message": {
|
||||
# "mid": "<MESSAGE_ID>",
|
||||
# "text": "<MESSAGE_CONTENT>"
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ],
|
||||
# }
|
||||
end
|
|
@ -16,6 +16,7 @@ class Messages::MessageBuilder
|
|||
def perform
|
||||
@message = @conversation.messages.build(message_params)
|
||||
process_attachments
|
||||
process_emails
|
||||
@message.save!
|
||||
@message
|
||||
end
|
||||
|
@ -34,6 +35,16 @@ class Messages::MessageBuilder
|
|||
end
|
||||
end
|
||||
|
||||
def process_emails
|
||||
return unless @conversation.inbox&.inbox_type == 'Email'
|
||||
|
||||
cc_emails = @params[:cc_emails].split(',') if @params[:cc_emails]
|
||||
bcc_emails = @params[:bcc_emails].split(',') if @params[:bcc_emails]
|
||||
|
||||
@message.content_attributes[:cc_emails] = cc_emails
|
||||
@message.content_attributes[:bcc_emails] = bcc_emails
|
||||
end
|
||||
|
||||
def message_type
|
||||
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
|
||||
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
|
||||
|
|
42
app/builders/messages/messenger/message_builder.rb
Normal file
42
app/builders/messages/messenger/message_builder.rb
Normal file
|
@ -0,0 +1,42 @@
|
|||
class Messages::Messenger::MessageBuilder
|
||||
def process_attachment(attachment)
|
||||
return if attachment['type'].to_sym == :template
|
||||
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
attachment_file = Down.download(
|
||||
file_url
|
||||
)
|
||||
attachment.file.attach(
|
||||
io: attachment_file,
|
||||
filename: attachment_file.original_filename,
|
||||
content_type: attachment_file.content_type
|
||||
)
|
||||
end
|
||||
|
||||
def attachment_params(attachment)
|
||||
file_type = attachment['type'].to_sym
|
||||
params = { file_type: file_type, account_id: @message.account_id }
|
||||
|
||||
if [:image, :file, :audio, :video].include? file_type
|
||||
params.merge!(file_type_params(attachment))
|
||||
elsif file_type == :location
|
||||
params.merge!(location_params(attachment))
|
||||
elsif file_type == :fallback
|
||||
params.merge!(fallback_params(attachment))
|
||||
end
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
def file_type_params(attachment)
|
||||
{
|
||||
external_url: attachment['payload']['url'],
|
||||
remote_file_url: attachment['payload']['url']
|
||||
}
|
||||
end
|
||||
end
|
|
@ -41,19 +41,25 @@ class V2::ReportBuilder
|
|||
user
|
||||
when :label
|
||||
label
|
||||
when :team
|
||||
team
|
||||
end
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= account.inboxes.where(id: params[:id]).first
|
||||
@inbox ||= account.inboxes.find(params[:id])
|
||||
end
|
||||
|
||||
def user
|
||||
@user ||= account.users.where(id: params[:id]).first
|
||||
@user ||= account.users.find(params[:id])
|
||||
end
|
||||
|
||||
def label
|
||||
@label ||= account.labels.where(id: params[:id]).first
|
||||
@label ||= account.labels.find(params[:id])
|
||||
end
|
||||
|
||||
def team
|
||||
@team ||= account.teams.find(params[:id])
|
||||
end
|
||||
|
||||
def conversations_count
|
||||
|
@ -62,15 +68,14 @@ class V2::ReportBuilder
|
|||
.count
|
||||
end
|
||||
|
||||
# unscoped removes all scopes added to a model previously
|
||||
def incoming_messages_count
|
||||
scope.messages.unscoped.where(account_id: account.id).incoming
|
||||
scope.messages.incoming.unscope(:order)
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
scope.messages.unscoped.where(account_id: account.id).outgoing
|
||||
scope.messages.outgoing.unscope(:order)
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
|
|
@ -9,21 +9,18 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||
@agents = agents
|
||||
end
|
||||
|
||||
def create; end
|
||||
|
||||
def update
|
||||
@agent.update!(agent_params.slice(:name).compact)
|
||||
@agent.current_account_user.update!(agent_params.slice(:role, :availability, :auto_offline).compact)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@agent.current_account_user.destroy
|
||||
head :ok
|
||||
end
|
||||
|
||||
def update
|
||||
@agent.update!(agent_params.except(:role))
|
||||
@agent.current_account_user.update!(role: agent_params[:role]) if agent_params[:role]
|
||||
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @agent }
|
||||
end
|
||||
|
||||
def create
|
||||
render partial: 'api/v1/models/agent.json.jbuilder', locals: { resource: @user }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
|
@ -47,22 +44,25 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def save_account_user
|
||||
AccountUser.create!(
|
||||
AccountUser.create!({
|
||||
account_id: Current.account.id,
|
||||
user_id: @user.id,
|
||||
role: new_agent_params[:role],
|
||||
inviter_id: current_user.id
|
||||
)
|
||||
}.merge({
|
||||
role: new_agent_params[:role],
|
||||
availability: new_agent_params[:availability],
|
||||
auto_offline: new_agent_params[:auto_offline]
|
||||
}.compact))
|
||||
end
|
||||
|
||||
def agent_params
|
||||
params.require(:agent).permit(:email, :name, :role)
|
||||
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline)
|
||||
end
|
||||
|
||||
def new_agent_params
|
||||
# intial string ensures the password requirements are met
|
||||
temp_password = "1!aA#{SecureRandom.alphanumeric(12)}"
|
||||
params.require(:agent).permit(:email, :name, :role)
|
||||
params.require(:agent).permit(:email, :name, :role, :availability, :auto_offline)
|
||||
.merge!(password: temp_password, password_confirmation: temp_password, inviter: current_user)
|
||||
end
|
||||
|
||||
|
|
|
@ -12,9 +12,10 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
|||
page_access_token: page_access_token
|
||||
)
|
||||
@facebook_inbox = Current.account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
||||
set_instagram_id(page_access_token, facebook_channel)
|
||||
set_avatar(@facebook_inbox, page_id)
|
||||
rescue StandardError => e
|
||||
Rails.logger.info e
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -22,6 +23,15 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
|||
@page_details = mark_already_existing_facebook_pages(fb_object.get_connections('me', 'accounts'))
|
||||
end
|
||||
|
||||
def set_instagram_id(page_access_token, facebook_channel)
|
||||
fb_object = Koala::Facebook::API.new(page_access_token)
|
||||
response = fb_object.get_connections('me', '', { fields: 'instagram_business_account' })
|
||||
return if response['instagram_business_account'].blank?
|
||||
|
||||
instagram_id = response['instagram_business_account']['id']
|
||||
facebook_channel.update(instagram_id: instagram_id)
|
||||
end
|
||||
|
||||
# get params[:inbox_id], current_account. params[:omniauth_token]
|
||||
def reauthorize_page
|
||||
if @inbox&.facebook?
|
||||
|
@ -45,8 +55,13 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController
|
|||
|
||||
def update_fb_page(fb_page_id, access_token)
|
||||
fb_page = get_fb_page(fb_page_id)
|
||||
fb_page&.update!(user_access_token: @user_access_token, page_access_token: access_token)
|
||||
fb_page&.reauthorized!
|
||||
ActiveRecord::Base.transaction do
|
||||
fb_page&.update!(user_access_token: @user_access_token, page_access_token: access_token)
|
||||
set_instagram_id(access_token, fb_page)
|
||||
fb_page&.reauthorized!
|
||||
rescue StandardError => e
|
||||
Sentry.capture_exception(e)
|
||||
end
|
||||
end
|
||||
|
||||
def get_fb_page(fb_page_id)
|
||||
|
|
|
@ -28,7 +28,7 @@ class Api::V1::Accounts::CampaignsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def campaign_params
|
||||
params.require(:campaign).permit(:title, :description, :message, :enabled, :inbox_id, :sender_id,
|
||||
params.require(:campaign).permit(:title, :description, :message, :enabled, :trigger_only_during_business_hours, :inbox_id, :sender_id,
|
||||
:scheduled_at, audience: [:type, :id], trigger_rules: {})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,7 +10,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
|
||||
before_action :check_authorization
|
||||
before_action :set_current_page, only: [:index, :active, :search]
|
||||
before_action :fetch_contact, only: [:show, :update, :contactable_inboxes]
|
||||
before_action :fetch_contact, only: [:show, :update, :destroy, :contactable_inboxes]
|
||||
before_action :set_include_contact_inboxes, only: [:index, :search]
|
||||
|
||||
def index
|
||||
|
@ -30,10 +30,13 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
end
|
||||
|
||||
def import
|
||||
render json: { error: I18n.t('errors.contacts.import.failed') }, status: :unprocessable_entity and return if params[:import_file].blank?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
import = Current.account.data_imports.create!(data_type: 'contacts')
|
||||
import.import_file.attach(params[:import_file])
|
||||
end
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
@ -70,6 +73,18 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
if ::OnlineStatusTracker.get_presence(
|
||||
@contact.account.id, 'Contact', @contact.id
|
||||
)
|
||||
return render_error({ message: I18n.t('contacts.online.delete', contact_name: @contact.name.capitalize) },
|
||||
:unprocessable_entity)
|
||||
end
|
||||
|
||||
@contact.destroy!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# TODO: Move this to a finder class
|
||||
|
@ -134,4 +149,8 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
def fetch_contact
|
||||
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
|
||||
end
|
||||
|
||||
def render_error(error, error_status)
|
||||
render json: error, status: error_status
|
||||
end
|
||||
end
|
||||
|
|
|
@ -69,6 +69,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
|
||||
def update_last_seen
|
||||
@conversation.agent_last_seen_at = DateTime.now.utc
|
||||
@conversation.assignee_last_seen_at = DateTime.now.utc if assignee?
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
def custom_attributes
|
||||
@conversation.custom_attributes = params.permit(custom_attributes: {})[:custom_attributes]
|
||||
@conversation.save!
|
||||
end
|
||||
|
||||
|
@ -112,6 +118,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
|
||||
def conversation_params
|
||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||
custom_attributes = params[:custom_attributes]&.permit! || {}
|
||||
status = params[:status].present? ? { status: params[:status] } : {}
|
||||
|
||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||
|
@ -122,11 +129,18 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: additional_attributes,
|
||||
snoozed_until: params[:snoozed_until]
|
||||
custom_attributes: custom_attributes,
|
||||
snoozed_until: params[:snoozed_until],
|
||||
assignee_id: params[:assignee_id],
|
||||
team_id: params[:team_id]
|
||||
}.merge(status)
|
||||
end
|
||||
|
||||
def conversation_finder
|
||||
@conversation_finder ||= ConversationFinder.new(current_user, params)
|
||||
end
|
||||
|
||||
def assignee?
|
||||
@conversation.assignee_id? && current_user == @conversation.assignee
|
||||
end
|
||||
end
|
||||
|
|
|
@ -96,6 +96,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
Current.account.line_channels.create!(permitted_params(Channel::Line::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'telegram'
|
||||
Current.account.telegram_channels.create!(permitted_params(Channel::Telegram::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
when 'whatsapp'
|
||||
Current.account.whatsapp_channels.create!(permitted_params(Channel::Whatsapp::EDITABLE_ATTRS)[:channel].except(:type))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
class Api::V1::ProfilesController < Api::BaseController
|
||||
before_action :set_user
|
||||
|
||||
def show
|
||||
render partial: 'api/v1/models/user.json.jbuilder', locals: { resource: @user }
|
||||
end
|
||||
def show; end
|
||||
|
||||
def update
|
||||
if password_params[:password].present?
|
||||
|
@ -15,19 +13,26 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||
@user.update!(profile_params)
|
||||
end
|
||||
|
||||
def availability
|
||||
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = current_user
|
||||
end
|
||||
|
||||
def availability_params
|
||||
params.require(:profile).permit(:account_id, :availability)
|
||||
end
|
||||
|
||||
def profile_params
|
||||
params.require(:profile).permit(
|
||||
:email,
|
||||
:name,
|
||||
:display_name,
|
||||
:avatar,
|
||||
:availability,
|
||||
ui_settings: {}
|
||||
)
|
||||
end
|
||||
|
|
|
@ -29,6 +29,12 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||
render layout: false, template: 'api/v2/accounts/reports/labels.csv.erb', format: 'csv'
|
||||
end
|
||||
|
||||
def teams
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = 'attachment; filename=teams_report.csv'
|
||||
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
|
|
30
app/controllers/webhooks/instagram_controller.rb
Normal file
30
app/controllers/webhooks/instagram_controller.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
class Webhooks::InstagramController < ApplicationController
|
||||
skip_before_action :authenticate_user!, raise: false
|
||||
skip_before_action :set_current_user
|
||||
|
||||
def verify
|
||||
if valid_instagram_token?(params['hub.verify_token'])
|
||||
Rails.logger.info('Instagram webhook verified')
|
||||
render json: params['hub.challenge']
|
||||
else
|
||||
render json: { error: 'Error; wrong verify token', status: 403 }
|
||||
end
|
||||
end
|
||||
|
||||
def events
|
||||
Rails.logger.info('Instagram webhook received events')
|
||||
if params['object'].casecmp('instagram').zero?
|
||||
::Webhooks::InstagramEventsJob.perform_later(params.to_unsafe_hash[:entry])
|
||||
render json: :ok
|
||||
else
|
||||
Rails.logger.info("Message is not received from the instagram webhook event: #{params['object']}")
|
||||
head :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def valid_instagram_token?(token)
|
||||
token == ENV['IG_VERIFY_TOKEN']
|
||||
end
|
||||
end
|
6
app/controllers/webhooks/whatsapp_controller.rb
Normal file
6
app/controllers/webhooks/whatsapp_controller.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
class Webhooks::WhatsappController < ActionController::API
|
||||
def process_payload
|
||||
Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash)
|
||||
head :ok
|
||||
end
|
||||
end
|
|
@ -94,6 +94,8 @@ class ConversationFinder
|
|||
end
|
||||
|
||||
def filter_by_status
|
||||
return if params[:status] == 'all'
|
||||
|
||||
@conversations = @conversations.where(status: params[:status] || DEFAULT_STATUS)
|
||||
end
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
:has-accounts="hasAccounts"
|
||||
/>
|
||||
<woot-snackbar-box />
|
||||
<network-notification />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -15,6 +16,7 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
import AddAccountModal from '../dashboard/components/layout/sidebarComponents/AddAccountModal';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
import NetworkNotification from './components/NetworkNotification';
|
||||
import { accountIdFromPathname } from './helper/URLHelper';
|
||||
|
||||
export default {
|
||||
|
@ -23,6 +25,7 @@ export default {
|
|||
components: {
|
||||
WootSnackbarBox,
|
||||
AddAccountModal,
|
||||
NetworkNotification,
|
||||
},
|
||||
|
||||
data() {
|
||||
|
|
18
app/javascript/dashboard/api/accountActions.js
Normal file
18
app/javascript/dashboard/api/accountActions.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/* global axios */
|
||||
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class AccountActions extends ApiClient {
|
||||
constructor() {
|
||||
super('actions', { accountScoped: true });
|
||||
}
|
||||
|
||||
merge(parentId, childId) {
|
||||
return axios.post(`${this.url}/contact_merge`, {
|
||||
base_contact_id: parentId,
|
||||
mergee_contact_id: childId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccountActions();
|
|
@ -161,9 +161,9 @@ export default {
|
|||
});
|
||||
},
|
||||
|
||||
updateAvailability({ availability }) {
|
||||
return axios.put(endPoints('profileUpdate').url, {
|
||||
profile: { availability },
|
||||
updateAvailability(availabilityData) {
|
||||
return axios.post(endPoints('availabilityUpdate').url, {
|
||||
profile: { ...availabilityData },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -52,6 +52,14 @@ class ContactAPI extends ApiClient {
|
|||
)}`;
|
||||
return axios.get(requestURL);
|
||||
}
|
||||
|
||||
importContacts(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('import_file', file);
|
||||
return axios.post(`${this.url}/import`, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ContactAPI();
|
||||
|
|
|
@ -13,6 +13,9 @@ const endPoints = {
|
|||
profileUpdate: {
|
||||
url: '/api/v1/profile',
|
||||
},
|
||||
availabilityUpdate: {
|
||||
url: '/api/v1/profile/availability',
|
||||
},
|
||||
logout: {
|
||||
url: 'auth/sign_out',
|
||||
},
|
||||
|
|
|
@ -8,6 +8,8 @@ export const buildCreatePayload = ({
|
|||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
}) => {
|
||||
let payload;
|
||||
if (file) {
|
||||
|
@ -18,12 +20,16 @@ export const buildCreatePayload = ({
|
|||
}
|
||||
payload.append('private', isPrivate);
|
||||
payload.append('echo_id', echoId);
|
||||
payload.append('cc_emails', ccEmails);
|
||||
payload.append('bcc_emails', bccEmails);
|
||||
} else {
|
||||
payload = {
|
||||
content: message,
|
||||
private: isPrivate,
|
||||
echo_id: echoId,
|
||||
content_attributes: contentAttributes,
|
||||
cc_emails: ccEmails,
|
||||
bcc_emails: bccEmails,
|
||||
};
|
||||
}
|
||||
return payload;
|
||||
|
@ -41,6 +47,8 @@ class MessageApi extends ApiClient {
|
|||
contentAttributes,
|
||||
echo_id: echoId,
|
||||
file,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
}) {
|
||||
return axios({
|
||||
method: 'post',
|
||||
|
@ -51,6 +59,8 @@ class MessageApi extends ApiClient {
|
|||
contentAttributes,
|
||||
echoId,
|
||||
file,
|
||||
ccEmails,
|
||||
bccEmails,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,15 +6,15 @@ class ReportsAPI extends ApiClient {
|
|||
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
||||
}
|
||||
|
||||
getAccountReports(metric, since, until) {
|
||||
getReports(metric, since, until, type = 'account', id) {
|
||||
return axios.get(`${this.url}`, {
|
||||
params: { metric, since, until, type: 'account' },
|
||||
params: { metric, since, until, type, id },
|
||||
});
|
||||
}
|
||||
|
||||
getAccountSummary(since, until) {
|
||||
getSummary(since, until, type = 'account', id) {
|
||||
return axios.get(`${this.url}/summary`, {
|
||||
params: { since, until, type: 'account' },
|
||||
params: { since, until, type, id },
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,24 @@ class ReportsAPI extends ApiClient {
|
|||
params: { since, until },
|
||||
});
|
||||
}
|
||||
|
||||
getLabelReports(since, until) {
|
||||
return axios.get(`${this.url}/labels`, {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
|
||||
getInboxReports(since, until) {
|
||||
return axios.get(`${this.url}/inboxes`, {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
|
||||
getTeamReports(since, until) {
|
||||
return axios.get(`${this.url}/teams`, {
|
||||
params: { since, until },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ReportsAPI();
|
||||
|
|
23
app/javascript/dashboard/api/specs/accountActions.spec.js
Normal file
23
app/javascript/dashboard/api/specs/accountActions.spec.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import accountActionsAPI from '../accountActions';
|
||||
import ApiClient from '../ApiClient';
|
||||
import describeWithAPIMock from './apiSpecHelper';
|
||||
|
||||
describe('#ContactsAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(accountActionsAPI).toBeInstanceOf(ApiClient);
|
||||
expect(accountActionsAPI).toHaveProperty('merge');
|
||||
});
|
||||
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#merge', () => {
|
||||
accountActionsAPI.merge(1, 2);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/actions/contact_merge',
|
||||
{
|
||||
base_contact_id: 1,
|
||||
mergee_contact_id: 2,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -59,6 +59,18 @@ describe('#ContactsAPI', () => {
|
|||
'/api/v1/contacts/search?include_contact_inboxes=false&page=1&sort=date&q=leads&labels[]=customer-support'
|
||||
);
|
||||
});
|
||||
|
||||
it('#importContacts', () => {
|
||||
const file = 'file';
|
||||
contactAPI.importContacts(file);
|
||||
expect(context.axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/contacts/import',
|
||||
expect.any(FormData),
|
||||
{
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -11,39 +11,35 @@ describe('#Reports API', () => {
|
|||
expect(reportsAPI).toHaveProperty('create');
|
||||
expect(reportsAPI).toHaveProperty('update');
|
||||
expect(reportsAPI).toHaveProperty('delete');
|
||||
expect(reportsAPI).toHaveProperty('getAccountReports');
|
||||
expect(reportsAPI).toHaveProperty('getAccountSummary');
|
||||
expect(reportsAPI).toHaveProperty('getReports');
|
||||
expect(reportsAPI).toHaveProperty('getSummary');
|
||||
expect(reportsAPI).toHaveProperty('getAgentReports');
|
||||
expect(reportsAPI).toHaveProperty('getLabelReports');
|
||||
expect(reportsAPI).toHaveProperty('getInboxReports');
|
||||
expect(reportsAPI).toHaveProperty('getTeamReports');
|
||||
});
|
||||
describeWithAPIMock('API calls', context => {
|
||||
it('#getAccountReports', () => {
|
||||
reportsAPI.getAccountReports(
|
||||
'conversations_count',
|
||||
1621103400,
|
||||
1621621800
|
||||
);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports',
|
||||
{
|
||||
params: {
|
||||
metric: 'conversations_count',
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account'
|
||||
},
|
||||
}
|
||||
);
|
||||
reportsAPI.getReports('conversations_count', 1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
|
||||
params: {
|
||||
metric: 'conversations_count',
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('#getAccountSummary', () => {
|
||||
reportsAPI.getAccountSummary(1621103400, 1621621800);
|
||||
reportsAPI.getSummary(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/summary',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account'
|
||||
type: 'account',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
@ -61,5 +57,44 @@ describe('#Reports API', () => {
|
|||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getLabelReports', () => {
|
||||
reportsAPI.getLabelReports(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/labels',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getInboxReports', () => {
|
||||
reportsAPI.getInboxReports(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/inboxes',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('#getTeamReports', () => {
|
||||
reportsAPI.getTeamReports(1621103400, 1621621800);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v2/reports/teams',
|
||||
{
|
||||
params: {
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
BIN
app/javascript/dashboard/assets/images/channels/messenger.png
Normal file
BIN
app/javascript/dashboard/assets/images/channels/messenger.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
Binary file not shown.
Before Width: | Height: | Size: 22 KiB |
Binary file not shown.
Before Width: | Height: | Size: 36 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 12 KiB |
|
@ -93,3 +93,17 @@
|
|||
/* .slide-fade-leave-active for <2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.network-notification-fade-enter-active {
|
||||
transition: all .1s $ease-in-sine;
|
||||
}
|
||||
|
||||
.network-notification-fade-leave-active {
|
||||
transition: all .1s $ease-out-sine;
|
||||
}
|
||||
|
||||
.network-notification-fade-enter,
|
||||
.network-notification-fade-leave-to {
|
||||
transform: translateY(-$space-small);
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.margin-right-small {
|
||||
margin-right: var(--space-small);
|
||||
}
|
|
@ -14,6 +14,7 @@
|
|||
@import 'helper-classes';
|
||||
@import 'formulate';
|
||||
@import 'date-picker';
|
||||
@import 'utility-helpers';
|
||||
|
||||
@import 'foundation-sites/scss/foundation';
|
||||
@import '~bourbon/core/bourbon';
|
||||
|
|
|
@ -15,6 +15,10 @@
|
|||
.multiselect {
|
||||
margin-bottom: var(--space-normal);
|
||||
|
||||
&.multiselect--disabled {
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
.multiselect--active {
|
||||
>.multiselect__tags {
|
||||
border-color: $color-woot;
|
||||
|
@ -209,3 +213,53 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.multiselect-wrap--medium {
|
||||
$multiselect-height: 4.8rem;
|
||||
|
||||
.multiselect__tags,
|
||||
.multiselect__input {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.multiselect__tags,
|
||||
.multiselect__input,
|
||||
.multiselect {
|
||||
background: var(--white);
|
||||
font-size: var(--font-size-small);
|
||||
height: $multiselect-height;
|
||||
min-height: $multiselect-height;
|
||||
}
|
||||
|
||||
.multiselect__input {
|
||||
height: $multiselect-height - $space-micro;
|
||||
min-height: $multiselect-height - $space-micro;
|
||||
}
|
||||
|
||||
.multiselect__single {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
margin: 0;
|
||||
padding: var(--space-smaller) var(--space-micro);
|
||||
}
|
||||
|
||||
.multiselect__placeholder {
|
||||
margin: 0;
|
||||
padding: var(--space-smaller) var(--space-micro);
|
||||
}
|
||||
|
||||
.multiselect__select {
|
||||
min-height: $multiselect-height;
|
||||
}
|
||||
|
||||
.multiselect--disabled .multiselect__current,
|
||||
.multiselect--disabled .multiselect__select {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.multiselect__tags-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,14 +42,6 @@ $resolve-button-width: 13.2rem;
|
|||
margin-right: var(--space-normal);
|
||||
min-width: 0;
|
||||
|
||||
.user--name {
|
||||
@include margin(0);
|
||||
display: inline-block;
|
||||
font-size: $font-size-medium;
|
||||
line-height: 1.3;
|
||||
text-transform: capitalize;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.user--profile__meta {
|
||||
align-items: flex-start;
|
||||
|
@ -59,12 +51,6 @@ $resolve-button-width: 13.2rem;
|
|||
margin-left: $space-slab;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.user--profile__button {
|
||||
font-size: $font-size-mini;
|
||||
margin-top: $space-micro;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
|
||||
.conversation-panel {
|
||||
@include flex;
|
||||
@include flex-weight(1);
|
||||
@include flex-weight(1 1 1px);
|
||||
@include margin($zero);
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
|
|
@ -71,7 +71,8 @@
|
|||
@include padding($space-large);
|
||||
}
|
||||
|
||||
form {
|
||||
form,
|
||||
.modal-content {
|
||||
@include padding($space-large);
|
||||
align-self: center;
|
||||
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
.report-bar {
|
||||
@include margin(-1px $zero);
|
||||
@include background-white;
|
||||
|
|
30
app/javascript/dashboard/assets/scss/widgets/_reports.scss
Normal file
30
app/javascript/dashboard/assets/scss/widgets/_reports.scss
Normal file
|
@ -0,0 +1,30 @@
|
|||
.date-picker {
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
|
||||
.margin-left-small {
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
|
||||
.reports-option__rounded--item {
|
||||
border-radius: 100%;
|
||||
height: var(--space-two);
|
||||
width: var(--space-two);
|
||||
}
|
||||
|
||||
.reports-option__item {
|
||||
flex-shrink: 0;
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.reports-option__label--swatch {
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.margin-right-small {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
|
||||
.display-flex {
|
||||
display: flex;
|
||||
}
|
|
@ -26,6 +26,7 @@
|
|||
:active-label="label"
|
||||
:team-id="teamId"
|
||||
:chat="chat"
|
||||
:show-assignee="showAssigneeInConversationCard"
|
||||
/>
|
||||
|
||||
<div v-if="chatListLoading" class="text-center">
|
||||
|
@ -119,6 +120,9 @@ export default {
|
|||
};
|
||||
});
|
||||
},
|
||||
showAssigneeInConversationCard() {
|
||||
return this.activeAssigneeTab === wootConstants.ASSIGNEE_TYPE.ALL;
|
||||
},
|
||||
inbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](this.activeInbox);
|
||||
},
|
||||
|
|
123
app/javascript/dashboard/components/NetworkNotification.vue
Normal file
123
app/javascript/dashboard/components/NetworkNotification.vue
Normal file
|
@ -0,0 +1,123 @@
|
|||
<template>
|
||||
<transition name="network-notification-fade" tag="div">
|
||||
<div v-show="showNotification" class="ui-notification-container">
|
||||
<div class="ui-notification">
|
||||
<svg
|
||||
class="ui-notification-icon"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414"
|
||||
/>
|
||||
</svg>
|
||||
<p class="ui-notification-text">
|
||||
{{ $t('NETWORK.NOTIFICATION.TEXT') }}
|
||||
</p>
|
||||
<button class="ui-refresh-button" @click="refreshPage">
|
||||
{{ $t('NETWORK.BUTTON.REFRESH') }}
|
||||
</button>
|
||||
<button class="ui-close-button" @click="closeNotification">
|
||||
<i class="ui-close-icon icon ion-ios-close-outline" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showNotification: !navigator.onLine,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
window.addEventListener('offline', this.updateOnlineStatus);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('offline', this.updateOnlineStatus);
|
||||
},
|
||||
|
||||
methods: {
|
||||
refreshPage() {
|
||||
window.location.reload();
|
||||
},
|
||||
|
||||
closeNotification() {
|
||||
this.showNotification = false;
|
||||
},
|
||||
|
||||
updateOnlineStatus(event) {
|
||||
if (event.type === 'offline') {
|
||||
this.showNotification = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.ui-notification-container {
|
||||
max-width: 40rem;
|
||||
position: absolute;
|
||||
right: var(--space-normal);
|
||||
top: var(--space-normal);
|
||||
width: 100%;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.ui-notification {
|
||||
@include shadow;
|
||||
align-items: center;
|
||||
background-color: var(--white);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--space-one);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-width: 40rem;
|
||||
min-height: 3rem;
|
||||
min-width: 24rem;
|
||||
padding: var(--space-normal) var(--space-two);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ui-notification-text {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ui-refresh-button {
|
||||
color: var(--color-woot);
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: bold;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-notification-icon {
|
||||
color: var(--b-600);
|
||||
width: var(--font-size-mega);
|
||||
}
|
||||
|
||||
.ui-close-icon {
|
||||
color: var(--b-600);
|
||||
font-size: var(--font-size-large);
|
||||
}
|
||||
|
||||
.ui-close-button {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="status">
|
||||
<div class="status-view">
|
||||
<availability-status-badge :status="currentUserAvailabilityStatus" />
|
||||
<availability-status-badge :status="currentUserAvailability" />
|
||||
<div class="status-view--title">
|
||||
{{ availabilityDisplayLabel }}
|
||||
</div>
|
||||
|
@ -26,7 +26,9 @@
|
|||
color-scheme="secondary"
|
||||
class-names="status-change--dropdown-button"
|
||||
:is-disabled="status.disabled"
|
||||
@click="changeAvailabilityStatus(status.value)"
|
||||
@click="
|
||||
changeAvailabilityStatus(status.value, currentAccountId)
|
||||
"
|
||||
>
|
||||
<availability-status-badge :status="status.value" />
|
||||
{{ status.label }}
|
||||
|
@ -75,18 +77,22 @@ export default {
|
|||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
||||
getCurrentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
availabilityDisplayLabel() {
|
||||
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
||||
key => key === this.currentUserAvailabilityStatus
|
||||
key => key === this.currentUserAvailability
|
||||
);
|
||||
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST')[
|
||||
availabilityIndex
|
||||
];
|
||||
},
|
||||
currentUserAvailabilityStatus() {
|
||||
return this.currentUser.availability_status;
|
||||
currentAccountId() {
|
||||
return this.getCurrentAccountId;
|
||||
},
|
||||
currentUserAvailability() {
|
||||
return this.getCurrentUserAvailability;
|
||||
},
|
||||
availabilityStatuses() {
|
||||
return this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.STATUSES_LIST').map(
|
||||
|
@ -94,7 +100,7 @@ export default {
|
|||
label: statusLabel,
|
||||
value: AVAILABILITY_STATUS_KEYS[index],
|
||||
disabled:
|
||||
this.currentUserAvailabilityStatus ===
|
||||
this.currentUserAvailability ===
|
||||
AVAILABILITY_STATUS_KEYS[index],
|
||||
})
|
||||
);
|
||||
|
@ -108,16 +114,16 @@ export default {
|
|||
closeStatusMenu() {
|
||||
this.isStatusMenuOpened = false;
|
||||
},
|
||||
changeAvailabilityStatus(availability) {
|
||||
changeAvailabilityStatus(availability, accountId) {
|
||||
if (this.isUpdating) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isUpdating = true;
|
||||
|
||||
this.$store
|
||||
.dispatch('updateAvailability', {
|
||||
availability,
|
||||
availability: availability,
|
||||
account_id: accountId,
|
||||
})
|
||||
.finally(() => {
|
||||
this.isUpdating = false;
|
||||
|
|
|
@ -17,7 +17,8 @@ const i18nConfig = new VueI18n({
|
|||
});
|
||||
|
||||
describe('AvailabilityStatus', () => {
|
||||
const currentUser = { availability_status: 'online' };
|
||||
const currentAvailability = 'online';
|
||||
const currentAccountId = '1';
|
||||
let store = null;
|
||||
let actions = null;
|
||||
let modules = null;
|
||||
|
@ -33,7 +34,8 @@ describe('AvailabilityStatus', () => {
|
|||
modules = {
|
||||
auth: {
|
||||
getters: {
|
||||
getCurrentUser: () => currentUser,
|
||||
getCurrentUserAvailability: () => currentAvailability,
|
||||
getCurrentAccountId: () => currentAccountId,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -77,7 +79,7 @@ describe('AvailabilityStatus', () => {
|
|||
|
||||
expect(actions.updateAvailability).toBeCalledWith(
|
||||
expect.any(Object),
|
||||
{ availability: 'offline' },
|
||||
{ availability: 'offline', account_id: currentAccountId },
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
>
|
||||
<img
|
||||
v-if="channel.key === 'facebook'"
|
||||
src="~dashboard/assets/images/channels/facebook.png"
|
||||
src="~dashboard/assets/images/channels/messenger.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel.key === 'twitter'"
|
||||
|
|
35
app/javascript/dashboard/components/widgets/InboxName.vue
Normal file
35
app/javascript/dashboard/components/widgets/InboxName.vue
Normal file
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<span class="inbox--name">
|
||||
<i :class="computedInboxClass" />
|
||||
{{ inbox.name }}
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
inbox: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
computedInboxClass() {
|
||||
const { phone_number: phoneNumber, channel_type: type } = this.inbox;
|
||||
const classByType = getInboxClassByType(type, phoneNumber);
|
||||
return classByType;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.inbox--name {
|
||||
padding: var(--space-micro) 0;
|
||||
line-height: var(--space-slab);
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: none;
|
||||
color: var(--s-500);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
</style>
|
|
@ -15,39 +15,60 @@
|
|||
:size="avatarSize"
|
||||
/>
|
||||
<img
|
||||
v-if="badge === 'Channel::FacebookPage'"
|
||||
v-if="badge === 'instagram_direct_message'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="~dashboard/assets/images/fb-badge.png"
|
||||
src="/integrations/channels/badges/instagram-dm.png"
|
||||
/>
|
||||
<img
|
||||
v-if="badge === 'Channel::TwitterProfile'"
|
||||
v-else-if="badge === 'facebook'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="~dashboard/assets/images/twitter-badge.png"
|
||||
src="/integrations/channels/badges/messenger.png"
|
||||
/>
|
||||
<img
|
||||
v-if="badge === 'Channel::TwilioSms'"
|
||||
v-else-if="badge === 'twitter-tweet'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="~dashboard/assets/images/channels/whatsapp.png"
|
||||
src="/integrations/channels/badges/twitter-tweet.png"
|
||||
/>
|
||||
<img
|
||||
v-if="badge === 'Channel::Line'"
|
||||
v-else-if="badge === 'twitter-dm'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="~dashboard/assets/images/channels/line.png"
|
||||
src="/integrations/channels/badges/twitter-dm.png"
|
||||
/>
|
||||
<img
|
||||
v-if="badge === 'Channel::Telegram'"
|
||||
v-else-if="badge === 'whatsapp'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="~dashboard/assets/images/channels/telegram.png"
|
||||
src="/integrations/channels/badges/whatsapp.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="badge === 'sms'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="/integrations/channels/badges/sms.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="badge === 'Channel::Line'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="/integrations/channels/badges/line.png"
|
||||
/>
|
||||
<img
|
||||
v-else-if="badge === 'Channel::Telegram'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="/integrations/channels/badges/telegram.png"
|
||||
/>
|
||||
<div
|
||||
v-if="showStatusIndicator"
|
||||
|
@ -109,8 +130,10 @@ export default {
|
|||
return Number(this.size.replace(/\D+/g, ''));
|
||||
},
|
||||
badgeStyle() {
|
||||
const badgeSize = `${this.avatarSize / 3}px`;
|
||||
return { width: badgeSize, height: badgeSize };
|
||||
const size = Math.floor(this.avatarSize / 3);
|
||||
const badgeSize = `${size + 2}px`;
|
||||
const borderRadius = `${size / 2}px`;
|
||||
return { width: badgeSize, height: badgeSize, borderRadius };
|
||||
},
|
||||
statusStyle() {
|
||||
const statusSize = `${this.avatarSize / 4}px`;
|
||||
|
@ -152,6 +175,7 @@ export default {
|
|||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
object-fit: cover;
|
||||
|
||||
&.border {
|
||||
border: 1px solid white;
|
||||
|
@ -159,8 +183,12 @@ export default {
|
|||
}
|
||||
|
||||
.source-badge {
|
||||
background: white;
|
||||
border-radius: var(--border-radius-small);
|
||||
bottom: -$space-micro;
|
||||
box-shadow: var(--shadow-small);
|
||||
height: $space-slab;
|
||||
padding: var(--space-micro);
|
||||
position: absolute;
|
||||
right: $zero;
|
||||
width: $space-slab;
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
>
|
||||
{{ item['TEXT'] }}
|
||||
</option>
|
||||
<option value="all">
|
||||
{{ $t('CHAT_LIST.FILTER_ALL') }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
|
@ -30,6 +33,8 @@ export default {
|
|||
} else if (this.activeStatus === wootConstants.STATUS_TYPE.PENDING) {
|
||||
this.activeStatus = wootConstants.STATUS_TYPE.SNOOZED;
|
||||
} else if (this.activeStatus === wootConstants.STATUS_TYPE.SNOOZED) {
|
||||
this.activeStatus = wootConstants.STATUS_TYPE.ALL;
|
||||
} else if (this.activeStatus === wootConstants.STATUS_TYPE.ALL) {
|
||||
this.activeStatus = wootConstants.STATUS_TYPE.OPEN;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,20 +8,26 @@
|
|||
}"
|
||||
@click="cardClick(chat)"
|
||||
>
|
||||
<Thumbnail
|
||||
<thumbnail
|
||||
v-if="!hideThumbnail"
|
||||
:src="currentContact.thumbnail"
|
||||
:badge="chatMetadata.channel"
|
||||
:badge="inboxBadge"
|
||||
class="columns"
|
||||
:username="currentContact.name"
|
||||
:status="currentContact.availability_status"
|
||||
size="40px"
|
||||
/>
|
||||
<div class="conversation--details columns">
|
||||
<span v-if="showInboxName" class="label">
|
||||
<i :class="computedInboxClass" />
|
||||
{{ inboxName }}
|
||||
</span>
|
||||
<div class="conversation--metadata">
|
||||
<inbox-name v-if="showInboxName" :inbox="inbox" />
|
||||
<span
|
||||
v-if="showAssignee && assignee.name"
|
||||
class="label assignee-label text-truncate"
|
||||
>
|
||||
<i class="ion-person" />
|
||||
{{ assignee.name }}
|
||||
</span>
|
||||
</div>
|
||||
<h4 class="conversation--user">
|
||||
{{ currentContact.name }}
|
||||
</h4>
|
||||
|
@ -62,19 +68,21 @@
|
|||
import { mapGetters } from 'vuex';
|
||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||
import Thumbnail from '../Thumbnail';
|
||||
import conversationMixin from '../../../mixins/conversations';
|
||||
import timeMixin from '../../../mixins/time';
|
||||
import router from '../../../routes';
|
||||
import { frontendURL, conversationUrl } from '../../../helper/URLHelper';
|
||||
import InboxName from '../InboxName';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InboxName,
|
||||
Thumbnail,
|
||||
},
|
||||
|
||||
mixins: [timeMixin, conversationMixin, messageFormatterMixin],
|
||||
mixins: [inboxMixin, timeMixin, conversationMixin, messageFormatterMixin],
|
||||
props: {
|
||||
activeLabel: {
|
||||
type: String,
|
||||
|
@ -96,6 +104,10 @@ export default {
|
|||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
showAssignee: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
@ -108,7 +120,11 @@ export default {
|
|||
}),
|
||||
|
||||
chatMetadata() {
|
||||
return this.chat.meta;
|
||||
return this.chat.meta || {};
|
||||
},
|
||||
|
||||
assignee() {
|
||||
return this.chatMetadata.assignee || {};
|
||||
},
|
||||
|
||||
currentContact() {
|
||||
|
@ -167,18 +183,12 @@ export default {
|
|||
return this.getPlainText(subject || this.lastMessageInChat.content);
|
||||
},
|
||||
|
||||
chatInbox() {
|
||||
inbox() {
|
||||
const { inbox_id: inboxId } = this.chat;
|
||||
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
|
||||
return stateInbox;
|
||||
},
|
||||
|
||||
computedInboxClass() {
|
||||
const { phone_number: phoneNumber, channel_type: type } = this.chatInbox;
|
||||
const classByType = getInboxClassByType(type, phoneNumber);
|
||||
return classByType;
|
||||
},
|
||||
|
||||
showInboxName() {
|
||||
return (
|
||||
!this.hideInboxName &&
|
||||
|
@ -187,11 +197,10 @@ export default {
|
|||
);
|
||||
},
|
||||
inboxName() {
|
||||
const stateInbox = this.chatInbox;
|
||||
const stateInbox = this.inbox;
|
||||
return stateInbox.name || '';
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
cardClick(chat) {
|
||||
const { activeInbox } = this;
|
||||
|
@ -226,15 +235,6 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.conversation--details .label {
|
||||
padding: var(--space-micro) 0 var(--space-micro) 0;
|
||||
line-height: var(--space-slab);
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: none;
|
||||
color: var(--s-500);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
.conversation--details {
|
||||
.conversation--user {
|
||||
padding-top: var(--space-micro);
|
||||
|
@ -252,4 +252,23 @@ export default {
|
|||
color: var(--s-600);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
.conversation--metadata {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-right: var(--space-normal);
|
||||
|
||||
.label {
|
||||
padding: var(--space-micro) 0 var(--space-micro) 0;
|
||||
line-height: var(--space-slab);
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: none;
|
||||
color: var(--s-500);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
.assignee-label {
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<Thumbnail
|
||||
:src="currentContact.thumbnail"
|
||||
size="40px"
|
||||
:badge="chatMetadata.channel"
|
||||
:badge="inboxBadge"
|
||||
:username="currentContact.name"
|
||||
:status="currentContact.availability_status"
|
||||
/>
|
||||
|
@ -12,20 +12,23 @@
|
|||
<h3 class="user--name text-truncate">
|
||||
{{ currentContact.name }}
|
||||
</h3>
|
||||
<woot-button
|
||||
class="user--profile__button"
|
||||
size="small"
|
||||
variant="link"
|
||||
@click="$emit('contact-panel-toggle')"
|
||||
>
|
||||
{{
|
||||
`${
|
||||
isContactPanelOpen
|
||||
? $t('CONVERSATION.HEADER.CLOSE')
|
||||
: $t('CONVERSATION.HEADER.OPEN')
|
||||
} ${$t('CONVERSATION.HEADER.DETAILS')}`
|
||||
}}
|
||||
</woot-button>
|
||||
<div class="conversation--header--actions">
|
||||
<inbox-name :inbox="inbox" class="margin-right-small" />
|
||||
<span
|
||||
v-if="isSnoozed"
|
||||
class="snoozed--display-text margin-right-small"
|
||||
>
|
||||
{{ snoozedDisplayText }}
|
||||
</span>
|
||||
<woot-button
|
||||
class="user--profile__button margin-right-small"
|
||||
size="small"
|
||||
variant="link"
|
||||
@click="$emit('contact-panel-toggle')"
|
||||
>
|
||||
{{ contactPanelToggleText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -42,14 +45,19 @@ import MoreActions from './MoreActions';
|
|||
import Thumbnail from '../Thumbnail';
|
||||
import agentMixin from '../../../mixins/agentMixin.js';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
|
||||
import wootConstants from '../../../constants';
|
||||
import differenceInHours from 'date-fns/differenceInHours';
|
||||
import InboxName from '../InboxName';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
InboxName,
|
||||
MoreActions,
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [agentMixin, eventListenerMixins],
|
||||
mixins: [inboxMixin, agentMixin, eventListenerMixins],
|
||||
props: {
|
||||
chat: {
|
||||
type: Object,
|
||||
|
@ -60,14 +68,6 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
currentChatAssignee: null,
|
||||
inboxId: null,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||
|
@ -83,10 +83,37 @@ export default {
|
|||
this.chat.meta.sender.id
|
||||
);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { inbox_id: inboxId } = this.chat;
|
||||
this.inboxId = inboxId;
|
||||
isSnoozed() {
|
||||
return this.currentChat.status === wootConstants.STATUS_TYPE.SNOOZED;
|
||||
},
|
||||
snoozedDisplayText() {
|
||||
const { snoozed_until: snoozedUntil } = this.currentChat;
|
||||
if (snoozedUntil) {
|
||||
// When the snooze is applied, it schedules the unsnooze event to next day/week 9AM.
|
||||
// By that logic if the time difference is less than or equal to 24 + 9 hours we can consider it tomorrow.
|
||||
const MAX_TIME_DIFFERENCE = 33;
|
||||
const isSnoozedUntilTomorrow =
|
||||
differenceInHours(new Date(snoozedUntil), new Date()) <=
|
||||
MAX_TIME_DIFFERENCE;
|
||||
return this.$t(
|
||||
isSnoozedUntilTomorrow
|
||||
? 'CONVERSATION.HEADER.SNOOZED_UNTIL_TOMORROW'
|
||||
: 'CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_WEEK'
|
||||
);
|
||||
}
|
||||
return this.$t('CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_REPLY');
|
||||
},
|
||||
contactPanelToggleText() {
|
||||
return `${
|
||||
this.isContactPanelOpen
|
||||
? this.$t('CONVERSATION.HEADER.CLOSE')
|
||||
: this.$t('CONVERSATION.HEADER.OPEN')
|
||||
} ${this.$t('CONVERSATION.HEADER.DETAILS')}`;
|
||||
},
|
||||
inbox() {
|
||||
const { inbox_id: inboxId } = this.chat;
|
||||
return this.$store.getters['inboxes/getInbox'](inboxId);
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -122,4 +149,28 @@ export default {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.user--name {
|
||||
display: inline-block;
|
||||
font-size: var(--font-size-medium);
|
||||
line-height: 1.3;
|
||||
margin: 0;
|
||||
text-transform: capitalize;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.conversation--header--actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: var(--font-size-mini);
|
||||
|
||||
.user--profile__button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.snoozed--display-text {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--y-900);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
<div :class="wrapClass">
|
||||
<div v-tooltip.top-start="sentByMessage" :class="bubbleClass">
|
||||
<bubble-mail-head
|
||||
v-if="isEmailContentType"
|
||||
:email-attributes="contentAttributes.email"
|
||||
:cc="emailHeadAttributes.cc"
|
||||
:bcc="emailHeadAttributes.bcc"
|
||||
:is-incoming="isIncoming"
|
||||
/>
|
||||
<bubble-text
|
||||
|
@ -222,6 +223,13 @@ export default {
|
|||
isIncoming() {
|
||||
return this.data.message_type === MESSAGE_TYPE.INCOMING;
|
||||
},
|
||||
emailHeadAttributes() {
|
||||
return {
|
||||
email: this.contentAttributes.email,
|
||||
cc: this.contentAttributes.cc_emails,
|
||||
bcc: this.contentAttributes.bcc_emails
|
||||
}
|
||||
},
|
||||
hasAttachments() {
|
||||
return !!(this.data.attachments && this.data.attachments.length > 0);
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="view-box fill-height">
|
||||
<div
|
||||
v-if="!currentChat.can_reply && !isATwilioWhatsappChannel"
|
||||
v-if="!currentChat.can_reply && !isAWhatsappChannel"
|
||||
class="banner messenger-policy--banner"
|
||||
>
|
||||
<span>
|
||||
|
@ -16,7 +16,7 @@
|
|||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!currentChat.can_reply && isATwilioWhatsappChannel"
|
||||
v-if="!currentChat.can_reply && isAWhatsappChannel"
|
||||
class="banner messenger-policy--banner"
|
||||
>
|
||||
<span>
|
||||
|
|
|
@ -1,41 +1,29 @@
|
|||
<template>
|
||||
<div class="flex-container actions--container">
|
||||
<woot-button
|
||||
v-if="!currentChat.muted"
|
||||
v-tooltip="$t('CONTACT_PANEL.MUTE_CONTACT')"
|
||||
class="hollow secondary actions--button"
|
||||
icon="ion-volume-mute"
|
||||
@click="mute"
|
||||
/>
|
||||
<woot-button
|
||||
v-else
|
||||
v-tooltip.left="$t('CONTACT_PANEL.UNMUTE_CONTACT')"
|
||||
class="hollow secondary actions--button"
|
||||
icon="ion-volume-medium"
|
||||
@click="unmute"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip="$t('CONTACT_PANEL.SEND_TRANSCRIPT')"
|
||||
class="hollow secondary actions--button"
|
||||
icon="ion-share"
|
||||
@click="toggleEmailActionsModal"
|
||||
/>
|
||||
<resolve-action
|
||||
:conversation-id="currentChat.id"
|
||||
:status="currentChat.status"
|
||||
/>
|
||||
<woot-button
|
||||
class="more--button"
|
||||
variant="clear"
|
||||
size="large"
|
||||
color-scheme="secondary"
|
||||
icon="ion-android-more-vertical"
|
||||
@click="toggleConversationActions"
|
||||
/>
|
||||
<div
|
||||
v-if="showConversationActions"
|
||||
v-on-clickaway="hideConversationActions"
|
||||
class="dropdown-pane dropdowm--bottom"
|
||||
:class="{ 'dropdown-pane--open': showConversationActions }"
|
||||
>
|
||||
<woot-dropdown-menu>
|
||||
<woot-dropdown-item v-if="!currentChat.muted">
|
||||
<button class="button clear alert " @click="mute">
|
||||
<span>{{ $t('CONTACT_PANEL.MUTE_CONTACT') }}</span>
|
||||
</button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item v-else>
|
||||
<button class="button clear alert" @click="unmute">
|
||||
<span>{{ $t('CONTACT_PANEL.UNMUTE_CONTACT') }}</span>
|
||||
</button>
|
||||
</woot-dropdown-item>
|
||||
<woot-dropdown-item>
|
||||
<button class="button clear" @click="toggleEmailActionsModal">
|
||||
{{ $t('CONTACT_PANEL.SEND_TRANSCRIPT') }}
|
||||
</button>
|
||||
</woot-dropdown-item>
|
||||
</woot-dropdown-menu>
|
||||
</div>
|
||||
<email-transcript-modal
|
||||
v-if="showEmailActionsModal"
|
||||
:show="showEmailActionsModal"
|
||||
|
@ -50,13 +38,9 @@ import { mixin as clickaway } from 'vue-clickaway';
|
|||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import EmailTranscriptModal from './EmailTranscriptModal';
|
||||
import ResolveAction from '../../buttons/ResolveAction';
|
||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootDropdownMenu,
|
||||
WootDropdownItem,
|
||||
EmailTranscriptModal,
|
||||
ResolveAction,
|
||||
},
|
||||
|
@ -97,7 +81,16 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
.actions--container {
|
||||
align-items: center;
|
||||
|
||||
.button {
|
||||
font-size: var(--font-size-large);
|
||||
margin-right: var(--space-small);
|
||||
border-color: var(--color-border);
|
||||
color: var(--s-400);
|
||||
}
|
||||
}
|
||||
|
||||
.more--button {
|
||||
align-items: center;
|
||||
|
|
|
@ -20,6 +20,11 @@
|
|||
v-on-clickaway="hideEmojiPicker"
|
||||
:on-click="emojiOnClick"
|
||||
/>
|
||||
<reply-email-head
|
||||
v-if="showReplyHead"
|
||||
:clear-mails="clearMails"
|
||||
@set-emails="setCcEmails"
|
||||
/>
|
||||
<resizable-text-area
|
||||
v-if="!showRichContentEditor"
|
||||
ref="messageInput"
|
||||
|
@ -82,6 +87,7 @@ import CannedResponse from './CannedResponse';
|
|||
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
|
||||
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel';
|
||||
import ReplyEmailHead from './ReplyEmailHead';
|
||||
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel';
|
||||
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
|
||||
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
|
||||
|
@ -104,6 +110,7 @@ export default {
|
|||
ResizableTextArea,
|
||||
AttachmentPreview,
|
||||
ReplyTopPanel,
|
||||
ReplyEmailHead,
|
||||
ReplyBottomPanel,
|
||||
WootMessageEditor,
|
||||
},
|
||||
|
@ -134,6 +141,7 @@ export default {
|
|||
mentionSearchKey: '',
|
||||
hasUserMention: false,
|
||||
hasSlashCommand: false,
|
||||
clearMails: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -156,7 +164,7 @@ export default {
|
|||
return !!this.uiSettings.enter_to_send_enabled;
|
||||
},
|
||||
isPrivate() {
|
||||
if (this.currentChat.can_reply || this.isATwilioWhatsappChannel) {
|
||||
if (this.currentChat.can_reply || this.isAWhatsappChannel) {
|
||||
return this.isOnPrivateNote;
|
||||
}
|
||||
return true;
|
||||
|
@ -203,7 +211,7 @@ export default {
|
|||
if (this.isAFacebookInbox) {
|
||||
return MESSAGE_MAX_LENGTH.FACEBOOK;
|
||||
}
|
||||
if (this.isATwilioWhatsappChannel) {
|
||||
if (this.isAWhatsappChannel) {
|
||||
return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP;
|
||||
}
|
||||
if (this.isATwilioSMSChannel) {
|
||||
|
@ -223,7 +231,8 @@ export default {
|
|||
this.isATwilioWhatsappChannel ||
|
||||
this.isAPIInbox ||
|
||||
this.isAnEmailChannel ||
|
||||
this.isATwilioSMSChannel
|
||||
this.isATwilioSMSChannel ||
|
||||
this.isATelegramChannel
|
||||
);
|
||||
},
|
||||
replyButtonLabel() {
|
||||
|
@ -269,6 +278,9 @@ export default {
|
|||
}
|
||||
return !this.message.trim().replace(/\n/g, '').length;
|
||||
},
|
||||
showReplyHead() {
|
||||
return !this.isOnPrivateNote && this.isAnEmailChannel;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentChat(conversation) {
|
||||
|
@ -277,7 +289,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
|
||||
if (canReply || this.isATwilioWhatsappChannel) {
|
||||
if (canReply || this.isAWhatsappChannel) {
|
||||
this.replyType = REPLY_EDITOR_MODES.REPLY;
|
||||
} else {
|
||||
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
||||
|
@ -347,9 +359,13 @@ export default {
|
|||
await this.$store.dispatch('sendMessage', messagePayload);
|
||||
this.$emit('scrollToMessage');
|
||||
} catch (error) {
|
||||
// Error
|
||||
const errorMessage =
|
||||
error?.response?.data?.error ||
|
||||
this.$t('CONVERSATION.MESSAGE_ERROR');
|
||||
this.showAlert(errorMessage);
|
||||
}
|
||||
this.hideEmojiPicker();
|
||||
this.clearMails = false;
|
||||
}
|
||||
},
|
||||
replaceText(message) {
|
||||
|
@ -360,7 +376,7 @@ export default {
|
|||
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
|
||||
const { can_reply: canReply } = this.currentChat;
|
||||
|
||||
if (canReply || this.isATwilioWhatsappChannel) this.replyType = mode;
|
||||
if (canReply || this.isAWhatsappChannel) this.replyType = mode;
|
||||
if (this.showRichContentEditor) {
|
||||
return;
|
||||
}
|
||||
|
@ -372,6 +388,7 @@ export default {
|
|||
clearMessage() {
|
||||
this.message = '';
|
||||
this.attachedFiles = [];
|
||||
this.clearMails = true;
|
||||
},
|
||||
toggleEmojiPicker() {
|
||||
this.showEmojiPicker = !this.showEmojiPicker;
|
||||
|
@ -448,11 +465,23 @@ export default {
|
|||
messagePayload.file = attachment.resource.file;
|
||||
}
|
||||
|
||||
if (this.ccEmails) {
|
||||
messagePayload.ccEmails = this.ccEmails;
|
||||
}
|
||||
|
||||
if (this.bccEmails) {
|
||||
messagePayload.bccEmails = this.bccEmails;
|
||||
}
|
||||
|
||||
return messagePayload;
|
||||
},
|
||||
setFormatMode(value) {
|
||||
this.updateUISettings({ display_rich_content_editor: value });
|
||||
},
|
||||
setCcEmails(value) {
|
||||
this.bccEmails = value.bccEmails;
|
||||
this.ccEmails = value.ccEmails;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="input-group-wrap">
|
||||
<div class="input-group small" :class="{ error: $v.ccEmails.$error }">
|
||||
<div class="input-group small" :class="{ error: $v.ccEmailsVal.$error }">
|
||||
<label class="input-group-label">
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.LABEL') }}
|
||||
</label>
|
||||
<div class="input-group-field">
|
||||
<woot-input
|
||||
v-model.trim="ccEmails"
|
||||
v-model.trim="$v.ccEmailsVal.$model"
|
||||
type="email"
|
||||
:class="{ error: $v.ccEmails.$error }"
|
||||
:class="{ error: $v.ccEmailsVal.$error }"
|
||||
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
|
||||
@blur="$v.ccEmails.$touch"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</div>
|
||||
<woot-button
|
||||
|
@ -23,28 +23,28 @@
|
|||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.ADD_BCC') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<span v-if="$v.ccEmails.$error" class="message">
|
||||
<span v-if="$v.ccEmailsVal.$error" class="message">
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.ERROR') }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="showBcc" class="input-group-wrap">
|
||||
<div class="input-group small" :class="{ error: $v.bccEmails.$error }">
|
||||
<div class="input-group small" :class="{ error: $v.bccEmailsVal.$error }">
|
||||
<label class="input-group-label">
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.LABEL') }}
|
||||
</label>
|
||||
<div class="input-group-field">
|
||||
<woot-input
|
||||
v-model.trim="bccEmails"
|
||||
v-model.trim="$v.bccEmailsVal.$model"
|
||||
type="email"
|
||||
:class="{ error: $v.bccEmails.$error }"
|
||||
:class="{ error: $v.bccEmailsVal.$error }"
|
||||
:placeholder="
|
||||
$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER')
|
||||
"
|
||||
@blur="$v.bccEmails.$touch"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span v-if="$v.bccEmails.$error" class="message">
|
||||
<span v-if="$v.bccEmailsVal.$error" class="message">
|
||||
{{ $t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.ERROR') }}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -55,27 +55,25 @@
|
|||
import { validEmailsByComma } from './helpers/emailHeadHelper';
|
||||
export default {
|
||||
props: {
|
||||
ccEmails: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
bccEmails: {
|
||||
type: String,
|
||||
default: '',
|
||||
clearMails: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showBcc: false,
|
||||
ccEmailsVal: '',
|
||||
bccEmailsVal: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
ccEmails: {
|
||||
ccEmailsVal: {
|
||||
hasValidEmails(value) {
|
||||
return validEmailsByComma(value);
|
||||
},
|
||||
},
|
||||
bccEmails: {
|
||||
bccEmailsVal: {
|
||||
hasValidEmails(value) {
|
||||
return validEmailsByComma(value);
|
||||
},
|
||||
|
@ -85,7 +83,20 @@ export default {
|
|||
handleAddBcc() {
|
||||
this.showBcc = true;
|
||||
},
|
||||
onBlur() {
|
||||
this.$v.$touch();
|
||||
this.$emit("set-emails", { bccEmails: this.bccEmailsVal, ccEmails: this.ccEmailsVal });
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
clearMails: function(value){
|
||||
if(value) {
|
||||
this.ccEmailsVal = '';
|
||||
this.bccEmailsVal = '';
|
||||
this.clearMails = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
<template>
|
||||
<div class="message-text--metadata">
|
||||
<span class="time">{{ readableTime }}</span>
|
||||
<span v-if="showSentIndicator" class="time">
|
||||
<i
|
||||
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
|
||||
class="icon ion-checkmark"
|
||||
/>
|
||||
</span>
|
||||
<i
|
||||
v-if="isEmail"
|
||||
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
|
||||
|
@ -36,8 +42,10 @@
|
|||
<script>
|
||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
|
||||
export default {
|
||||
mixins: [inboxMixin],
|
||||
props: {
|
||||
sender: {
|
||||
type: Object,
|
||||
|
@ -99,6 +107,9 @@ export default {
|
|||
return `https://twitter.com/${screenName ||
|
||||
this.inbox.name}/status/${sourceId}`;
|
||||
},
|
||||
showSentIndicator() {
|
||||
return this.isOutgoing && this.sourceId && this.isAnEmailChannel;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTweetReply() {
|
||||
|
@ -117,6 +128,10 @@ export default {
|
|||
color: var(--w-100);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
|
@ -127,13 +142,6 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
.ion-reply,
|
||||
.ion-android-open {
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.message-text--metadata {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
|
@ -192,6 +200,10 @@ export default {
|
|||
.time {
|
||||
color: var(--s-400);
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: var(--s-400);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-image {
|
||||
|
@ -201,4 +213,8 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delivered-icon {
|
||||
margin-left: -var(--space-normal);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -36,6 +36,14 @@ export default {
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
cc: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
bcc: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
toMails() {
|
||||
|
@ -43,11 +51,11 @@ export default {
|
|||
return to.join(', ');
|
||||
},
|
||||
ccMails() {
|
||||
const cc = this.emailAttributes.cc || [];
|
||||
const cc = this.emailAttributes.cc || this.cc || [];
|
||||
return cc.join(', ');
|
||||
},
|
||||
bccMails() {
|
||||
const bcc = this.emailAttributes.bcc || [];
|
||||
const bcc = this.emailAttributes.bcc || this.bcc || [];
|
||||
return bcc.join(', ');
|
||||
},
|
||||
subject() {
|
||||
|
|
|
@ -60,11 +60,9 @@ export default {
|
|||
.text-content {
|
||||
overflow: auto;
|
||||
|
||||
&::v-deep {
|
||||
ul,
|
||||
ol {
|
||||
margin-left: var(--space-normal);
|
||||
}
|
||||
ul,
|
||||
ol {
|
||||
padding-left: var(--space-two);
|
||||
}
|
||||
table {
|
||||
all: revert;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import VTooltip from 'v-tooltip';
|
||||
|
||||
import Button from 'dashboard/components/buttons/Button';
|
||||
import i18n from 'dashboard/i18n';
|
||||
|
@ -10,6 +11,7 @@ import MoreActions from '../MoreActions';
|
|||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
localVue.use(VueI18n);
|
||||
localVue.use(VTooltip);
|
||||
|
||||
localVue.component('woot-button', Button);
|
||||
|
||||
|
@ -63,21 +65,9 @@ describe('MoveActions', () => {
|
|||
moreActions = mount(MoreActions, { store, localVue, i18n: i18nConfig });
|
||||
});
|
||||
|
||||
it('opens the menu when user clicks "more"', async () => {
|
||||
expect(moreActions.find('.dropdown-pane').exists()).toBe(false);
|
||||
|
||||
await moreActions.find('.more--button').trigger('click');
|
||||
|
||||
expect(moreActions.find('.dropdown-pane').exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('muting discussion', () => {
|
||||
it('triggers "muteConversation"', async () => {
|
||||
await moreActions.find('.more--button').trigger('click');
|
||||
|
||||
await moreActions
|
||||
.find('.dropdown-pane button:first-child')
|
||||
.trigger('click');
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
|
||||
expect(muteConversation).toBeCalledWith(
|
||||
expect.any(Object),
|
||||
|
@ -87,11 +77,7 @@ describe('MoveActions', () => {
|
|||
});
|
||||
|
||||
it('shows alert', async () => {
|
||||
await moreActions.find('.more--button').trigger('click');
|
||||
|
||||
await moreActions
|
||||
.find('.dropdown-pane button:first-child')
|
||||
.trigger('click');
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
|
||||
expect(window.bus.$emit).toBeCalledWith(
|
||||
'newToastMessage',
|
||||
|
@ -106,11 +92,7 @@ describe('MoveActions', () => {
|
|||
});
|
||||
|
||||
it('triggers "unmuteConversation"', async () => {
|
||||
await moreActions.find('.more--button').trigger('click');
|
||||
|
||||
await moreActions
|
||||
.find('.dropdown-pane button:first-child')
|
||||
.trigger('click');
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
|
||||
expect(unmuteConversation).toBeCalledWith(
|
||||
expect.any(Object),
|
||||
|
@ -120,11 +102,7 @@ describe('MoveActions', () => {
|
|||
});
|
||||
|
||||
it('shows alert', async () => {
|
||||
await moreActions.find('.more--button').trigger('click');
|
||||
|
||||
await moreActions
|
||||
.find('.dropdown-pane button:first-child')
|
||||
.trigger('click');
|
||||
await moreActions.find('button:first-child').trigger('click');
|
||||
|
||||
expect(window.bus.$emit).toBeCalledWith(
|
||||
'newToastMessage',
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
:value="value"
|
||||
:type="type"
|
||||
:placeholder="placeholder"
|
||||
:readonly="readonly"
|
||||
@input="onChange"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
|
@ -42,6 +43,10 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
deafaut: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onChange(e) {
|
||||
|
|
|
@ -10,6 +10,7 @@ export default {
|
|||
RESOLVED: 'resolved',
|
||||
PENDING: 'pending',
|
||||
SNOOZED: 'snoozed',
|
||||
ALL: 'all',
|
||||
},
|
||||
};
|
||||
export const DEFAULT_REDIRECT_URL = '/app/';
|
||||
|
|
|
@ -19,6 +19,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
'conversation.typing_off': this.onTypingOff,
|
||||
'conversation.contact_changed': this.onConversationContactChange,
|
||||
'presence.update': this.onPresenceUpdate,
|
||||
'contact.deleted': this.onContactDelete,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -33,7 +34,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
onPresenceUpdate = data => {
|
||||
this.app.$store.dispatch('contacts/updatePresence', data.contacts);
|
||||
this.app.$store.dispatch('agents/updatePresence', data.users);
|
||||
this.app.$store.dispatch('setCurrentUserAvailabilityStatus', data.users);
|
||||
this.app.$store.dispatch('setCurrentUserAvailability', data.users);
|
||||
};
|
||||
|
||||
onConversationContactChange = payload => {
|
||||
|
@ -115,6 +116,14 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
fetchConversationStats = () => {
|
||||
bus.$emit('fetch_conversation_stats');
|
||||
};
|
||||
|
||||
onContactDelete = data => {
|
||||
this.app.$store.dispatch(
|
||||
'contacts/deleteContactThroughConversations',
|
||||
data.id
|
||||
);
|
||||
this.fetchConversationStats();
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
|
|
6
app/javascript/dashboard/helper/downloadCsvFile.js
Normal file
6
app/javascript/dashboard/helper/downloadCsvFile.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const downloadCsvFile = (fileName, fileContent) => {
|
||||
const link = document.createElement('a');
|
||||
link.download = fileName;
|
||||
link.href = `data:text/csv;charset=utf-8,` + encodeURI(fileContent);
|
||||
link.click();
|
||||
};
|
|
@ -16,6 +16,9 @@ export const getInboxClassByType = (type, phoneNumber) => {
|
|||
? 'ion-social-whatsapp-outline'
|
||||
: 'ion-android-textsms';
|
||||
|
||||
case INBOX_TYPES.WHATSAPP:
|
||||
return 'ion-social-whatsapp-outline';
|
||||
|
||||
case INBOX_TYPES.API:
|
||||
return 'ion-cloud';
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { downloadCsvFile } from '../downloadCsvFile';
|
||||
|
||||
const fileName = 'test.csv';
|
||||
const fileData = `Agent name,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes)
|
||||
Pranav,36,114,28411`;
|
||||
|
||||
describe('#downloadCsvFile', () => {
|
||||
it('should download the csv file', () => {
|
||||
const link = {
|
||||
click: jest.fn(),
|
||||
};
|
||||
jest.spyOn(document, 'createElement').mockImplementation(() => link);
|
||||
|
||||
downloadCsvFile(fileName, fileData);
|
||||
expect(link.download).toEqual(fileName);
|
||||
expect(link.href).toEqual(
|
||||
`data:text/csv;charset=utf-8,${encodeURI(fileData)}`
|
||||
);
|
||||
expect(link.click).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -1,245 +1,13 @@
|
|||
import { frontendURL } from '../helper/URLHelper';
|
||||
import common from './sidebarItems/common';
|
||||
import contacts from './sidebarItems/contacts';
|
||||
import reports from './sidebarItems/reports';
|
||||
import campaigns from './sidebarItems/campaigns';
|
||||
import settings from './sidebarItems/settings';
|
||||
|
||||
export const getSidebarItems = accountId => ({
|
||||
common: {
|
||||
routes: [
|
||||
'home',
|
||||
'inbox_dashboard',
|
||||
'inbox_conversation',
|
||||
'conversation_through_inbox',
|
||||
'notifications_dashboard',
|
||||
'profile_settings',
|
||||
'profile_settings_index',
|
||||
'label_conversations',
|
||||
'conversations_through_label',
|
||||
'team_conversations',
|
||||
'conversations_through_team',
|
||||
'notifications_index',
|
||||
],
|
||||
menuItems: {
|
||||
assignedToMe: {
|
||||
icon: 'ion-chatbox-working',
|
||||
label: 'CONVERSATIONS',
|
||||
hasSubMenu: false,
|
||||
key: '',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
toolTip: 'Conversation from all subscribed inboxes',
|
||||
toStateName: 'home',
|
||||
},
|
||||
contacts: {
|
||||
icon: 'ion-person',
|
||||
label: 'CONTACTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||
toStateName: 'contacts_dashboard',
|
||||
},
|
||||
notifications: {
|
||||
icon: 'ion-ios-bell',
|
||||
label: 'NOTIFICATIONS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/notifications`),
|
||||
toStateName: 'notifications_dashboard',
|
||||
},
|
||||
report: {
|
||||
icon: 'ion-arrow-graph-up-right',
|
||||
label: 'REPORTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports`),
|
||||
toStateName: 'settings_account_reports',
|
||||
},
|
||||
campaigns: {
|
||||
icon: 'ion-speakerphone',
|
||||
label: 'CAMPAIGNS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
||||
toStateName: 'settings_account_campaigns',
|
||||
},
|
||||
settings: {
|
||||
icon: 'ion-settings',
|
||||
label: 'SETTINGS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings`),
|
||||
toStateName: 'settings_home',
|
||||
},
|
||||
},
|
||||
},
|
||||
contacts: {
|
||||
routes: [
|
||||
'contacts_dashboard',
|
||||
'contacts_dashboard_manage',
|
||||
'contacts_labels_dashboard',
|
||||
],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
contacts: {
|
||||
icon: 'ion-person',
|
||||
label: 'ALL_CONTACTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||
toStateName: 'contacts_dashboard',
|
||||
},
|
||||
},
|
||||
},
|
||||
reports: {
|
||||
routes: ['settings_account_reports', 'csat_reports'],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
reportOverview: {
|
||||
icon: 'ion-arrow-graph-up-right',
|
||||
label: 'REPORTS_OVERVIEW',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/overview`),
|
||||
toStateName: 'settings_account_reports',
|
||||
},
|
||||
csatReports: {
|
||||
icon: 'ion-happy',
|
||||
label: 'CSAT',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/reports/csat`),
|
||||
toStateName: 'csat_reports',
|
||||
},
|
||||
},
|
||||
},
|
||||
campaigns: {
|
||||
routes: ['settings_account_campaigns', 'one_off'],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
ongoingCampaigns: {
|
||||
icon: 'ion-arrow-swap',
|
||||
label: 'ONGOING',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns/ongoing`),
|
||||
toStateName: 'settings_account_campaigns',
|
||||
},
|
||||
onOffCampaigns: {
|
||||
icon: 'ion-radio-waves',
|
||||
label: 'ONE_OFF',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/campaigns/one_off`),
|
||||
toStateName: 'one_off',
|
||||
},
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
routes: [
|
||||
'agent_list',
|
||||
'canned_list',
|
||||
'labels_list',
|
||||
'settings_inbox',
|
||||
'attributes_list',
|
||||
'settings_inbox_new',
|
||||
'settings_inbox_list',
|
||||
'settings_inbox_show',
|
||||
'settings_inboxes_page_channel',
|
||||
'settings_inboxes_add_agents',
|
||||
'settings_inbox_finish',
|
||||
'settings_integrations',
|
||||
'settings_integrations_webhook',
|
||||
'settings_integrations_integration',
|
||||
'settings_applications',
|
||||
'settings_applications_webhook',
|
||||
'settings_applications_integration',
|
||||
'general_settings',
|
||||
'general_settings_index',
|
||||
'settings_teams_list',
|
||||
'settings_teams_new',
|
||||
'settings_teams_add_agents',
|
||||
'settings_teams_finish',
|
||||
'settings_teams_edit',
|
||||
'settings_teams_edit_members',
|
||||
'settings_teams_edit_finish',
|
||||
],
|
||||
menuItems: {
|
||||
back: {
|
||||
icon: 'ion-ios-arrow-back',
|
||||
label: 'HOME',
|
||||
hasSubMenu: false,
|
||||
toStateName: 'home',
|
||||
toState: frontendURL(`accounts/${accountId}/dashboard`),
|
||||
},
|
||||
agents: {
|
||||
icon: 'ion-person-stalker',
|
||||
label: 'AGENTS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/agents/list`),
|
||||
toStateName: 'agent_list',
|
||||
},
|
||||
teams: {
|
||||
icon: 'ion-ios-people',
|
||||
label: 'TEAMS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/teams/list`),
|
||||
toStateName: 'settings_teams_list',
|
||||
},
|
||||
inboxes: {
|
||||
icon: 'ion-archive',
|
||||
label: 'INBOXES',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/inboxes/list`),
|
||||
toStateName: 'settings_inbox_list',
|
||||
},
|
||||
labels: {
|
||||
icon: 'ion-pricetags',
|
||||
label: 'LABELS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/labels/list`),
|
||||
toStateName: 'labels_list',
|
||||
},
|
||||
attributes: {
|
||||
icon: 'ion-code',
|
||||
label: 'ATTRIBUTES',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/attributes/list`),
|
||||
toStateName: 'attributes_list',
|
||||
},
|
||||
cannedResponses: {
|
||||
icon: 'ion-chatbox-working',
|
||||
label: 'CANNED_RESPONSES',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(
|
||||
`accounts/${accountId}/settings/canned-response/list`
|
||||
),
|
||||
toStateName: 'canned_list',
|
||||
},
|
||||
settings_integrations: {
|
||||
icon: 'ion-flash',
|
||||
label: 'INTEGRATIONS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/integrations`),
|
||||
toStateName: 'settings_integrations',
|
||||
},
|
||||
settings_applications: {
|
||||
icon: 'ion-asterisk',
|
||||
label: 'APPLICATIONS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/applications`),
|
||||
toStateName: 'settings_applications',
|
||||
},
|
||||
general_settings_index: {
|
||||
icon: 'ion-gear-a',
|
||||
label: 'ACCOUNT_SETTINGS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/settings/general`),
|
||||
toStateName: 'general_settings_index',
|
||||
},
|
||||
},
|
||||
},
|
||||
common: common(accountId),
|
||||
contacts: contacts(accountId),
|
||||
reports: reports(accountId),
|
||||
campaigns: campaigns(accountId),
|
||||
settings: settings(accountId),
|
||||
});
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
"ERROR": "الوقت على الصفحة مطلوب"
|
||||
},
|
||||
"ENABLED": "تفعيل الحملة",
|
||||
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours",
|
||||
"SUBMIT": "إضافة حملة"
|
||||
},
|
||||
"API": {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"SEARCH": {
|
||||
"INPUT": "البحث عن جهات الاتصال، المحادثات، قوالب الردود الجاهزة .."
|
||||
},
|
||||
"FILTER_ALL": "الكل",
|
||||
"STATUS_TABS": [
|
||||
{
|
||||
"NAME": "فتح",
|
||||
|
@ -48,11 +49,11 @@
|
|||
},
|
||||
{
|
||||
"TEXT": "معلق",
|
||||
"VALUE": "pending"
|
||||
"VALUE": "معلق"
|
||||
},
|
||||
{
|
||||
"TEXT": "غفوة",
|
||||
"VALUE": "snoozed"
|
||||
"VALUE": "غفوة"
|
||||
}
|
||||
],
|
||||
"ATTACHMENTS": {
|
||||
|
@ -85,6 +86,8 @@
|
|||
"VIEW_TWEET_IN_TWITTER": "عرض التغريدة في تويتر",
|
||||
"REPLY_TO_TWEET": "الرد على هذه التغريدة",
|
||||
"NO_MESSAGES": "لا توجد رسائل",
|
||||
"NO_CONTENT": "لم يتم العثور على محتوى"
|
||||
"NO_CONTENT": "لم يتم العثور على محتوى",
|
||||
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
||||
"SHOW_QUOTED_TEXT": "Show Quoted Text"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@
|
|||
"NO_RESULT": "لم يتم العثور على تصنيفات"
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACT": "Merge contact",
|
||||
"CONTACT_ACTIONS": "Contact actions",
|
||||
"MUTE_CONTACT": "كتم المحادثة",
|
||||
"UNMUTE_CONTACT": "إلغاء كتم المحادثة",
|
||||
"MUTED_SUCCESS": "تم كتم هذه المحادثة لمدة 6 ساعات",
|
||||
|
@ -54,6 +56,35 @@
|
|||
"TITLE": "إنشاء جهة اتصال جديدة",
|
||||
"DESC": "إضافة معلومات أساسية حول جهة الاتصال."
|
||||
},
|
||||
"IMPORT_CONTACTS": {
|
||||
"BUTTON_LABEL": "Import",
|
||||
"TITLE": "Import Contacts",
|
||||
"DESC": "Import contacts through a CSV file.",
|
||||
"DOWNLOAD_LABEL": "Download a sample csv.",
|
||||
"FORM": {
|
||||
"LABEL": "CSV File",
|
||||
"SUBMIT": "Import",
|
||||
"CANCEL": "إلغاء"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
||||
"ERROR_MESSAGE": "حدث خطأ، الرجاء المحاولة مرة أخرى"
|
||||
},
|
||||
"DELETE_CONTACT": {
|
||||
"BUTTON_LABEL": "Delete Contact",
|
||||
"TITLE": "Delete contact",
|
||||
"DESC": "Delete contact details",
|
||||
"CONFIRM": {
|
||||
"TITLE": "تأكيد الحذف",
|
||||
"MESSAGE": "هل أنت متأكد من الحذف ",
|
||||
"PLACE_HOLDER": "Please type {contactName} to confirm",
|
||||
"YES": "نعم، احذف ",
|
||||
"NO": "لا، احتفظ "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not delete contact. Please try again later."
|
||||
}
|
||||
},
|
||||
"CONTACT_FORM": {
|
||||
"FORM": {
|
||||
"SUBMIT": "إرسال",
|
||||
|
@ -213,17 +244,19 @@
|
|||
},
|
||||
"MERGE_CONTACTS": {
|
||||
"TITLE": "دمج جهة الاتصال",
|
||||
"DESCRIPTION": "دمج جهة الاتصال مفيد عندما يكون لديك مدخلات مكررة لنفس جهة الاتصال. عملية الدمج تأخذ جهة اتصال رئيسية وتدمجها بجهة الاتصال المكررة. بعد الدمج، ستبقى جميع التفاصيل في جهة الاتصال الرئيسية كما هي. إذا لم يكن لدى جهة الاتصال الرئيسية حقل ، فسيتم استخدام القيمة من جهة الاتصال المكررة بعد الدمج. إذا حدث تضارب بالبيانات، ستبقى الحقول في جهة الاتصال الأساسية غير متأثرة، ولكن الحقول من جهة الاتصال الثانوية سيتم نسخها إلى السمات المخصصة في جهة الاتصال الرئيسية.",
|
||||
"DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact’ s attributes will take precedence.",
|
||||
"PRIMARY": {
|
||||
"TITLE": "جهة الاتصال الرئيسية"
|
||||
"TITLE": "جهة الاتصال الرئيسية",
|
||||
"HELP_LABEL": "To be kept"
|
||||
},
|
||||
"CHILD": {
|
||||
"TITLE": "دمج جهة الإتصال",
|
||||
"PLACEHOLDER": "اختر جهة اتصال"
|
||||
"PLACEHOLDER": "Search for a contact",
|
||||
"HELP_LABEL": "To be deleted"
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "ملخص",
|
||||
"DELETE_WARNING": "الاتصال بـ <strong>%{childContactName}</strong>سيتم حذفه.",
|
||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "سيتم نسخ تفاصيل الاتصال بـ <strong>%{childContactName}</strong> إلى <strong>%{primaryContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
|
@ -236,7 +269,7 @@
|
|||
"ERROR": "حدد جهة اتصال فرعية للدمج"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "تم دمج جهة الاتصال بنجاح",
|
||||
"ERROR_MESSAGE": "تعذر دمج جهات الاتصال ، حاول مرة أخرى!"
|
||||
"ERROR_MESSAGE": "Could not merge contacts, try again!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,10 @@
|
|||
"OPEN_ACTION": "فتح",
|
||||
"OPEN": "المزيد",
|
||||
"CLOSE": "أغلق",
|
||||
"DETAILS": "التفاصيل"
|
||||
"DETAILS": "التفاصيل",
|
||||
"SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow",
|
||||
"SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week",
|
||||
"SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply"
|
||||
},
|
||||
"RESOLVE_DROPDOWN": {
|
||||
"MARK_PENDING": "تحديد كمعلق",
|
||||
|
@ -84,6 +87,7 @@
|
|||
"CHANGE_AGENT": "تم تغيير الموظف الذي تم إحالة المحادثة إليه",
|
||||
"CHANGE_TEAM": "تم تغيير فريق المحادثة",
|
||||
"FILE_SIZE_LIMIT": "حجم الملف يتجاوز حد الاقصى وهو {MAXIMUM_FILE_UPLOAD_SIZE}",
|
||||
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
||||
"SENT_BY": "أرسلت بواسطة:",
|
||||
"ASSIGNMENT": {
|
||||
"SELECT_AGENT": "اختر وكيل",
|
||||
|
|
|
@ -71,5 +71,13 @@
|
|||
"assigned_conversation_new_message": "رسالة جديدة",
|
||||
"conversation_mention": "إشارة"
|
||||
}
|
||||
},
|
||||
"NETWORK": {
|
||||
"NOTIFICATION": {
|
||||
"TEXT": "Disconnected from Chatwoot"
|
||||
},
|
||||
"BUTTON": {
|
||||
"REFRESH": "Refresh"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,11 @@
|
|||
"CHANNEL_AVATAR": {
|
||||
"LABEL": "الصورة الرمزية للقناة"
|
||||
},
|
||||
"CHANNEL_WEBHOOK_URL": {
|
||||
"LABEL": "رابط Webhook",
|
||||
"PLACEHOLDER": "Enter your Webhook URL",
|
||||
"ERROR": "الرجاء إدخال عنوان URL صالح"
|
||||
},
|
||||
"CHANNEL_DOMAIN": {
|
||||
"LABEL": "نطاق الموقع",
|
||||
"PLACEHOLDER": "أدخل نطاق موقعك الإلكتروني (مثال: acme.com)"
|
||||
|
@ -92,8 +97,8 @@
|
|||
"SUBMIT_BUTTON": "إنشاء قناة تواصل"
|
||||
},
|
||||
"TWILIO": {
|
||||
"TITLE": "قناة Twilio SMS/WhatsApp",
|
||||
"DESC": "قم بإضافة قناة Twilio لتمكن عملائك من التواصل معك عبر الرسائل القصيرة SMS أو عبر واتساب.",
|
||||
"TITLE": "Twilio SMS/WhatsApp Channel",
|
||||
"DESC": "Integrate Twilio and start supporting your customers via SMS or WhatsApp.",
|
||||
"ACCOUNT_SID": {
|
||||
"LABEL": "معرف حساب Twilio (يعرف أيضاً بـ Account SID)",
|
||||
"PLACEHOLDER": "الرجاء إدخال معرف حساب Twilio الخاص بك (يعرف أيضاً بـ Account SID)",
|
||||
|
@ -109,8 +114,8 @@
|
|||
"ERROR": "هذا الحقل مطلوب"
|
||||
},
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "اسم القناة",
|
||||
"PLACEHOLDER": "الرجاء إدخال اسم القناة",
|
||||
"LABEL": "اسم صندوق الوارد لقناة التواصل",
|
||||
"PLACEHOLDER": "Please enter a inbox name",
|
||||
"ERROR": "هذا الحقل مطلوب"
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
|
@ -132,8 +137,34 @@
|
|||
"DESC": "ابدأ في دعم عملائك عبر الرسائل القصيرة بإستخدام Twilio."
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"TITLE": "قناة Whatsapp عبر Twilio",
|
||||
"DESC": "ابدأ في دعم عملائك عبر الواتساب بإستخدام Twilio."
|
||||
"TITLE": "WhatsApp Channel",
|
||||
"DESC": "Start supporting your customers via WhatsApp.",
|
||||
"PROVIDERS": {
|
||||
"LABEL": "API Provider",
|
||||
"TWILIO": "Twilio",
|
||||
"360_DIALOG": "360Dialog"
|
||||
},
|
||||
"INBOX_NAME": {
|
||||
"LABEL": "اسم صندوق الوارد لقناة التواصل",
|
||||
"PLACEHOLDER": "Please enter an inbox name",
|
||||
"ERROR": "هذا الحقل مطلوب"
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"LABEL": "رقم الهاتف",
|
||||
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
|
||||
"ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
|
||||
},
|
||||
"API_KEY": {
|
||||
"LABEL": "API key",
|
||||
"SUBTITLE": "Configure the WhatsApp API key.",
|
||||
"PLACEHOLDER": "API key",
|
||||
"APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here",
|
||||
"ERROR": "Please enter a valid value."
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||
}
|
||||
},
|
||||
"API_CHANNEL": {
|
||||
"TITLE": "قناة API",
|
||||
|
@ -195,6 +226,10 @@
|
|||
"SUBMIT_BUTTON": "Create LINE Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the LINE channel"
|
||||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "عنوان Callback URL",
|
||||
"SUBTITLE": "You have to configure the webhook URL in LINE application with the URL mentioned here."
|
||||
}
|
||||
},
|
||||
"TELEGRAM_CHANNEL": {
|
||||
|
@ -212,7 +247,7 @@
|
|||
},
|
||||
"AUTH": {
|
||||
"TITLE": "اختر قناة",
|
||||
"DESC": "شاتوت يدعم أداة الدردشة المباشرة، صفحة الفيسبوك، ملف تويتر الشخصي، واتسب، البريد الإلكتروني وما إلى ذلك، كقنوات. إذا كنت ترغب في إنشاء قناة مخصصة، يمكنك إنشاءها باستخدام قناة API. حدد قناة واحدة من الخيارات أدناه للمتابعة."
|
||||
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, WhatsApp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
|
||||
},
|
||||
"AGENTS": {
|
||||
"TITLE": "موظف الدعم",
|
||||
|
@ -266,6 +301,9 @@
|
|||
"ENABLE_CSAT": {
|
||||
"ENABLED": "مفعل",
|
||||
"DISABLED": "معطّل"
|
||||
},
|
||||
"ENABLE_HMAC": {
|
||||
"LABEL": "Enable"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
|
@ -315,6 +353,8 @@
|
|||
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
|
||||
"HMAC_VERIFICATION": "التحقق من هوية المستخدم",
|
||||
"HMAC_DESCRIPTION": "Inorder to validate the user's identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here.",
|
||||
"HMAC_MANDATORY_VERIFICATION": "Enforce User Identity Validation",
|
||||
"HMAC_MANDATORY_DESCRIPTION": "If enabled, Chatwoot SDKs setUser method will not work unless the `identifier_hash` is provided for each user.",
|
||||
"INBOX_IDENTIFIER": "Inbox Identifier",
|
||||
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
|
||||
"FORWARD_EMAIL_TITLE": "Forward to Email",
|
||||
|
@ -350,7 +390,7 @@
|
|||
"TIMEZONE_LABEL": "اختر المنطقة الزمنية",
|
||||
"UPDATE": "تحديث إعدادات ساعات العمل",
|
||||
"TOGGLE_AVAILABILITY": "تمكين توافر العمل لهذا البريد الوارد",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "رسالة غير متاح للزائرين",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "نحن غير متوفرين في هذه اللحظة. اترك رسالة سنرد عليها بمجرد عودتنا.",
|
||||
"TOGGLE_HELP": "تمكين توفر العمل سيظهر الساعات المتاحة على أداة الدردشة المباشرة حتى لو كان جميع الوكلاء غير متصلين بالإنترنت. خارج الساعات المتاحة يمكن تحذير الزوار برسالة ونموذج ما قبل الدردشة.",
|
||||
"DAY": {
|
||||
|
|
|
@ -61,6 +61,258 @@
|
|||
"PLACEHOLDER": "اختر نطاق المدة"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
"HEADER": "Agents Overview",
|
||||
"LOADING_CHART": "تحميل بيانات الرسم البياني...",
|
||||
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
|
||||
"DOWNLOAD_AGENT_REPORTS": "تنزيل تقارير الوكيل",
|
||||
"FILTER_DROPDOWN_LABEL": "اختر وكيل",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "المحادثات",
|
||||
"DESC": "(الإجمالي)"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "الرسائل الواردة",
|
||||
"DESC": "(الإجمالي)"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "الرسائل الصادرة",
|
||||
"DESC": "(الإجمالي)"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "وقت الاستجابة الأولى",
|
||||
"DESC": "(متوسط)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "وقت إغلاق المحادثات",
|
||||
"DESC": "(متوسط)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "عدد مرات الإغلاق",
|
||||
"DESC": "(الإجمالي)"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "آخر 7 أيام"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "آخر 30 يوماً"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "العام الماضي"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "تحديد نطاق المدة"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "تطبيق",
|
||||
"PLACEHOLDER": "اختر نطاق المدة"
|
||||
}
|
||||
},
|
||||
"LABEL_REPORTS": {
|
||||
"HEADER": "Labels Overview",
|
||||
"LOADING_CHART": "تحميل بيانات الرسم البياني...",
|
||||
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
|
||||
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Label",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "المحادثات",
|
||||
"DESC": "(الإجمالي)"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "الرسائل الواردة",
|
||||
"DESC": "(الإجمالي)"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "الرسائل الصادرة",
|
||||
"DESC": "(الإجمالي)"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "وقت الاستجابة الأولى",
|
||||
"DESC": "(متوسط)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "وقت إغلاق المحادثات",
|
||||
"DESC": "(متوسط)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "عدد مرات الإغلاق",
|
||||
"DESC": "(الإجمالي)"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "آخر 7 أيام"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "آخر 30 يوماً"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "العام الماضي"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "تحديد نطاق المدة"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "تطبيق",
|
||||
"PLACEHOLDER": "اختر نطاق المدة"
|
||||
}
|
||||
},
|
||||
"INBOX_REPORTS": {
|
||||
"HEADER": "Inbox Overview",
|
||||
"LOADING_CHART": "تحميل بيانات الرسم البياني...",
|
||||
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
|
||||
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
|
||||
"FILTER_DROPDOWN_LABEL": "اختر صندوق الوارد",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "المحادثات",
|
||||
"DESC": "(الإجمالي)"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "الرسائل الواردة",
|
||||
"DESC": "(الإجمالي)"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "الرسائل الصادرة",
|
||||
"DESC": "(الإجمالي)"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "وقت الاستجابة الأولى",
|
||||
"DESC": "(متوسط)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "وقت إغلاق المحادثات",
|
||||
"DESC": "(متوسط)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "عدد مرات الإغلاق",
|
||||
"DESC": "(الإجمالي)"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "آخر 7 أيام"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "آخر 30 يوماً"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "العام الماضي"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "تحديد نطاق المدة"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "تطبيق",
|
||||
"PLACEHOLDER": "اختر نطاق المدة"
|
||||
}
|
||||
},
|
||||
"TEAM_REPORTS": {
|
||||
"HEADER": "Team Overview",
|
||||
"LOADING_CHART": "تحميل بيانات الرسم البياني...",
|
||||
"NO_ENOUGH_DATA": "لم يتم جمع بيانات بقدر كافي لإنشاء التقرير، الرجاء المحاولة مرة أخرى لاحقاً.",
|
||||
"DOWNLOAD_TEAM_REPORTS": "Download team reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Team",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "المحادثات",
|
||||
"DESC": "(الإجمالي)"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "الرسائل الواردة",
|
||||
"DESC": "(الإجمالي)"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "الرسائل الصادرة",
|
||||
"DESC": "(الإجمالي)"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "وقت الاستجابة الأولى",
|
||||
"DESC": "(متوسط)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "وقت إغلاق المحادثات",
|
||||
"DESC": "(متوسط)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "عدد مرات الإغلاق",
|
||||
"DESC": "(الإجمالي)"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "آخر 7 أيام"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "آخر 30 يوماً"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "العام الماضي"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "تحديد نطاق المدة"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "تطبيق",
|
||||
"PLACEHOLDER": "اختر نطاق المدة"
|
||||
}
|
||||
},
|
||||
"CSAT_REPORTS": {
|
||||
"HEADER": "تقارير CSAT",
|
||||
"NO_RECORDS": "لا توجد ردود متوفرة على الدراسة الاستقصائية CSAT.",
|
||||
|
@ -87,4 +339,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -150,7 +150,11 @@
|
|||
"CSAT": "CSAT",
|
||||
"CAMPAIGNS": "الحملات",
|
||||
"ONGOING": "جارية",
|
||||
"ONE_OFF": "إيقاف واحد"
|
||||
"ONE_OFF": "إيقاف واحد",
|
||||
"REPORTS_AGENT": "موظف الدعم",
|
||||
"REPORTS_LABEL": "الوسوم",
|
||||
"REPORTS_INBOX": "صندوق الوارد",
|
||||
"REPORTS_TEAM": "Team"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NO_ACCOUNT_WARNING": "أوه! لم نتمكن من العثور على الحساب. الرجاء إنشاء حساب جديد للمتابعة.",
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
"ERROR": "Time on page is required"
|
||||
},
|
||||
"ENABLED": "Enable campaign",
|
||||
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours",
|
||||
"SUBMIT": "Add Campaign"
|
||||
},
|
||||
"API": {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"SEARCH": {
|
||||
"INPUT": "Cerca persones, xats, respostes desades .."
|
||||
},
|
||||
"FILTER_ALL": "Totes",
|
||||
"STATUS_TABS": [
|
||||
{
|
||||
"NAME": "Obrir",
|
||||
|
@ -85,6 +86,8 @@
|
|||
"VIEW_TWEET_IN_TWITTER": "Veure el tuit a Twitter",
|
||||
"REPLY_TO_TWEET": "Respon a aquest tuit",
|
||||
"NO_MESSAGES": "Cap Missatge",
|
||||
"NO_CONTENT": "No content available"
|
||||
"NO_CONTENT": "No content available",
|
||||
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
||||
"SHOW_QUOTED_TEXT": "Show Quoted Text"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@
|
|||
"NO_RESULT": "No labels found"
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACT": "Merge contact",
|
||||
"CONTACT_ACTIONS": "Contact actions",
|
||||
"MUTE_CONTACT": "Silencia la conversa",
|
||||
"UNMUTE_CONTACT": "Desactiva el silenci de la conversa",
|
||||
"MUTED_SUCCESS": "Aquesta conversa s'ha silenciat durant 6 hores",
|
||||
|
@ -54,6 +56,35 @@
|
|||
"TITLE": "Crear un nou contacte",
|
||||
"DESC": "Afegir informació bàsica sobre el contacte."
|
||||
},
|
||||
"IMPORT_CONTACTS": {
|
||||
"BUTTON_LABEL": "Import",
|
||||
"TITLE": "Import Contacts",
|
||||
"DESC": "Import contacts through a CSV file.",
|
||||
"DOWNLOAD_LABEL": "Download a sample csv.",
|
||||
"FORM": {
|
||||
"LABEL": "CSV File",
|
||||
"SUBMIT": "Import",
|
||||
"CANCEL": "Cancel·la"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
||||
"ERROR_MESSAGE": "S'ha produït un error; tornau-ho a provar"
|
||||
},
|
||||
"DELETE_CONTACT": {
|
||||
"BUTTON_LABEL": "Delete Contact",
|
||||
"TITLE": "Delete contact",
|
||||
"DESC": "Delete contact details",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirma l'esborrat",
|
||||
"MESSAGE": "N'estas segur? ",
|
||||
"PLACE_HOLDER": "Please type {contactName} to confirm",
|
||||
"YES": "Si, esborra ",
|
||||
"NO": "No, segueix "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not delete contact. Please try again later."
|
||||
}
|
||||
},
|
||||
"CONTACT_FORM": {
|
||||
"FORM": {
|
||||
"SUBMIT": "Envia",
|
||||
|
@ -213,17 +244,19 @@
|
|||
},
|
||||
"MERGE_CONTACTS": {
|
||||
"TITLE": "Merge contacts",
|
||||
"DESCRIPTION": "Merge contact is helpful when you have duplicated entries of the same contact. Merging action takes a primary contact and a child contact. After merging, all details in the primary contact will remain the same. If the primary contact doesn't have a field, then the value from the child contact will be used after merging. If a conflict happens, fields in primary contact will remain unaffected, but fields from secondary will be copied to the custom attributes in the primary contact.",
|
||||
"DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact’ s attributes will take precedence.",
|
||||
"PRIMARY": {
|
||||
"TITLE": "Primary contact"
|
||||
"TITLE": "Primary contact",
|
||||
"HELP_LABEL": "To be kept"
|
||||
},
|
||||
"CHILD": {
|
||||
"TITLE": "Contact to merge",
|
||||
"PLACEHOLDER": "Choose a contact"
|
||||
"PLACEHOLDER": "Search for a contact",
|
||||
"HELP_LABEL": "To be deleted"
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Summary",
|
||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong>will be deleted.",
|
||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
|
@ -236,7 +269,7 @@
|
|||
"ERROR": "Select a child contact to merge"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contact merged successfully",
|
||||
"ERROR_MESSAGE": "Could not merge contcts, try again!"
|
||||
"ERROR_MESSAGE": "Could not merge contacts, try again!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,10 @@
|
|||
"OPEN_ACTION": "Obrir",
|
||||
"OPEN": "Més",
|
||||
"CLOSE": "Tanca",
|
||||
"DETAILS": "detalls"
|
||||
"DETAILS": "detalls",
|
||||
"SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow",
|
||||
"SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week",
|
||||
"SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply"
|
||||
},
|
||||
"RESOLVE_DROPDOWN": {
|
||||
"MARK_PENDING": "Mark as pending",
|
||||
|
@ -84,6 +87,7 @@
|
|||
"CHANGE_AGENT": "Assignació de la conversa canviat",
|
||||
"CHANGE_TEAM": "Conversation team changed",
|
||||
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
|
||||
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
||||
"SENT_BY": "Enviat per:",
|
||||
"ASSIGNMENT": {
|
||||
"SELECT_AGENT": "Seleccionar Agent",
|
||||
|
|
|
@ -71,5 +71,13 @@
|
|||
"assigned_conversation_new_message": "Missatge Nou",
|
||||
"conversation_mention": "Menció"
|
||||
}
|
||||
},
|
||||
"NETWORK": {
|
||||
"NOTIFICATION": {
|
||||
"TEXT": "Disconnected from Chatwoot"
|
||||
},
|
||||
"BUTTON": {
|
||||
"REFRESH": "Refresh"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,11 @@
|
|||
"CHANNEL_AVATAR": {
|
||||
"LABEL": "Avatar del canal"
|
||||
},
|
||||
"CHANNEL_WEBHOOK_URL": {
|
||||
"LABEL": "URL del webhook",
|
||||
"PLACEHOLDER": "Enter your Webhook URL",
|
||||
"ERROR": "Introduïu una URL vàlid"
|
||||
},
|
||||
"CHANNEL_DOMAIN": {
|
||||
"LABEL": "Domini del lloc web",
|
||||
"PLACEHOLDER": "Introduïu el vostre domini de lloc web (pe: acme.com)"
|
||||
|
@ -92,8 +97,8 @@
|
|||
"SUBMIT_BUTTON": "Crea la safata entrada"
|
||||
},
|
||||
"TWILIO": {
|
||||
"TITLE": "Canal Twilio SMS",
|
||||
"DESC": "Integra Twilio i comença a donar suport als teus clients mitjançant SMS.",
|
||||
"TITLE": "Twilio SMS/WhatsApp Channel",
|
||||
"DESC": "Integrate Twilio and start supporting your customers via SMS or WhatsApp.",
|
||||
"ACCOUNT_SID": {
|
||||
"LABEL": "Compte SID",
|
||||
"PLACEHOLDER": "Introduïu el vostre compte Twilio SID",
|
||||
|
@ -109,8 +114,8 @@
|
|||
"ERROR": "Aquest camp és obligatori"
|
||||
},
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "Nom del canal",
|
||||
"PLACEHOLDER": "Introduïu el nom del canal",
|
||||
"LABEL": "Nom de la safata d'entrada",
|
||||
"PLACEHOLDER": "Please enter a inbox name",
|
||||
"ERROR": "Aquest camp és obligatori"
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
|
@ -132,8 +137,34 @@
|
|||
"DESC": "Start supporting your customers via SMS with Twilio integration."
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"TITLE": "Whatsapp Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via Whatsapp with Twilio integration."
|
||||
"TITLE": "WhatsApp Channel",
|
||||
"DESC": "Start supporting your customers via WhatsApp.",
|
||||
"PROVIDERS": {
|
||||
"LABEL": "API Provider",
|
||||
"TWILIO": "Twilio",
|
||||
"360_DIALOG": "360Dialog"
|
||||
},
|
||||
"INBOX_NAME": {
|
||||
"LABEL": "Nom de la safata d'entrada",
|
||||
"PLACEHOLDER": "Please enter an inbox name",
|
||||
"ERROR": "Aquest camp és obligatori"
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"LABEL": "Número de telèfon",
|
||||
"PLACEHOLDER": "Introduïu el número de telèfon des del qual serà enviat el missatge.",
|
||||
"ERROR": "Introduïu un valor vàlid. El número de telèfon hauria de començar amb el signe `+`."
|
||||
},
|
||||
"API_KEY": {
|
||||
"LABEL": "API key",
|
||||
"SUBTITLE": "Configure the WhatsApp API key.",
|
||||
"PLACEHOLDER": "API key",
|
||||
"APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here",
|
||||
"ERROR": "Please enter a valid value."
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||
}
|
||||
},
|
||||
"API_CHANNEL": {
|
||||
"TITLE": "Canal de l'API",
|
||||
|
@ -195,6 +226,10 @@
|
|||
"SUBMIT_BUTTON": "Create LINE Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the LINE channel"
|
||||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "Callback URL",
|
||||
"SUBTITLE": "You have to configure the webhook URL in LINE application with the URL mentioned here."
|
||||
}
|
||||
},
|
||||
"TELEGRAM_CHANNEL": {
|
||||
|
@ -212,7 +247,7 @@
|
|||
},
|
||||
"AUTH": {
|
||||
"TITLE": "Choose a channel",
|
||||
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, Whatsapp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
|
||||
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, WhatsApp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
|
||||
},
|
||||
"AGENTS": {
|
||||
"TITLE": "Agents",
|
||||
|
@ -266,6 +301,9 @@
|
|||
"ENABLE_CSAT": {
|
||||
"ENABLED": "Habilita",
|
||||
"DISABLED": "Inhabilita"
|
||||
},
|
||||
"ENABLE_HMAC": {
|
||||
"LABEL": "Enable"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
|
@ -315,6 +353,8 @@
|
|||
"AUTO_ASSIGNMENT_SUB_TEXT": "Activa o desactiva l'assignació automàtica d'agents disponibles a les noves converses",
|
||||
"HMAC_VERIFICATION": "Validació de la Identitat del Usuari",
|
||||
"HMAC_DESCRIPTION": "Inorder to validate the user's identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here.",
|
||||
"HMAC_MANDATORY_VERIFICATION": "Enforce User Identity Validation",
|
||||
"HMAC_MANDATORY_DESCRIPTION": "If enabled, Chatwoot SDKs setUser method will not work unless the `identifier_hash` is provided for each user.",
|
||||
"INBOX_IDENTIFIER": "Inbox Identifier",
|
||||
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
|
||||
"FORWARD_EMAIL_TITLE": "Forward to Email",
|
||||
|
@ -350,7 +390,7 @@
|
|||
"TIMEZONE_LABEL": "Select timezone",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -61,6 +61,258 @@
|
|||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
"HEADER": "Agents Overview",
|
||||
"LOADING_CHART": "S'estan carregant dades del gràfic...",
|
||||
"NO_ENOUGH_DATA": "No hem rebut suficients punts de dades per generar l'informe. Torneu-ho a provar més endavant.",
|
||||
"DOWNLOAD_AGENT_REPORTS": "Descarregar Informes d'Agent",
|
||||
"FILTER_DROPDOWN_LABEL": "Seleccionar Agent",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Converses",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Missatges d'entrada",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Missatges de sortida",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "Primer temps de resposta",
|
||||
"DESC": "( Promig )"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Temps de resolució",
|
||||
"DESC": "( Promig )"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Total de resolucions",
|
||||
"DESC": "( Total )"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Últims 7 dies"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Últims 30 dies"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"LABEL_REPORTS": {
|
||||
"HEADER": "Labels Overview",
|
||||
"LOADING_CHART": "S'estan carregant dades del gràfic...",
|
||||
"NO_ENOUGH_DATA": "No hem rebut suficients punts de dades per generar l'informe. Torneu-ho a provar més endavant.",
|
||||
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Label",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Converses",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Missatges d'entrada",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Missatges de sortida",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "Primer temps de resposta",
|
||||
"DESC": "( Promig )"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Temps de resolució",
|
||||
"DESC": "( Promig )"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Total de resolucions",
|
||||
"DESC": "( Total )"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Últims 7 dies"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Últims 30 dies"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"INBOX_REPORTS": {
|
||||
"HEADER": "Inbox Overview",
|
||||
"LOADING_CHART": "S'estan carregant dades del gràfic...",
|
||||
"NO_ENOUGH_DATA": "No hem rebut suficients punts de dades per generar l'informe. Torneu-ho a provar més endavant.",
|
||||
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Inbox",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Converses",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Missatges d'entrada",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Missatges de sortida",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "Primer temps de resposta",
|
||||
"DESC": "( Promig )"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Temps de resolució",
|
||||
"DESC": "( Promig )"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Total de resolucions",
|
||||
"DESC": "( Total )"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Últims 7 dies"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Últims 30 dies"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"TEAM_REPORTS": {
|
||||
"HEADER": "Team Overview",
|
||||
"LOADING_CHART": "S'estan carregant dades del gràfic...",
|
||||
"NO_ENOUGH_DATA": "No hem rebut suficients punts de dades per generar l'informe. Torneu-ho a provar més endavant.",
|
||||
"DOWNLOAD_TEAM_REPORTS": "Download team reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Team",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Converses",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Missatges d'entrada",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Missatges de sortida",
|
||||
"DESC": "( Total )"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "Primer temps de resposta",
|
||||
"DESC": "( Promig )"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Temps de resolució",
|
||||
"DESC": "( Promig )"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Total de resolucions",
|
||||
"DESC": "( Total )"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Últims 7 dies"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Últims 30 dies"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"CSAT_REPORTS": {
|
||||
"HEADER": "CSAT Reports",
|
||||
"NO_RECORDS": "There are no CSAT survey responses available.",
|
||||
|
@ -87,4 +339,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -150,7 +150,11 @@
|
|||
"CSAT": "CSAT",
|
||||
"CAMPAIGNS": "Campaigns",
|
||||
"ONGOING": "Ongoing",
|
||||
"ONE_OFF": "One off"
|
||||
"ONE_OFF": "One off",
|
||||
"REPORTS_AGENT": "Agents",
|
||||
"REPORTS_LABEL": "Etiquetes",
|
||||
"REPORTS_INBOX": "Inbox",
|
||||
"REPORTS_TEAM": "Team"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
"ERROR": "Time on page is required"
|
||||
},
|
||||
"ENABLED": "Enable campaign",
|
||||
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours",
|
||||
"SUBMIT": "Add Campaign"
|
||||
},
|
||||
"API": {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"SEARCH": {
|
||||
"INPUT": "Hledat lidi, chaty, Uložené odpovědi .."
|
||||
},
|
||||
"FILTER_ALL": "Vše",
|
||||
"STATUS_TABS": [
|
||||
{
|
||||
"NAME": "Otevřít",
|
||||
|
@ -48,11 +49,11 @@
|
|||
},
|
||||
{
|
||||
"TEXT": "Čekající",
|
||||
"VALUE": "pending"
|
||||
"VALUE": "čekající"
|
||||
},
|
||||
{
|
||||
"TEXT": "Odložené",
|
||||
"VALUE": "snoozed"
|
||||
"VALUE": "odložené"
|
||||
}
|
||||
],
|
||||
"ATTACHMENTS": {
|
||||
|
@ -85,6 +86,8 @@
|
|||
"VIEW_TWEET_IN_TWITTER": "Zobrazit tweet na Twitteru",
|
||||
"REPLY_TO_TWEET": "Odpovědět na tento tweet",
|
||||
"NO_MESSAGES": "Žádné zprávy",
|
||||
"NO_CONTENT": "Žádný obsah k dispozici"
|
||||
"NO_CONTENT": "Žádný obsah k dispozici",
|
||||
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
||||
"SHOW_QUOTED_TEXT": "Show Quoted Text"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@
|
|||
"NO_RESULT": "No labels found"
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACT": "Merge contact",
|
||||
"CONTACT_ACTIONS": "Contact actions",
|
||||
"MUTE_CONTACT": "Ztlumit konverzaci",
|
||||
"UNMUTE_CONTACT": "Zrušit ztlumení konverzace",
|
||||
"MUTED_SUCCESS": "Tato konverzace je ztlumena na 6 hodin",
|
||||
|
@ -54,6 +56,35 @@
|
|||
"TITLE": "Vytvořit nový kontakt",
|
||||
"DESC": "Přidat základní informace o kontaktu."
|
||||
},
|
||||
"IMPORT_CONTACTS": {
|
||||
"BUTTON_LABEL": "Import",
|
||||
"TITLE": "Import Contacts",
|
||||
"DESC": "Import contacts through a CSV file.",
|
||||
"DOWNLOAD_LABEL": "Download a sample csv.",
|
||||
"FORM": {
|
||||
"LABEL": "CSV File",
|
||||
"SUBMIT": "Import",
|
||||
"CANCEL": "Zrušit"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
||||
"ERROR_MESSAGE": "Došlo k chybě, zkuste to prosím znovu"
|
||||
},
|
||||
"DELETE_CONTACT": {
|
||||
"BUTTON_LABEL": "Delete Contact",
|
||||
"TITLE": "Delete contact",
|
||||
"DESC": "Delete contact details",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Potvrdit odstranění",
|
||||
"MESSAGE": "Opravdu chcete odstranit ",
|
||||
"PLACE_HOLDER": "Please type {contactName} to confirm",
|
||||
"YES": "Ano, odstranit ",
|
||||
"NO": "Ne, zachovat "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not delete contact. Please try again later."
|
||||
}
|
||||
},
|
||||
"CONTACT_FORM": {
|
||||
"FORM": {
|
||||
"SUBMIT": "Odeslat",
|
||||
|
@ -213,17 +244,19 @@
|
|||
},
|
||||
"MERGE_CONTACTS": {
|
||||
"TITLE": "Merge contacts",
|
||||
"DESCRIPTION": "Merge contact is helpful when you have duplicated entries of the same contact. Merging action takes a primary contact and a child contact. After merging, all details in the primary contact will remain the same. If the primary contact doesn't have a field, then the value from the child contact will be used after merging. If a conflict happens, fields in primary contact will remain unaffected, but fields from secondary will be copied to the custom attributes in the primary contact.",
|
||||
"DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact’ s attributes will take precedence.",
|
||||
"PRIMARY": {
|
||||
"TITLE": "Primary contact"
|
||||
"TITLE": "Primary contact",
|
||||
"HELP_LABEL": "To be kept"
|
||||
},
|
||||
"CHILD": {
|
||||
"TITLE": "Contact to merge",
|
||||
"PLACEHOLDER": "Choose a contact"
|
||||
"PLACEHOLDER": "Search for a contact",
|
||||
"HELP_LABEL": "To be deleted"
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Summary",
|
||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong>will be deleted.",
|
||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
|
@ -236,7 +269,7 @@
|
|||
"ERROR": "Select a child contact to merge"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contact merged successfully",
|
||||
"ERROR_MESSAGE": "Could not merge contcts, try again!"
|
||||
"ERROR_MESSAGE": "Could not merge contacts, try again!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,10 @@
|
|||
"OPEN_ACTION": "Otevřít",
|
||||
"OPEN": "Více",
|
||||
"CLOSE": "Zavřít",
|
||||
"DETAILS": "Podrobnosti"
|
||||
"DETAILS": "Podrobnosti",
|
||||
"SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow",
|
||||
"SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week",
|
||||
"SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply"
|
||||
},
|
||||
"RESOLVE_DROPDOWN": {
|
||||
"MARK_PENDING": "Mark as pending",
|
||||
|
@ -84,6 +87,7 @@
|
|||
"CHANGE_AGENT": "Konverzace pověřená osoba změněna",
|
||||
"CHANGE_TEAM": "Tým konverzace se změnil",
|
||||
"FILE_SIZE_LIMIT": "Soubor překračuje limit {MAXIMUM_FILE_UPLOAD_SIZE} přílohy",
|
||||
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
||||
"SENT_BY": "Odeslal:",
|
||||
"ASSIGNMENT": {
|
||||
"SELECT_AGENT": "Vybrat agenta",
|
||||
|
|
|
@ -71,5 +71,13 @@
|
|||
"assigned_conversation_new_message": "Nová zpráva",
|
||||
"conversation_mention": "Zmínka"
|
||||
}
|
||||
},
|
||||
"NETWORK": {
|
||||
"NOTIFICATION": {
|
||||
"TEXT": "Disconnected from Chatwoot"
|
||||
},
|
||||
"BUTTON": {
|
||||
"REFRESH": "Refresh"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,11 @@
|
|||
"CHANNEL_AVATAR": {
|
||||
"LABEL": "Avatar kanálu"
|
||||
},
|
||||
"CHANNEL_WEBHOOK_URL": {
|
||||
"LABEL": "URL webového háčku",
|
||||
"PLACEHOLDER": "Enter your Webhook URL",
|
||||
"ERROR": "Zadejte prosím platnou URL"
|
||||
},
|
||||
"CHANNEL_DOMAIN": {
|
||||
"LABEL": "Doména webových stránek",
|
||||
"PLACEHOLDER": "Zadejte doménu webu (např. acme.com)"
|
||||
|
@ -92,8 +97,8 @@
|
|||
"SUBMIT_BUTTON": "Vytvořit doručenou poštu"
|
||||
},
|
||||
"TWILIO": {
|
||||
"TITLE": "Twilio SMS/Whatsapp Channel",
|
||||
"DESC": "Integrate Twilio and start supporting your customers via SMS or Whatsapp.",
|
||||
"TITLE": "Twilio SMS/WhatsApp Channel",
|
||||
"DESC": "Integrate Twilio and start supporting your customers via SMS or WhatsApp.",
|
||||
"ACCOUNT_SID": {
|
||||
"LABEL": "SID účtu",
|
||||
"PLACEHOLDER": "Zadejte SID vašeho Twilio účtu",
|
||||
|
@ -109,8 +114,8 @@
|
|||
"ERROR": "Toto pole je povinné"
|
||||
},
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "Název kanálu",
|
||||
"PLACEHOLDER": "Zadejte název kanálu",
|
||||
"LABEL": "Název schránky",
|
||||
"PLACEHOLDER": "Please enter a inbox name",
|
||||
"ERROR": "Toto pole je povinné"
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
|
@ -132,8 +137,34 @@
|
|||
"DESC": "Start supporting your customers via SMS with Twilio integration."
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"TITLE": "Whatsapp Channel via Twilio",
|
||||
"DESC": "Start supporting your customers via Whatsapp with Twilio integration."
|
||||
"TITLE": "WhatsApp Channel",
|
||||
"DESC": "Start supporting your customers via WhatsApp.",
|
||||
"PROVIDERS": {
|
||||
"LABEL": "API Provider",
|
||||
"TWILIO": "Twilio",
|
||||
"360_DIALOG": "360Dialog"
|
||||
},
|
||||
"INBOX_NAME": {
|
||||
"LABEL": "Název schránky",
|
||||
"PLACEHOLDER": "Please enter an inbox name",
|
||||
"ERROR": "Toto pole je povinné"
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"LABEL": "Telefonní číslo",
|
||||
"PLACEHOLDER": "Zadejte prosím telefonní číslo, ze kterého bude zpráva odeslána.",
|
||||
"ERROR": "Zadejte platnou hodnotu. Telefonní číslo by mělo začínat znakem `+`."
|
||||
},
|
||||
"API_KEY": {
|
||||
"LABEL": "API key",
|
||||
"SUBTITLE": "Configure the WhatsApp API key.",
|
||||
"PLACEHOLDER": "API key",
|
||||
"APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here",
|
||||
"ERROR": "Please enter a valid value."
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the WhatsApp channel"
|
||||
}
|
||||
},
|
||||
"API_CHANNEL": {
|
||||
"TITLE": "API Channel",
|
||||
|
@ -195,6 +226,10 @@
|
|||
"SUBMIT_BUTTON": "Create LINE Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the LINE channel"
|
||||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "Callback URL",
|
||||
"SUBTITLE": "You have to configure the webhook URL in LINE application with the URL mentioned here."
|
||||
}
|
||||
},
|
||||
"TELEGRAM_CHANNEL": {
|
||||
|
@ -212,7 +247,7 @@
|
|||
},
|
||||
"AUTH": {
|
||||
"TITLE": "Choose a channel",
|
||||
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, Whatsapp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
|
||||
"DESC": "Chatwoot supports live-chat widget, Facebook page, Twitter profile, WhatsApp, Email etc., as channels. If you want to build a custom channel, you can create it using the API channel. Select one channel from the options below to proceed."
|
||||
},
|
||||
"AGENTS": {
|
||||
"TITLE": "Agenti",
|
||||
|
@ -266,6 +301,9 @@
|
|||
"ENABLE_CSAT": {
|
||||
"ENABLED": "Povoleno",
|
||||
"DISABLED": "Zakázáno"
|
||||
},
|
||||
"ENABLE_HMAC": {
|
||||
"LABEL": "Enable"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
|
@ -315,6 +353,8 @@
|
|||
"AUTO_ASSIGNMENT_SUB_TEXT": "Povolit nebo zakázat automatické přiřazování nových konverzací agentům přidaným do této schránky.",
|
||||
"HMAC_VERIFICATION": "User Identity Validation",
|
||||
"HMAC_DESCRIPTION": "Inorder to validate the user's identity, the SDK allows you to pass an `identifier_hash` for each user. You can generate HMAC using 'sha256' with the key shown here.",
|
||||
"HMAC_MANDATORY_VERIFICATION": "Enforce User Identity Validation",
|
||||
"HMAC_MANDATORY_DESCRIPTION": "If enabled, Chatwoot SDKs setUser method will not work unless the `identifier_hash` is provided for each user.",
|
||||
"INBOX_IDENTIFIER": "Inbox Identifier",
|
||||
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
|
||||
"FORWARD_EMAIL_TITLE": "Forward to Email",
|
||||
|
@ -350,7 +390,7 @@
|
|||
"TIMEZONE_LABEL": "Vyberte časové pásmo",
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for vistors",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
|
|
|
@ -61,6 +61,258 @@
|
|||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"AGENT_REPORTS": {
|
||||
"HEADER": "Agents Overview",
|
||||
"LOADING_CHART": "Načítání dat mapy...",
|
||||
"NO_ENOUGH_DATA": "Pro vytvoření hlášení jsme neobdrželi dostatek dat, zkuste to prosím později.",
|
||||
"DOWNLOAD_AGENT_REPORTS": "Stáhnout reporty agentů",
|
||||
"FILTER_DROPDOWN_LABEL": "Vybrat agenta",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Konverzace",
|
||||
"DESC": "( celkem)"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Příchozí zprávy",
|
||||
"DESC": "( celkem)"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Odchozí zprávy",
|
||||
"DESC": "( celkem)"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "Čas první odpovědi",
|
||||
"DESC": "(Průměrný)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Čas rozlišení",
|
||||
"DESC": "(Průměrný)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Počet rozlišení",
|
||||
"DESC": "( celkem)"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Posledních 7 dní"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Posledních 30 dní"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"LABEL_REPORTS": {
|
||||
"HEADER": "Labels Overview",
|
||||
"LOADING_CHART": "Načítání dat mapy...",
|
||||
"NO_ENOUGH_DATA": "Pro vytvoření hlášení jsme neobdrželi dostatek dat, zkuste to prosím později.",
|
||||
"DOWNLOAD_LABEL_REPORTS": "Download label reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Label",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Konverzace",
|
||||
"DESC": "( celkem)"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Příchozí zprávy",
|
||||
"DESC": "( celkem)"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Odchozí zprávy",
|
||||
"DESC": "( celkem)"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "Čas první odpovědi",
|
||||
"DESC": "(Průměrný)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Čas rozlišení",
|
||||
"DESC": "(Průměrný)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Počet rozlišení",
|
||||
"DESC": "( celkem)"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Posledních 7 dní"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Posledních 30 dní"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"INBOX_REPORTS": {
|
||||
"HEADER": "Inbox Overview",
|
||||
"LOADING_CHART": "Načítání dat mapy...",
|
||||
"NO_ENOUGH_DATA": "Pro vytvoření hlášení jsme neobdrželi dostatek dat, zkuste to prosím později.",
|
||||
"DOWNLOAD_INBOX_REPORTS": "Download inbox reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Inbox",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Konverzace",
|
||||
"DESC": "( celkem)"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Příchozí zprávy",
|
||||
"DESC": "( celkem)"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Odchozí zprávy",
|
||||
"DESC": "( celkem)"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "Čas první odpovědi",
|
||||
"DESC": "(Průměrný)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Čas rozlišení",
|
||||
"DESC": "(Průměrný)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Počet rozlišení",
|
||||
"DESC": "( celkem)"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Posledních 7 dní"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Posledních 30 dní"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"TEAM_REPORTS": {
|
||||
"HEADER": "Team Overview",
|
||||
"LOADING_CHART": "Načítání dat mapy...",
|
||||
"NO_ENOUGH_DATA": "Pro vytvoření hlášení jsme neobdrželi dostatek dat, zkuste to prosím později.",
|
||||
"DOWNLOAD_TEAM_REPORTS": "Download team reports",
|
||||
"FILTER_DROPDOWN_LABEL": "Select Team",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "Konverzace",
|
||||
"DESC": "( celkem)"
|
||||
},
|
||||
"INCOMING_MESSAGES": {
|
||||
"NAME": "Příchozí zprávy",
|
||||
"DESC": "( celkem)"
|
||||
},
|
||||
"OUTGOING_MESSAGES": {
|
||||
"NAME": "Odchozí zprávy",
|
||||
"DESC": "( celkem)"
|
||||
},
|
||||
"FIRST_RESPONSE_TIME": {
|
||||
"NAME": "Čas první odpovědi",
|
||||
"DESC": "(Průměrný)"
|
||||
},
|
||||
"RESOLUTION_TIME": {
|
||||
"NAME": "Čas rozlišení",
|
||||
"DESC": "(Průměrný)"
|
||||
},
|
||||
"RESOLUTION_COUNT": {
|
||||
"NAME": "Počet rozlišení",
|
||||
"DESC": "( celkem)"
|
||||
}
|
||||
},
|
||||
"DATE_RANGE": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Posledních 7 dní"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Posledních 30 dní"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Last 3 months"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Last 6 months"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Last year"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Custom date range"
|
||||
}
|
||||
],
|
||||
"CUSTOM_DATE_RANGE": {
|
||||
"CONFIRM": "Apply",
|
||||
"PLACEHOLDER": "Select date range"
|
||||
}
|
||||
},
|
||||
"CSAT_REPORTS": {
|
||||
"HEADER": "CSAT Reports",
|
||||
"NO_RECORDS": "There are no CSAT survey responses available.",
|
||||
|
@ -87,4 +339,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -150,7 +150,11 @@
|
|||
"CSAT": "CSAT",
|
||||
"CAMPAIGNS": "Kampaně",
|
||||
"ONGOING": "Ongoing",
|
||||
"ONE_OFF": "One off"
|
||||
"ONE_OFF": "One off",
|
||||
"REPORTS_AGENT": "Agenti",
|
||||
"REPORTS_LABEL": "Štítky",
|
||||
"REPORTS_INBOX": "Inbox",
|
||||
"REPORTS_TEAM": "Team"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
"ERROR": "Time on page is required"
|
||||
},
|
||||
"ENABLED": "Enable campaign",
|
||||
"TRIGGER_ONLY_BUSINESS_HOURS": "Trigger only during business hours",
|
||||
"SUBMIT": "Add Campaign"
|
||||
},
|
||||
"API": {
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"SEARCH": {
|
||||
"INPUT": "Søg efter Mennesker, Chats, Gemte svar .."
|
||||
},
|
||||
"FILTER_ALL": "Alle",
|
||||
"STATUS_TABS": [
|
||||
{
|
||||
"NAME": "Åbn",
|
||||
|
@ -85,6 +86,8 @@
|
|||
"VIEW_TWEET_IN_TWITTER": "Se tweet på Twitter",
|
||||
"REPLY_TO_TWEET": "Svar på dette tweet",
|
||||
"NO_MESSAGES": "No Messages",
|
||||
"NO_CONTENT": "No content available"
|
||||
"NO_CONTENT": "No content available",
|
||||
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
||||
"SHOW_QUOTED_TEXT": "Show Quoted Text"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@
|
|||
"NO_RESULT": "No labels found"
|
||||
}
|
||||
},
|
||||
"MERGE_CONTACT": "Merge contact",
|
||||
"CONTACT_ACTIONS": "Contact actions",
|
||||
"MUTE_CONTACT": "Gør Samtale Lydløs",
|
||||
"UNMUTE_CONTACT": "Fjern Lydløs",
|
||||
"MUTED_SUCCESS": "Denne samtale er gjort tavs i 6 timer",
|
||||
|
@ -54,6 +56,35 @@
|
|||
"TITLE": "Create new contact",
|
||||
"DESC": "Add basic information details about the contact."
|
||||
},
|
||||
"IMPORT_CONTACTS": {
|
||||
"BUTTON_LABEL": "Import",
|
||||
"TITLE": "Import Contacts",
|
||||
"DESC": "Import contacts through a CSV file.",
|
||||
"DOWNLOAD_LABEL": "Download a sample csv.",
|
||||
"FORM": {
|
||||
"LABEL": "CSV File",
|
||||
"SUBMIT": "Import",
|
||||
"CANCEL": "Annuller"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
||||
"ERROR_MESSAGE": "Der opstod en fejl. Prøv venligst igen"
|
||||
},
|
||||
"DELETE_CONTACT": {
|
||||
"BUTTON_LABEL": "Delete Contact",
|
||||
"TITLE": "Delete contact",
|
||||
"DESC": "Delete contact details",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Bekræft Sletning",
|
||||
"MESSAGE": "Er du sikker på du vil slette ",
|
||||
"PLACE_HOLDER": "Please type {contactName} to confirm",
|
||||
"YES": "Ja, Slet ",
|
||||
"NO": "Nej, Behold "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Contact deleted successfully",
|
||||
"ERROR_MESSAGE": "Could not delete contact. Please try again later."
|
||||
}
|
||||
},
|
||||
"CONTACT_FORM": {
|
||||
"FORM": {
|
||||
"SUBMIT": "Send",
|
||||
|
@ -213,17 +244,19 @@
|
|||
},
|
||||
"MERGE_CONTACTS": {
|
||||
"TITLE": "Merge contacts",
|
||||
"DESCRIPTION": "Merge contact is helpful when you have duplicated entries of the same contact. Merging action takes a primary contact and a child contact. After merging, all details in the primary contact will remain the same. If the primary contact doesn't have a field, then the value from the child contact will be used after merging. If a conflict happens, fields in primary contact will remain unaffected, but fields from secondary will be copied to the custom attributes in the primary contact.",
|
||||
"DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact’ s attributes will take precedence.",
|
||||
"PRIMARY": {
|
||||
"TITLE": "Primary contact"
|
||||
"TITLE": "Primary contact",
|
||||
"HELP_LABEL": "To be kept"
|
||||
},
|
||||
"CHILD": {
|
||||
"TITLE": "Contact to merge",
|
||||
"PLACEHOLDER": "Choose a contact"
|
||||
"PLACEHOLDER": "Search for a contact",
|
||||
"HELP_LABEL": "To be deleted"
|
||||
},
|
||||
"SUMMARY": {
|
||||
"TITLE": "Summary",
|
||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong>will be deleted.",
|
||||
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
|
||||
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
|
||||
},
|
||||
"SEARCH": {
|
||||
|
@ -236,7 +269,7 @@
|
|||
"ERROR": "Select a child contact to merge"
|
||||
},
|
||||
"SUCCESS_MESSAGE": "Contact merged successfully",
|
||||
"ERROR_MESSAGE": "Could not merge contcts, try again!"
|
||||
"ERROR_MESSAGE": "Could not merge contacts, try again!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,10 @@
|
|||
"OPEN_ACTION": "Åbn",
|
||||
"OPEN": "Mere",
|
||||
"CLOSE": "Luk",
|
||||
"DETAILS": "detaljer"
|
||||
"DETAILS": "detaljer",
|
||||
"SNOOZED_UNTIL_TOMORROW": "Snoozed until tomorrow",
|
||||
"SNOOZED_UNTIL_NEXT_WEEK": "Snoozed until next week",
|
||||
"SNOOZED_UNTIL_NEXT_REPLY": "Snoozed until next reply"
|
||||
},
|
||||
"RESOLVE_DROPDOWN": {
|
||||
"MARK_PENDING": "Mark as pending",
|
||||
|
@ -84,6 +87,7 @@
|
|||
"CHANGE_AGENT": "Samtaleansvarlig ændret",
|
||||
"CHANGE_TEAM": "Conversation team changed",
|
||||
"FILE_SIZE_LIMIT": "File exceeds the {MAXIMUM_FILE_UPLOAD_SIZE} attachment limit",
|
||||
"MESSAGE_ERROR": "Unable to send this message, please try again later",
|
||||
"SENT_BY": "Sent by:",
|
||||
"ASSIGNMENT": {
|
||||
"SELECT_AGENT": "Select Agent",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue