Merge branch 'release/3.1.0'
This commit is contained in:
commit
05df4d4212
@ -15,9 +15,9 @@ defaults: &defaults
|
|||||||
- image: cimg/postgres:15.3
|
- image: cimg/postgres:15.3
|
||||||
- image: cimg/redis:6.2.6
|
- image: cimg/redis:6.2.6
|
||||||
environment:
|
environment:
|
||||||
- RAILS_LOG_TO_STDOUT: false
|
- RAILS_LOG_TO_STDOUT: false
|
||||||
- COVERAGE: true
|
- COVERAGE: true
|
||||||
- LOG_LEVEL: warn
|
- LOG_LEVEL: warn
|
||||||
parallelism: 4
|
parallelism: 4
|
||||||
resource_class: large
|
resource_class: large
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ jobs:
|
|||||||
export NVM_DIR="$HOME/.nvm"
|
export NVM_DIR="$HOME/.nvm"
|
||||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
|
||||||
nvm install v16
|
nvm install v20
|
||||||
echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
|
echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV
|
||||||
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
|
echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
|
||||||
|
|
||||||
@ -64,7 +64,6 @@ jobs:
|
|||||||
- ~/.bundle
|
- ~/.bundle
|
||||||
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
||||||
|
|
||||||
|
|
||||||
# Only necessary if app uses webpacker or yarn in some other way
|
# Only necessary if app uses webpacker or yarn in some other way
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
keys:
|
keys:
|
||||||
@ -82,7 +81,7 @@ jobs:
|
|||||||
- ~/.cache/yarn
|
- ~/.cache/yarn
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: Download cc-test-reporter
|
name: Download cc-test-reporter
|
||||||
command: |
|
command: |
|
||||||
mkdir -p ~/tmp
|
mkdir -p ~/tmp
|
||||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
||||||
@ -182,7 +181,7 @@ jobs:
|
|||||||
- attach_workspace:
|
- attach_workspace:
|
||||||
at: ~/build
|
at: ~/build
|
||||||
- run:
|
- run:
|
||||||
name: Download cc-test-reporter
|
name: Download cc-test-reporter
|
||||||
command: |
|
command: |
|
||||||
mkdir -p ~/tmp
|
mkdir -p ~/tmp
|
||||||
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ~/tmp/cc-test-reporter
|
||||||
@ -204,4 +203,4 @@ workflows:
|
|||||||
- build
|
- build
|
||||||
- upload-coverage:
|
- upload-coverage:
|
||||||
requires:
|
requires:
|
||||||
- build
|
- build
|
||||||
|
|||||||
@ -199,6 +199,8 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
|
|||||||
## Rack Attack configuration
|
## Rack Attack configuration
|
||||||
## To prevent and throttle abusive requests
|
## To prevent and throttle abusive requests
|
||||||
# ENABLE_RACK_ATTACK=true
|
# ENABLE_RACK_ATTACK=true
|
||||||
|
# RACK_ATTACK_IP_LIMIT=3000
|
||||||
|
# ENABLE_RACK_ATTACK_WIDGET_API=true
|
||||||
|
|
||||||
## Running chatwoot as an API only server
|
## Running chatwoot as an API only server
|
||||||
## setting this value to true will disable the frontend dashboard endpoints
|
## setting this value to true will disable the frontend dashboard endpoints
|
||||||
|
|||||||
5
.github/workflows/run_foss_spec.yml
vendored
5
.github/workflows/run_foss_spec.yml
vendored
@ -52,7 +52,8 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
|
cache: yarn
|
||||||
|
|
||||||
- name: yarn
|
- name: yarn
|
||||||
run: yarn install
|
run: yarn install
|
||||||
@ -75,6 +76,8 @@ jobs:
|
|||||||
- name: Run backend tests
|
- name: Run backend tests
|
||||||
run: |
|
run: |
|
||||||
bundle exec rspec --profile=10 --format documentation
|
bundle exec rspec --profile=10 --format documentation
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --openssl-legacy-provider
|
||||||
|
|
||||||
- name: Upload rails log folder
|
- name: Upload rails log folder
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
|
|||||||
3
.github/workflows/run_response_bot_spec.yml
vendored
3
.github/workflows/run_response_bot_spec.yml
vendored
@ -51,7 +51,8 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
|
cache: yarn
|
||||||
|
|
||||||
- name: yarn
|
- name: yarn
|
||||||
run: yarn install
|
run: yarn install
|
||||||
|
|||||||
7
.github/workflows/size-limit.yml
vendored
7
.github/workflows/size-limit.yml
vendored
@ -21,7 +21,8 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: 20
|
||||||
|
cache: 'yarn'
|
||||||
|
|
||||||
- name: yarn
|
- name: yarn
|
||||||
run: yarn install
|
run: yarn install
|
||||||
@ -30,9 +31,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
rm -rf enterprise
|
rm -rf enterprise
|
||||||
rm -rf spec/enterprise
|
rm -rf spec/enterprise
|
||||||
|
|
||||||
- name: Run asset compile
|
- name: Run asset compile
|
||||||
run: bundle exec rake assets:precompile
|
run: bundle exec rake assets:precompile
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --openssl-legacy-provider
|
||||||
|
|
||||||
- name: Size Check
|
- name: Size Check
|
||||||
run: yarn run size
|
run: yarn run size
|
||||||
|
|||||||
13
.rubocop.yml
13
.rubocop.yml
@ -8,15 +8,11 @@ Layout/LineLength:
|
|||||||
Max: 150
|
Max: 150
|
||||||
|
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
Max: 125
|
Max: 175
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/models/conversation.rb'
|
|
||||||
- 'app/models/contact.rb'
|
|
||||||
- 'app/mailers/conversation_reply_mailer.rb'
|
|
||||||
- 'app/models/message.rb'
|
- 'app/models/message.rb'
|
||||||
- 'app/builders/messages/facebook/message_builder.rb'
|
- 'app/models/conversation.rb'
|
||||||
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
|
||||||
- 'app/listeners/action_cable_listener.rb'
|
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
Max: 25
|
Max: 25
|
||||||
Style/Documentation:
|
Style/Documentation:
|
||||||
@ -73,7 +69,7 @@ Rails/ApplicationController:
|
|||||||
- 'app/controllers/survey/responses_controller.rb'
|
- 'app/controllers/survey/responses_controller.rb'
|
||||||
Rails/FindEach:
|
Rails/FindEach:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Include:
|
Include:
|
||||||
- 'app/**/*.rb'
|
- 'app/**/*.rb'
|
||||||
Rails/CompactBlank:
|
Rails/CompactBlank:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
@ -189,7 +185,6 @@ RSpec/IndexedLet:
|
|||||||
RSpec/NamedSubject:
|
RSpec/NamedSubject:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
|
||||||
# we should bring this down
|
# we should bring this down
|
||||||
RSpec/MultipleMemoizedHelpers:
|
RSpec/MultipleMemoizedHelpers:
|
||||||
Max: 14
|
Max: 14
|
||||||
|
|||||||
12
Gemfile
12
Gemfile
@ -4,7 +4,7 @@ ruby '3.2.2'
|
|||||||
|
|
||||||
##-- base gems for rails --##
|
##-- base gems for rails --##
|
||||||
gem 'rack-cors', require: 'rack/cors'
|
gem 'rack-cors', require: 'rack/cors'
|
||||||
gem 'rails', '~> 7.0.5.1'
|
gem 'rails', '~> 7.0.8.0'
|
||||||
# Reduces boot times through caching; required in config/boot.rb
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
|
|
||||||
@ -108,14 +108,14 @@ gem 'elastic-apm', require: false
|
|||||||
gem 'newrelic_rpm', require: false
|
gem 'newrelic_rpm', require: false
|
||||||
gem 'newrelic-sidekiq-metrics', require: false
|
gem 'newrelic-sidekiq-metrics', require: false
|
||||||
gem 'scout_apm', require: false
|
gem 'scout_apm', require: false
|
||||||
gem 'sentry-rails', '>= 5.10.0', require: false
|
gem 'sentry-rails', '>= 5.11.0', require: false
|
||||||
gem 'sentry-ruby', require: false
|
gem 'sentry-ruby', require: false
|
||||||
gem 'sentry-sidekiq', '>= 5.10.0', require: false
|
gem 'sentry-sidekiq', '>= 5.11.0', require: false
|
||||||
|
|
||||||
##-- background job processing --##
|
##-- background job processing --##
|
||||||
gem 'sidekiq'
|
gem 'sidekiq', '>= 7.1.3'
|
||||||
# We want cron jobs
|
# We want cron jobs
|
||||||
gem 'sidekiq-cron'
|
gem 'sidekiq-cron', '>= 1.10.1'
|
||||||
|
|
||||||
##-- Push notification service --##
|
##-- Push notification service --##
|
||||||
gem 'fcm'
|
gem 'fcm'
|
||||||
@ -188,7 +188,7 @@ group :development do
|
|||||||
gem 'bullet'
|
gem 'bullet'
|
||||||
gem 'letter_opener'
|
gem 'letter_opener'
|
||||||
gem 'scss_lint', require: false
|
gem 'scss_lint', require: false
|
||||||
gem 'web-console'
|
gem 'web-console', '>= 4.2.1'
|
||||||
|
|
||||||
# used in swagger build
|
# used in swagger build
|
||||||
gem 'json_refs'
|
gem 'json_refs'
|
||||||
|
|||||||
162
Gemfile.lock
162
Gemfile.lock
@ -33,70 +33,70 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (7.0.5.1)
|
actioncable (7.0.8)
|
||||||
actionpack (= 7.0.5.1)
|
actionpack (= 7.0.8)
|
||||||
activesupport (= 7.0.5.1)
|
activesupport (= 7.0.8)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
actionmailbox (7.0.5.1)
|
actionmailbox (7.0.8)
|
||||||
actionpack (= 7.0.5.1)
|
actionpack (= 7.0.8)
|
||||||
activejob (= 7.0.5.1)
|
activejob (= 7.0.8)
|
||||||
activerecord (= 7.0.5.1)
|
activerecord (= 7.0.8)
|
||||||
activestorage (= 7.0.5.1)
|
activestorage (= 7.0.8)
|
||||||
activesupport (= 7.0.5.1)
|
activesupport (= 7.0.8)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
actionmailer (7.0.5.1)
|
actionmailer (7.0.8)
|
||||||
actionpack (= 7.0.5.1)
|
actionpack (= 7.0.8)
|
||||||
actionview (= 7.0.5.1)
|
actionview (= 7.0.8)
|
||||||
activejob (= 7.0.5.1)
|
activejob (= 7.0.8)
|
||||||
activesupport (= 7.0.5.1)
|
activesupport (= 7.0.8)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
actionpack (7.0.5.1)
|
actionpack (7.0.8)
|
||||||
actionview (= 7.0.5.1)
|
actionview (= 7.0.8)
|
||||||
activesupport (= 7.0.5.1)
|
activesupport (= 7.0.8)
|
||||||
rack (~> 2.0, >= 2.2.4)
|
rack (~> 2.0, >= 2.2.4)
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||||
actiontext (7.0.5.1)
|
actiontext (7.0.8)
|
||||||
actionpack (= 7.0.5.1)
|
actionpack (= 7.0.8)
|
||||||
activerecord (= 7.0.5.1)
|
activerecord (= 7.0.8)
|
||||||
activestorage (= 7.0.5.1)
|
activestorage (= 7.0.8)
|
||||||
activesupport (= 7.0.5.1)
|
activesupport (= 7.0.8)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.0.5.1)
|
actionview (7.0.8)
|
||||||
activesupport (= 7.0.5.1)
|
activesupport (= 7.0.8)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.4)
|
erubi (~> 1.4)
|
||||||
rails-dom-testing (~> 2.0)
|
rails-dom-testing (~> 2.0)
|
||||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||||
active_record_query_trace (1.8)
|
active_record_query_trace (1.8)
|
||||||
activejob (7.0.5.1)
|
activejob (7.0.8)
|
||||||
activesupport (= 7.0.5.1)
|
activesupport (= 7.0.8)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.0.5.1)
|
activemodel (7.0.8)
|
||||||
activesupport (= 7.0.5.1)
|
activesupport (= 7.0.8)
|
||||||
activerecord (7.0.5.1)
|
activerecord (7.0.8)
|
||||||
activemodel (= 7.0.5.1)
|
activemodel (= 7.0.8)
|
||||||
activesupport (= 7.0.5.1)
|
activesupport (= 7.0.8)
|
||||||
activerecord-import (1.4.1)
|
activerecord-import (1.4.1)
|
||||||
activerecord (>= 4.2)
|
activerecord (>= 4.2)
|
||||||
activestorage (7.0.5.1)
|
activestorage (7.0.8)
|
||||||
actionpack (= 7.0.5.1)
|
actionpack (= 7.0.8)
|
||||||
activejob (= 7.0.5.1)
|
activejob (= 7.0.8)
|
||||||
activerecord (= 7.0.5.1)
|
activerecord (= 7.0.8)
|
||||||
activesupport (= 7.0.5.1)
|
activesupport (= 7.0.8)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
mini_mime (>= 1.1.0)
|
mini_mime (>= 1.1.0)
|
||||||
activesupport (7.0.5.1)
|
activesupport (7.0.8)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
@ -269,8 +269,8 @@ GEM
|
|||||||
grpc (~> 1.36)
|
grpc (~> 1.36)
|
||||||
geocoder (1.8.1)
|
geocoder (1.8.1)
|
||||||
gli (2.21.0)
|
gli (2.21.0)
|
||||||
globalid (1.1.0)
|
globalid (1.2.1)
|
||||||
activesupport (>= 5.0)
|
activesupport (>= 6.1)
|
||||||
gmail_xoauth (0.4.2)
|
gmail_xoauth (0.4.2)
|
||||||
oauth (>= 0.3.6)
|
oauth (>= 0.3.6)
|
||||||
google-apis-core (0.11.0)
|
google-apis-core (0.11.0)
|
||||||
@ -450,9 +450,9 @@ GEM
|
|||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2023.0218.1)
|
mime-types-data (3.2023.0218.1)
|
||||||
mini_magick (4.12.0)
|
mini_magick (4.12.0)
|
||||||
mini_mime (1.1.2)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.4)
|
mini_portile2 (2.8.4)
|
||||||
minitest (5.19.0)
|
minitest (5.20.0)
|
||||||
mock_redis (0.36.0)
|
mock_redis (0.36.0)
|
||||||
ruby2_keywords
|
ruby2_keywords
|
||||||
msgpack (1.7.0)
|
msgpack (1.7.0)
|
||||||
@ -463,7 +463,7 @@ GEM
|
|||||||
activerecord (>= 5.2)
|
activerecord (>= 5.2)
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.3.6)
|
net-imap (0.3.7)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-pop (0.1.2)
|
net-pop (0.1.2)
|
||||||
@ -478,14 +478,14 @@ GEM
|
|||||||
sidekiq
|
sidekiq
|
||||||
newrelic_rpm (8.16.0)
|
newrelic_rpm (8.16.0)
|
||||||
nio4r (2.5.9)
|
nio4r (2.5.9)
|
||||||
nokogiri (1.15.3)
|
nokogiri (1.15.4)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.3-arm64-darwin)
|
nokogiri (1.15.4-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.3-x86_64-darwin)
|
nokogiri (1.15.4-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.3-x86_64-linux)
|
nokogiri (1.15.4-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
numo-narray (0.9.2.1)
|
numo-narray (0.9.2.1)
|
||||||
oauth (1.1.0)
|
oauth (1.1.0)
|
||||||
@ -543,7 +543,7 @@ GEM
|
|||||||
pry-rails (0.3.9)
|
pry-rails (0.3.9)
|
||||||
pry (>= 0.10.4)
|
pry (>= 0.10.4)
|
||||||
public_suffix (5.0.1)
|
public_suffix (5.0.1)
|
||||||
puma (6.2.2)
|
puma (6.3.1)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.3.0)
|
pundit (2.3.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
@ -563,30 +563,30 @@ GEM
|
|||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
rack (>= 1.3)
|
rack (>= 1.3)
|
||||||
rack-timeout (0.6.3)
|
rack-timeout (0.6.3)
|
||||||
rails (7.0.5.1)
|
rails (7.0.8)
|
||||||
actioncable (= 7.0.5.1)
|
actioncable (= 7.0.8)
|
||||||
actionmailbox (= 7.0.5.1)
|
actionmailbox (= 7.0.8)
|
||||||
actionmailer (= 7.0.5.1)
|
actionmailer (= 7.0.8)
|
||||||
actionpack (= 7.0.5.1)
|
actionpack (= 7.0.8)
|
||||||
actiontext (= 7.0.5.1)
|
actiontext (= 7.0.8)
|
||||||
actionview (= 7.0.5.1)
|
actionview (= 7.0.8)
|
||||||
activejob (= 7.0.5.1)
|
activejob (= 7.0.8)
|
||||||
activemodel (= 7.0.5.1)
|
activemodel (= 7.0.8)
|
||||||
activerecord (= 7.0.5.1)
|
activerecord (= 7.0.8)
|
||||||
activestorage (= 7.0.5.1)
|
activestorage (= 7.0.8)
|
||||||
activesupport (= 7.0.5.1)
|
activesupport (= 7.0.8)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.0.5.1)
|
railties (= 7.0.8)
|
||||||
rails-dom-testing (2.1.1)
|
rails-dom-testing (2.2.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.0)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
nokogiri (~> 1.14)
|
||||||
railties (7.0.5.1)
|
railties (7.0.8)
|
||||||
actionpack (= 7.0.5.1)
|
actionpack (= 7.0.8)
|
||||||
activesupport (= 7.0.5.1)
|
activesupport (= 7.0.8)
|
||||||
method_source
|
method_source
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
@ -598,7 +598,7 @@ GEM
|
|||||||
ffi (~> 1.0)
|
ffi (~> 1.0)
|
||||||
redis (5.0.6)
|
redis (5.0.6)
|
||||||
redis-client (>= 0.9.0)
|
redis-client (>= 0.9.0)
|
||||||
redis-client (0.14.1)
|
redis-client (0.17.0)
|
||||||
connection_pool
|
connection_pool
|
||||||
redis-namespace (1.10.0)
|
redis-namespace (1.10.0)
|
||||||
redis (>= 4)
|
redis (>= 4)
|
||||||
@ -697,23 +697,23 @@ GEM
|
|||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
selectize-rails (0.12.6)
|
selectize-rails (0.12.6)
|
||||||
semantic_range (3.0.0)
|
semantic_range (3.0.0)
|
||||||
sentry-rails (5.10.0)
|
sentry-rails (5.11.0)
|
||||||
railties (>= 5.0)
|
railties (>= 5.0)
|
||||||
sentry-ruby (~> 5.10.0)
|
sentry-ruby (~> 5.11.0)
|
||||||
sentry-ruby (5.10.0)
|
sentry-ruby (5.11.0)
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
sentry-sidekiq (5.10.0)
|
sentry-sidekiq (5.11.0)
|
||||||
sentry-ruby (~> 5.10.0)
|
sentry-ruby (~> 5.11.0)
|
||||||
sidekiq (>= 3.0)
|
sidekiq (>= 3.0)
|
||||||
sexp_processor (4.17.0)
|
sexp_processor (4.17.0)
|
||||||
shoulda-matchers (5.3.0)
|
shoulda-matchers (5.3.0)
|
||||||
activesupport (>= 5.2.0)
|
activesupport (>= 5.2.0)
|
||||||
sidekiq (7.1.2)
|
sidekiq (7.1.3)
|
||||||
concurrent-ruby (< 2)
|
concurrent-ruby (< 2)
|
||||||
connection_pool (>= 2.3.0)
|
connection_pool (>= 2.3.0)
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
redis-client (>= 0.14.0)
|
redis-client (>= 0.14.0)
|
||||||
sidekiq-cron (1.10.0)
|
sidekiq-cron (1.10.1)
|
||||||
fugit (~> 1.8)
|
fugit (~> 1.8)
|
||||||
globalid (>= 1.0.1)
|
globalid (>= 1.0.1)
|
||||||
sidekiq (>= 6)
|
sidekiq (>= 6)
|
||||||
@ -786,7 +786,7 @@ GEM
|
|||||||
version_gem (1.1.2)
|
version_gem (1.1.2)
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.2.0)
|
web-console (4.2.1)
|
||||||
actionview (>= 6.0.0)
|
actionview (>= 6.0.0)
|
||||||
activemodel (>= 6.0.0)
|
activemodel (>= 6.0.0)
|
||||||
bindex (>= 0.4.0)
|
bindex (>= 0.4.0)
|
||||||
@ -805,14 +805,14 @@ GEM
|
|||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
semantic_range (>= 2.3.0)
|
semantic_range (>= 2.3.0)
|
||||||
webrick (1.8.1)
|
webrick (1.8.1)
|
||||||
websocket-driver (0.7.5)
|
websocket-driver (0.7.6)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
wisper (2.0.0)
|
wisper (2.0.0)
|
||||||
working_hours (1.4.1)
|
working_hours (1.4.1)
|
||||||
activesupport (>= 3.2)
|
activesupport (>= 3.2)
|
||||||
tzinfo
|
tzinfo
|
||||||
zeitwerk (2.6.9)
|
zeitwerk (2.6.11)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
arm64-darwin-20
|
arm64-darwin-20
|
||||||
@ -907,7 +907,7 @@ DEPENDENCIES
|
|||||||
rack-cors
|
rack-cors
|
||||||
rack-mini-profiler (>= 3.1.1)
|
rack-mini-profiler (>= 3.1.1)
|
||||||
rack-timeout
|
rack-timeout
|
||||||
rails (~> 7.0.5.1)
|
rails (~> 7.0.8.0)
|
||||||
redis
|
redis
|
||||||
redis-namespace
|
redis-namespace
|
||||||
responders
|
responders
|
||||||
@ -922,12 +922,12 @@ DEPENDENCIES
|
|||||||
scout_apm
|
scout_apm
|
||||||
scss_lint
|
scss_lint
|
||||||
seed_dump
|
seed_dump
|
||||||
sentry-rails (>= 5.10.0)
|
sentry-rails (>= 5.11.0)
|
||||||
sentry-ruby
|
sentry-ruby
|
||||||
sentry-sidekiq (>= 5.10.0)
|
sentry-sidekiq (>= 5.11.0)
|
||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
sidekiq
|
sidekiq (>= 7.1.3)
|
||||||
sidekiq-cron
|
sidekiq-cron (>= 1.10.1)
|
||||||
simplecov (= 0.17.1)
|
simplecov (= 0.17.1)
|
||||||
slack-ruby-client (~> 2.0.0)
|
slack-ruby-client (~> 2.0.0)
|
||||||
spring
|
spring
|
||||||
@ -943,7 +943,7 @@ DEPENDENCIES
|
|||||||
tzinfo-data
|
tzinfo-data
|
||||||
uglifier
|
uglifier
|
||||||
valid_email2
|
valid_email2
|
||||||
web-console
|
web-console (>= 4.2.1)
|
||||||
web-push
|
web-push
|
||||||
webmock
|
webmock
|
||||||
webpacker
|
webpacker
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
backend: bin/rails s -p 3000
|
backend: bin/rails s -p 3000
|
||||||
frontend: bin/webpack-dev-server
|
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
|
||||||
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
|
# https://github.com/mperham/sidekiq/issues/3090#issuecomment-389748695
|
||||||
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
|
worker: dotenv bundle exec sidekiq -C config/sidekiq.yml
|
||||||
|
|||||||
3
app.json
3
app.json
@ -60,6 +60,9 @@
|
|||||||
],
|
],
|
||||||
"stack": "heroku-20",
|
"stack": "heroku-20",
|
||||||
"buildpacks": [
|
"buildpacks": [
|
||||||
|
{
|
||||||
|
"url": "heroku/nodejs"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "heroku/ruby"
|
"url": "heroku/ruby"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,3 +29,5 @@
|
|||||||
@import 'components/pagination';
|
@import 'components/pagination';
|
||||||
@import 'components/search';
|
@import 'components/search';
|
||||||
@import 'components/reports';
|
@import 'components/reports';
|
||||||
|
|
||||||
|
@import 'custom_styles';
|
||||||
|
|||||||
24
app/assets/stylesheets/administrate/custom_styles.scss
Normal file
24
app/assets/stylesheets/administrate/custom_styles.scss
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// custom styles for the dashboard
|
||||||
|
|
||||||
|
.feature-cell {
|
||||||
|
background: $color-extra-light-blue;
|
||||||
|
border-radius: 10px;
|
||||||
|
float: left;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
|
||||||
|
.icon-container {
|
||||||
|
margin-right: 4px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-container {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.feature-container {
|
||||||
|
max-width: 100rem;
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||||
before_action :portal
|
before_action :portal
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :fetch_article, except: [:index, :create, :attach_file, :reorder]
|
before_action :fetch_article, except: [:index, :create, :reorder]
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@ -36,17 +36,6 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def attach_file
|
|
||||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
|
||||||
key: nil,
|
|
||||||
io: params[:background_image].tempfile,
|
|
||||||
filename: params[:background_image].original_filename,
|
|
||||||
content_type: params[:background_image].content_type
|
|
||||||
)
|
|
||||||
file_blob.save!
|
|
||||||
render json: { file_url: url_for(file_blob) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def reorder
|
def reorder
|
||||||
Article.update_positions(params[:positions_hash])
|
Article.update_positions(params[:positions_hash])
|
||||||
head :ok
|
head :ok
|
||||||
|
|||||||
@ -20,16 +20,6 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
|||||||
@automation_rule
|
@automation_rule
|
||||||
end
|
end
|
||||||
|
|
||||||
def attach_file
|
|
||||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
|
||||||
key: nil,
|
|
||||||
io: params[:attachment].tempfile,
|
|
||||||
filename: params[:attachment].original_filename,
|
|
||||||
content_type: params[:attachment].content_type
|
|
||||||
)
|
|
||||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
automation_rule_update
|
automation_rule_update
|
||||||
|
|||||||
@ -18,7 +18,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
@contacts_count = resolved_contacts.count
|
@contacts_count = resolved_contacts.count
|
||||||
@contacts = fetch_contacts_with_conversation_count(resolved_contacts)
|
@contacts = fetch_contacts(resolved_contacts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def search
|
def search
|
||||||
@ -29,7 +29,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
search: "%#{params[:q].strip}%"
|
search: "%#{params[:q].strip}%"
|
||||||
)
|
)
|
||||||
@contacts_count = contacts.count
|
@contacts_count = contacts.count
|
||||||
@contacts = fetch_contacts_with_conversation_count(contacts)
|
@contacts = fetch_contacts(contacts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def import
|
def import
|
||||||
@ -63,7 +63,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
result = ::Contacts::FilterService.new(params.permit!, current_user).perform
|
result = ::Contacts::FilterService.new(params.permit!, current_user).perform
|
||||||
contacts = result[:contacts]
|
contacts = result[:contacts]
|
||||||
@contacts_count = result[:count]
|
@contacts_count = result[:count]
|
||||||
@contacts = fetch_contacts_with_conversation_count(contacts)
|
@contacts = fetch_contacts(contacts)
|
||||||
end
|
end
|
||||||
|
|
||||||
def contactable_inboxes
|
def contactable_inboxes
|
||||||
@ -125,17 +125,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||||||
@current_page = params[:page] || 1
|
@current_page = params[:page] || 1
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_contacts_with_conversation_count(contacts)
|
def fetch_contacts(contacts)
|
||||||
conversation_count_sub_query = 'SELECT COUNT(*) FROM "conversations" WHERE "conversations"."contact_id" = "contacts"."id"'
|
contacts_with_avatar = filtrate(contacts)
|
||||||
contacts_with_conversation_count = filtrate(contacts)
|
.includes([{ avatar_attachment: [:blob] }])
|
||||||
.select("contacts.*, (#{conversation_count_sub_query}) as conversations_count")
|
.page(@current_page).per(RESULTS_PER_PAGE)
|
||||||
.group('contacts.id')
|
|
||||||
.includes([{ avatar_attachment: [:blob] }])
|
|
||||||
.page(@current_page).per(RESULTS_PER_PAGE)
|
|
||||||
|
|
||||||
return contacts_with_conversation_count.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
return contacts_with_avatar.includes([{ contact_inboxes: [:inbox] }]) if @include_contact_inboxes
|
||||||
|
|
||||||
contacts_with_conversation_count
|
contacts_with_avatar
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_contact_inbox
|
def build_contact_inbox
|
||||||
|
|||||||
@ -39,16 +39,6 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def attach_file
|
|
||||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
|
||||||
key: nil,
|
|
||||||
io: params[:attachment].tempfile,
|
|
||||||
filename: params[:attachment].original_filename,
|
|
||||||
content_type: params[:attachment].content_type
|
|
||||||
)
|
|
||||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
def execute
|
def execute
|
||||||
::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user)
|
::MacrosExecutionJob.perform_later(@macro, conversation_ids: params[:conversation_ids], user: Current.user)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
include ::FileTypeHelper
|
include ::FileTypeHelper
|
||||||
|
|
||||||
before_action :fetch_portal, except: [:index, :create, :attach_file]
|
before_action :fetch_portal, except: [:index, :create]
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
|
|
||||||
@ -53,16 +53,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
|||||||
@portal.logo.attach(blob)
|
@portal.logo.attach(blob)
|
||||||
end
|
end
|
||||||
|
|
||||||
def attach_file
|
|
||||||
file_blob = ActiveStorage::Blob.create_and_upload!(
|
|
||||||
key: nil,
|
|
||||||
io: params[:logo].tempfile,
|
|
||||||
filename: params[:logo].original_filename,
|
|
||||||
content_type: params[:logo].content_type
|
|
||||||
)
|
|
||||||
render json: { blob_key: file_blob.key, blob_id: file_blob.id }
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_portal
|
def fetch_portal
|
||||||
|
|||||||
13
app/controllers/api/v1/upload_controller.rb
Normal file
13
app/controllers/api/v1/upload_controller.rb
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
class Api::V1::UploadController < Api::BaseController
|
||||||
|
def create
|
||||||
|
file_blob = ActiveStorage::Blob.create_and_upload!(
|
||||||
|
key: nil,
|
||||||
|
io: params[:attachment].tempfile,
|
||||||
|
filename: params[:attachment].original_filename,
|
||||||
|
content_type: params[:attachment].content_type
|
||||||
|
)
|
||||||
|
file_blob.save!
|
||||||
|
|
||||||
|
render json: { file_url: url_for(file_blob), blob_key: file_blob.key, blob_id: file_blob.id }
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -8,7 +8,7 @@ module EnsureCurrentAccountHelper
|
|||||||
|
|
||||||
def ensure_current_account
|
def ensure_current_account
|
||||||
account = Account.find(params[:account_id])
|
account = Account.find(params[:account_id])
|
||||||
ensure_account_is_active?(account)
|
render_unauthorized('Account is suspended') and return unless account.active?
|
||||||
|
|
||||||
if current_user
|
if current_user
|
||||||
account_accessible_for_user?(account)
|
account_accessible_for_user?(account)
|
||||||
@ -27,8 +27,4 @@ module EnsureCurrentAccountHelper
|
|||||||
def account_accessible_for_bot?(account)
|
def account_accessible_for_bot?(account)
|
||||||
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def ensure_account_is_active?(account)
|
|
||||||
render_unauthorized('Account is suspended') unless account.active?
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
class Public::Api::V1::Portals::BaseController < PublicController
|
class Public::Api::V1::Portals::BaseController < PublicController
|
||||||
before_action :show_plain_layout
|
before_action :show_plain_layout
|
||||||
around_action :set_locale
|
around_action :set_locale
|
||||||
|
after_action :allow_iframe_requests
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
@ -39,4 +40,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
|||||||
|
|
||||||
I18n.with_locale(@locale, &)
|
I18n.with_locale(@locale, &)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def allow_iframe_requests
|
||||||
|
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="!authUIFlags.isFetching"
|
v-if="!authUIFlags.isFetching"
|
||||||
id="app"
|
id="app"
|
||||||
class="app-wrapper app-root"
|
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
|
||||||
:class="{ 'app-rtl--wrapper': isRTLView }"
|
:class="{ 'app-rtl--wrapper': isRTLView }"
|
||||||
:dir="isRTLView ? 'rtl' : 'ltr'"
|
:dir="isRTLView ? 'rtl' : 'ltr'"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -9,14 +9,6 @@ class AutomationsAPI extends ApiClient {
|
|||||||
clone(automationId) {
|
clone(automationId) {
|
||||||
return axios.post(`${this.url}/${automationId}/clone`);
|
return axios.post(`${this.url}/${automationId}/clone`);
|
||||||
}
|
}
|
||||||
|
|
||||||
attachment(file) {
|
|
||||||
return axios.post(`${this.url}/attach_file`, file, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new AutomationsAPI();
|
export default new AutomationsAPI();
|
||||||
|
|||||||
@ -47,20 +47,6 @@ class ArticlesAPI extends PortalsAPI {
|
|||||||
return axios.delete(`${this.url}/${portalSlug}/articles/${articleId}`);
|
return axios.delete(`${this.url}/${portalSlug}/articles/${articleId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadImage({ portalSlug, file }) {
|
|
||||||
let formData = new FormData();
|
|
||||||
formData.append('background_image', file);
|
|
||||||
return axios.post(
|
|
||||||
`${this.url}/${portalSlug}/articles/attach_file`,
|
|
||||||
formData,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
reorderArticles({ portalSlug, reorderedGroup, categorySlug }) {
|
reorderArticles({ portalSlug, reorderedGroup, categorySlug }) {
|
||||||
return axios.post(`${this.url}/${portalSlug}/articles/reorder`, {
|
return axios.post(`${this.url}/${portalSlug}/articles/reorder`, {
|
||||||
positions_hash: reorderedGroup,
|
positions_hash: reorderedGroup,
|
||||||
|
|||||||
@ -26,7 +26,7 @@ code {
|
|||||||
background: $color-background;
|
background: $color-background;
|
||||||
border-radius: var(--border-radius-large);
|
border-radius: var(--border-radius-large);
|
||||||
padding: $space-two;
|
padding: $space-two;
|
||||||
@apply bg-slate-50 dark:bg-slate-600 text-slate-800 dark:text-slate-100;
|
@apply bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,87 +9,25 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-wrapper {
|
.app-wrapper {
|
||||||
@include full-height;
|
@apply h-full flex-grow-0 min-h-0 w-full;
|
||||||
flex-grow: 0;
|
|
||||||
min-height: 0;
|
.button--fixed-top {
|
||||||
width: 100%;
|
@apply fixed ltr:right-2 rtl:left-2 top-2 flex flex-row;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner + .app-wrapper {
|
.banner + .app-wrapper {
|
||||||
|
// Reduce the height of the dashboard to make room for the banner.
|
||||||
|
// And causing the top right green-action button to be pushed down when scrolling.
|
||||||
|
@apply h-[calc(100%-48px)];
|
||||||
|
|
||||||
.button--fixed-top {
|
.button--fixed-top {
|
||||||
top: 5.6 * $space-one;
|
@apply top-14;
|
||||||
}
|
}
|
||||||
|
|
||||||
.off-canvas-content {
|
.off-canvas-content {
|
||||||
.button--fixed-top {
|
.button--fixed-top {
|
||||||
top: $space-small;
|
@apply top-2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is-closed .app-root {
|
|
||||||
@include flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-content {
|
|
||||||
@include flex;
|
|
||||||
@include full-height;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-box {
|
|
||||||
@include full-height;
|
|
||||||
@include space-between-column;
|
|
||||||
|
|
||||||
height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-panel {
|
|
||||||
flex-direction: column;
|
|
||||||
margin: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: $space-normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-box {
|
|
||||||
overflow: auto;
|
|
||||||
padding: $space-normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-button {
|
|
||||||
@include flex;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
color: $color-woot;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: $font-size-default;
|
|
||||||
font-weight: $font-weight-normal;
|
|
||||||
margin-right: $space-normal;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
font-size: $font-size-large;
|
|
||||||
margin-right: $space-small;
|
|
||||||
vertical-align: text-bottom;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-spinner {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-items-error-message {
|
|
||||||
@include flex;
|
|
||||||
@include full-height;
|
|
||||||
|
|
||||||
align-items: center;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: $space-mega;
|
|
||||||
padding: $space-one;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,20 +1,6 @@
|
|||||||
.app-rtl--wrapper {
|
.app-rtl--wrapper {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
|
|
||||||
.header-section.back-button {
|
|
||||||
direction: initial;
|
|
||||||
margin-left: var(--space-normal);
|
|
||||||
margin-right: var(--space-smaller);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Settings header action button
|
|
||||||
.button--fixed-top {
|
|
||||||
left: $space-small;
|
|
||||||
position: fixed;
|
|
||||||
right: unset;
|
|
||||||
top: $space-small;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Woot Tabs
|
// Woot Tabs
|
||||||
.tabs-title {
|
.tabs-title {
|
||||||
&:first-child {
|
&:first-child {
|
||||||
|
|||||||
@ -71,9 +71,3 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--fixed-top {
|
|
||||||
position: fixed;
|
|
||||||
right: var(--space-small);
|
|
||||||
top: var(--space-small);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
.button {
|
.button {
|
||||||
@apply items-center inline-flex h-10 mb-0;
|
@apply items-center inline-flex h-10 mb-0 gap-2;
|
||||||
|
|
||||||
.button__content {
|
.button__content {
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
@ -14,17 +14,9 @@
|
|||||||
@apply px-2 py-0;
|
@apply px-2 py-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon--emoji + .button__content {
|
|
||||||
@apply pl-2 rtl:pr-2 rtl:pl-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon--font + .button__content {
|
|
||||||
@apply pl-2 rtl:pr-2 rtl:pl-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// @TODDO - Remove after moving all buttons to woot-button
|
// @TODDO - Remove after moving all buttons to woot-button
|
||||||
.icon + .button__content {
|
.icon + .button__content {
|
||||||
@apply pl-2 w-auto rtl:pr-2 rtl:pl-0;
|
@apply w-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
@ -157,18 +149,10 @@
|
|||||||
// Sizes
|
// Sizes
|
||||||
&.tiny {
|
&.tiny {
|
||||||
@apply h-6;
|
@apply h-6;
|
||||||
|
|
||||||
.icon + .button__content {
|
|
||||||
@apply pl-1 rtl:pr-1 rtl:pl-0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.small {
|
&.small {
|
||||||
@apply h-8 pb-1 pt-1;
|
@apply h-8 pb-1 pt-1;
|
||||||
|
|
||||||
.icon + .button__content {
|
|
||||||
@apply pl-1 rtl:pr-1 rtl:pl-0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.large {
|
&.large {
|
||||||
|
|||||||
@ -76,13 +76,13 @@ table {
|
|||||||
|
|
||||||
.ve-pagination-goto {
|
.ve-pagination-goto {
|
||||||
@apply text-slate-600 dark:text-slate-200;
|
@apply text-slate-600 dark:text-slate-200;
|
||||||
|
|
||||||
|
.ve-pagination-goto-input {
|
||||||
|
@apply bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-200;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ve-pagination-li {
|
.ve-pagination-li {
|
||||||
@apply bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-200 border-slate-75 dark:border-slate-700;
|
@apply bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-200 border-slate-75 dark:border-slate-700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ve-pagination-goto-input {
|
|
||||||
@apply bg-white dark:bg-slate-900 text-slate-600 dark:text-slate-200;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -61,7 +61,7 @@
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
v-if="isAttributeTypeLink"
|
v-if="isAttributeTypeLink"
|
||||||
:href="value"
|
:href="hrefURL"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="value inline-block rounded-sm mb-0 break-all py-0.5 px-1"
|
class="value inline-block rounded-sm mb-0 break-all py-0.5 px-1"
|
||||||
@ -188,6 +188,9 @@ export default {
|
|||||||
urlValue() {
|
urlValue() {
|
||||||
return isValidURL(this.value) ? this.value : '---';
|
return isValidURL(this.value) ? this.value : '---';
|
||||||
},
|
},
|
||||||
|
hrefURL() {
|
||||||
|
return isValidURL(this.value) ? this.value : '';
|
||||||
|
},
|
||||||
notAttributeTypeCheckboxAndList() {
|
notAttributeTypeCheckboxAndList() {
|
||||||
return !this.isAttributeTypeCheckbox && !this.isAttributeTypeList;
|
return !this.isAttributeTypeCheckbox && !this.isAttributeTypeList;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="show"
|
v-if="show"
|
||||||
v-on-clickaway="onClickAway"
|
v-on-clickaway="onClickAway"
|
||||||
class="left-3 rtl:left-auto rtl:right-3 bottom-16 w-64 absolute z-20 rounded-md shadow-xl bg-white dark:bg-slate-800 py-2 px-2 border border-slate-25 dark:border-slate-700"
|
class="left-3 rtl:left-auto rtl:right-3 bottom-16 w-64 absolute z-30 rounded-md shadow-xl bg-white dark:bg-slate-800 py-2 px-2 border border-slate-25 dark:border-slate-700"
|
||||||
:class="{ 'block visible': show }"
|
:class="{ 'block visible': show }"
|
||||||
>
|
>
|
||||||
<availability-status />
|
<availability-status />
|
||||||
|
|||||||
@ -46,6 +46,7 @@ import AgentDetails from './AgentDetails';
|
|||||||
import NotificationBell from './NotificationBell';
|
import NotificationBell from './NotificationBell';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import { frontendURL } from 'dashboard/helper/URLHelper';
|
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||||
|
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -99,6 +100,7 @@ export default {
|
|||||||
window.$chatwoot.toggle();
|
window.$chatwoot.toggle();
|
||||||
},
|
},
|
||||||
openNotificationPanel() {
|
openNotificationPanel() {
|
||||||
|
this.$track(ACCOUNT_EVENTS.OPENED_NOTIFICATIONS);
|
||||||
this.$emit('open-notification-panel');
|
this.$emit('open-notification-panel');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="banner" :class="bannerClasses">
|
<div
|
||||||
|
class="banner flex items-center h-12 gap-4 text-white dark:text-white text-xs py-3 px-4 justify-center"
|
||||||
|
:class="bannerClasses"
|
||||||
|
>
|
||||||
<span class="banner-message">
|
<span class="banner-message">
|
||||||
{{ bannerMessage }}
|
{{ bannerMessage }}
|
||||||
<a
|
<a
|
||||||
@ -96,8 +99,6 @@ export default {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.banner {
|
.banner {
|
||||||
@apply flex items-center gap-4 text-white dark:text-white text-xs py-3 px-4 justify-center;
|
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
@apply bg-woot-500 dark:bg-woot-500;
|
@apply bg-woot-500 dark:bg-woot-500;
|
||||||
.banner-action__button {
|
.banner-action__button {
|
||||||
|
|||||||
@ -61,11 +61,9 @@ export default {
|
|||||||
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADING');
|
this.label = this.$t('AUTOMATION.ATTACHMENT.LABEL_UPLOADING');
|
||||||
try {
|
try {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('attachment', file, file.name);
|
|
||||||
const id = await this.$store.dispatch(
|
const id = await this.$store.dispatch(
|
||||||
'automations/uploadAttachment',
|
'automations/uploadAttachment',
|
||||||
formData
|
file
|
||||||
);
|
);
|
||||||
this.$emit('input', [id]);
|
this.$emit('input', [id]);
|
||||||
this.uploadState = 'uploaded';
|
this.uploadState = 'uploaded';
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<button class="header-section back-button" @click.capture="goBack">
|
<button
|
||||||
|
class="header-section flex items-center text-base font-normal mr-4 ml-2 cursor-pointer text-woot-500 dark:text-woot-500"
|
||||||
|
@click.capture="goBack"
|
||||||
|
>
|
||||||
<fluent-icon icon="chevron-left" />
|
<fluent-icon icon="chevron-left" />
|
||||||
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
|
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center text-left">
|
<div class="flex items-center gap-1.5 text-left">
|
||||||
<thumbnail
|
<thumbnail
|
||||||
:src="user.thumbnail"
|
:src="user.thumbnail"
|
||||||
:size="size"
|
:size="size"
|
||||||
@ -7,7 +7,7 @@
|
|||||||
:status="user.availability_status"
|
:status="user.availability_status"
|
||||||
/>
|
/>
|
||||||
<h6
|
<h6
|
||||||
class="my-0 mx-2 dark:text-slate-100 overflow-hidden whitespace-nowrap text-ellipsis text-capitalize"
|
class="my-0 dark:text-slate-100 overflow-hidden whitespace-nowrap text-ellipsis text-capitalize"
|
||||||
:class="textClass"
|
:class="textClass"
|
||||||
>
|
>
|
||||||
{{ user.name }}
|
{{ user.name }}
|
||||||
|
|||||||
@ -15,6 +15,13 @@
|
|||||||
:search-key="variableSearchTerm"
|
:search-key="variableSearchTerm"
|
||||||
@click="insertVariable"
|
@click="insertVariable"
|
||||||
/>
|
/>
|
||||||
|
<input
|
||||||
|
ref="imageUpload"
|
||||||
|
type="file"
|
||||||
|
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
|
||||||
|
hidden
|
||||||
|
@change="onFileChange"
|
||||||
|
/>
|
||||||
<div ref="editor" />
|
<div ref="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -22,7 +29,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
messageSchema,
|
messageSchema,
|
||||||
wootMessageWriterSetup,
|
buildEditor,
|
||||||
EditorView,
|
EditorView,
|
||||||
MessageMarkdownTransformer,
|
MessageMarkdownTransformer,
|
||||||
MessageMarkdownSerializer,
|
MessageMarkdownSerializer,
|
||||||
@ -37,8 +44,13 @@ import {
|
|||||||
import TagAgents from '../conversation/TagAgents';
|
import TagAgents from '../conversation/TagAgents';
|
||||||
import CannedResponse from '../conversation/CannedResponse';
|
import CannedResponse from '../conversation/CannedResponse';
|
||||||
import VariableList from '../conversation/VariableList';
|
import VariableList from '../conversation/VariableList';
|
||||||
|
import {
|
||||||
|
appendSignature,
|
||||||
|
removeSignature,
|
||||||
|
} from 'dashboard/helper/editorHelper';
|
||||||
|
|
||||||
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||||
|
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||||
|
|
||||||
import {
|
import {
|
||||||
hasPressedEnterAndNotCmdOrShift,
|
hasPressedEnterAndNotCmdOrShift,
|
||||||
@ -51,14 +63,27 @@ import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
|||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||||
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
||||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
|
import { uploadFile } from 'dashboard/helper/uploadHelper';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
import { findNodeToInsertImage } from 'dashboard/helper/messageEditorHelper';
|
||||||
|
import { MESSAGE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
|
||||||
|
|
||||||
const createState = (content, placeholder, plugins = []) => {
|
const createState = (
|
||||||
|
content,
|
||||||
|
placeholder,
|
||||||
|
plugins = [],
|
||||||
|
methods = {},
|
||||||
|
enabledMenuOptions
|
||||||
|
) => {
|
||||||
return EditorState.create({
|
return EditorState.create({
|
||||||
doc: new MessageMarkdownTransformer(messageSchema).parse(content),
|
doc: new MessageMarkdownTransformer(messageSchema).parse(content),
|
||||||
plugins: wootMessageWriterSetup({
|
plugins: buildEditor({
|
||||||
schema: messageSchema,
|
schema: messageSchema,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
methods,
|
||||||
plugins,
|
plugins,
|
||||||
|
enabledMenuOptions,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -66,7 +91,7 @@ const createState = (content, placeholder, plugins = []) => {
|
|||||||
export default {
|
export default {
|
||||||
name: 'WootMessageEditor',
|
name: 'WootMessageEditor',
|
||||||
components: { TagAgents, CannedResponse, VariableList },
|
components: { TagAgents, CannedResponse, VariableList },
|
||||||
mixins: [eventListenerMixins, uiSettingsMixin],
|
mixins: [eventListenerMixins, uiSettingsMixin, alertMixin],
|
||||||
props: {
|
props: {
|
||||||
value: { type: String, default: '' },
|
value: { type: String, default: '' },
|
||||||
editorId: { type: String, default: '' },
|
editorId: { type: String, default: '' },
|
||||||
@ -78,6 +103,11 @@ export default {
|
|||||||
enableVariables: { type: Boolean, default: false },
|
enableVariables: { type: Boolean, default: false },
|
||||||
enableCannedResponses: { type: Boolean, default: true },
|
enableCannedResponses: { type: Boolean, default: true },
|
||||||
variables: { type: Object, default: () => ({}) },
|
variables: { type: Object, default: () => ({}) },
|
||||||
|
enabledMenuOptions: { type: Array, default: () => [] },
|
||||||
|
signature: { type: String, default: '' },
|
||||||
|
// allowSignature is a kill switch, ensuring no signature methods
|
||||||
|
// are triggered except when this flag is true
|
||||||
|
allowSignature: { type: Boolean, default: false },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -104,6 +134,11 @@ export default {
|
|||||||
this.enableCannedResponses && this.showCannedMenu && !this.isPrivate
|
this.enableCannedResponses && this.showCannedMenu && !this.isPrivate
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
editorMenuOptions() {
|
||||||
|
return this.enabledMenuOptions.length
|
||||||
|
? this.enabledMenuOptions
|
||||||
|
: MESSAGE_EDITOR_MENU_OPTIONS;
|
||||||
|
},
|
||||||
plugins() {
|
plugins() {
|
||||||
if (!this.enableSuggestions) {
|
if (!this.enableSuggestions) {
|
||||||
return [];
|
return [];
|
||||||
@ -193,6 +228,12 @@ export default {
|
|||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
sendWithSignature() {
|
||||||
|
// this is considered the source of truth, we watch this property
|
||||||
|
// on change, we toggle the signature in the editor
|
||||||
|
const { send_with_signature: isEnabled } = this.uiSettings;
|
||||||
|
return isEnabled && this.allowSignature && !this.isPrivate;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
showUserMentions(updatedValue) {
|
showUserMentions(updatedValue) {
|
||||||
@ -204,20 +245,19 @@ export default {
|
|||||||
showVariables(updatedValue) {
|
showVariables(updatedValue) {
|
||||||
this.$emit('toggle-variables-menu', !this.isPrivate && updatedValue);
|
this.$emit('toggle-variables-menu', !this.isPrivate && updatedValue);
|
||||||
},
|
},
|
||||||
value(newValue = '') {
|
value(newVal = '') {
|
||||||
if (newValue !== this.contentFromEditor) {
|
if (newVal !== this.contentFromEditor) {
|
||||||
this.reloadState();
|
this.reloadState(newVal);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
editorId() {
|
editorId() {
|
||||||
this.showCannedMenu = false;
|
this.showCannedMenu = false;
|
||||||
this.cannedSearchTerm = '';
|
this.cannedSearchTerm = '';
|
||||||
this.reloadState();
|
this.reloadState(this.value);
|
||||||
},
|
},
|
||||||
isPrivate() {
|
isPrivate() {
|
||||||
this.reloadState();
|
this.reloadState(this.value);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateSelectionWith(newValue, oldValue) {
|
updateSelectionWith(newValue, oldValue) {
|
||||||
if (!this.editorView) {
|
if (!this.editorView) {
|
||||||
return null;
|
return null;
|
||||||
@ -236,20 +276,106 @@ export default {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
sendWithSignature(newValue) {
|
||||||
|
// see if the allowSignature flag is true
|
||||||
|
if (this.allowSignature) {
|
||||||
|
this.toggleSignatureInEditor(newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.state = createState(this.value, this.placeholder, this.plugins);
|
this.state = createState(
|
||||||
|
this.value,
|
||||||
|
this.placeholder,
|
||||||
|
this.plugins,
|
||||||
|
{ onImageUpload: this.openFileBrowser },
|
||||||
|
this.editorMenuOptions
|
||||||
|
);
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.createEditorView();
|
this.createEditorView();
|
||||||
this.editorView.updateState(this.state);
|
this.editorView.updateState(this.state);
|
||||||
this.focusEditorInputField();
|
this.focusEditor(this.value);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
reloadState() {
|
reloadState(content = this.value) {
|
||||||
this.state = createState(this.value, this.placeholder, this.plugins);
|
this.state = createState(
|
||||||
|
content,
|
||||||
|
this.placeholder,
|
||||||
|
this.plugins,
|
||||||
|
{ onImageUpload: this.openFileBrowser },
|
||||||
|
this.editorMenuOptions
|
||||||
|
);
|
||||||
this.editorView.updateState(this.state);
|
this.editorView.updateState(this.state);
|
||||||
this.focusEditorInputField();
|
|
||||||
|
this.focusEditor(content);
|
||||||
|
},
|
||||||
|
focusEditor(content) {
|
||||||
|
if (this.isBodyEmpty(content) && this.sendWithSignature) {
|
||||||
|
// reload state can be called when switching between conversations, or when drafts is loaded
|
||||||
|
// these drafts can also have a signature, so we need to check if the body is empty
|
||||||
|
// and handle things accordingly
|
||||||
|
this.handleEmptyBodyWithSignature();
|
||||||
|
} else {
|
||||||
|
// this is in the else block, handleEmptyBodyWithSignature also has a call to the focus method
|
||||||
|
// the position is set to start, because the signature is added at the end of the body
|
||||||
|
this.focusEditorInputField('end');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleSignatureInEditor(signatureEnabled) {
|
||||||
|
// The toggleSignatureInEditor gets the new value from the
|
||||||
|
// watcher, this means that if the value is true, the signature
|
||||||
|
// is supposed to be added, else we remove it.
|
||||||
|
if (signatureEnabled) {
|
||||||
|
this.addSignature();
|
||||||
|
} else {
|
||||||
|
this.removeSignature();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addSignature() {
|
||||||
|
let content = this.value;
|
||||||
|
// see if the content is empty, if it is before appending the signature
|
||||||
|
// we need to add a paragraph node and move the cursor at the start of the editor
|
||||||
|
const contentWasEmpty = this.isBodyEmpty(content);
|
||||||
|
content = appendSignature(content, this.signature);
|
||||||
|
// need to reload first, ensuring that the editorView is updated
|
||||||
|
this.reloadState(content);
|
||||||
|
|
||||||
|
if (contentWasEmpty) {
|
||||||
|
this.handleEmptyBodyWithSignature();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeSignature() {
|
||||||
|
if (!this.signature) return;
|
||||||
|
let content = this.value;
|
||||||
|
content = removeSignature(content, this.signature);
|
||||||
|
// reload the state, ensuring that the editorView is updated
|
||||||
|
this.reloadState(content);
|
||||||
|
},
|
||||||
|
isBodyEmpty(content) {
|
||||||
|
// if content is undefined, we assume that the body is empty
|
||||||
|
if (!content) return true;
|
||||||
|
|
||||||
|
// if the signature is present, we need to remove it before checking
|
||||||
|
// note that we don't update the editorView, so this is safe
|
||||||
|
const bodyWithoutSignature = this.signature
|
||||||
|
? removeSignature(content, this.signature)
|
||||||
|
: content;
|
||||||
|
|
||||||
|
// trimming should remove all the whitespaces, so we can check the length
|
||||||
|
return bodyWithoutSignature.trim().length === 0;
|
||||||
|
},
|
||||||
|
handleEmptyBodyWithSignature() {
|
||||||
|
const { schema, tr } = this.state;
|
||||||
|
|
||||||
|
// create a paragraph node and
|
||||||
|
// start a transaction to append it at the end
|
||||||
|
const paragraph = schema.nodes.paragraph.create();
|
||||||
|
const paragraphTransaction = tr.insert(0, paragraph);
|
||||||
|
this.editorView.dispatch(paragraphTransaction);
|
||||||
|
|
||||||
|
// Set the focus at the start of the input field
|
||||||
|
this.focusEditorInputField('start');
|
||||||
},
|
},
|
||||||
createEditorView() {
|
createEditorView() {
|
||||||
this.editorView = new EditorView(this.$refs.editor, {
|
this.editorView = new EditorView(this.$refs.editor, {
|
||||||
@ -294,9 +420,11 @@ export default {
|
|||||||
this.focusEditorInputField();
|
this.focusEditorInputField();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
focusEditorInputField() {
|
focusEditorInputField(pos = 'end') {
|
||||||
const { tr } = this.editorView.state;
|
const { tr } = this.editorView.state;
|
||||||
const selection = Selection.atEnd(tr.doc);
|
|
||||||
|
const selection =
|
||||||
|
pos === 'end' ? Selection.atEnd(tr.doc) : Selection.atStart(tr.doc);
|
||||||
|
|
||||||
this.editorView.dispatch(tr.setSelection(selection));
|
this.editorView.dispatch(tr.setSelection(selection));
|
||||||
this.editorView.focus();
|
this.editorView.focus();
|
||||||
@ -374,6 +502,57 @@ export default {
|
|||||||
tr.scrollIntoView();
|
tr.scrollIntoView();
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
openFileBrowser() {
|
||||||
|
this.$refs.imageUpload.click();
|
||||||
|
},
|
||||||
|
onFileChange() {
|
||||||
|
const file = this.$refs.imageUpload.files[0];
|
||||||
|
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
||||||
|
this.uploadImageToStorage(file);
|
||||||
|
} else {
|
||||||
|
this.showAlert(
|
||||||
|
this.$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR',
|
||||||
|
{
|
||||||
|
size: MAXIMUM_FILE_UPLOAD_SIZE,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$refs.imageUpload.value = '';
|
||||||
|
},
|
||||||
|
async uploadImageToStorage(file) {
|
||||||
|
try {
|
||||||
|
const { fileUrl } = await uploadFile(file);
|
||||||
|
if (fileUrl) {
|
||||||
|
this.onImageInsertInEditor(fileUrl);
|
||||||
|
}
|
||||||
|
this.showAlert(
|
||||||
|
this.$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SUCCESS'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.showAlert(
|
||||||
|
this.$t(
|
||||||
|
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_ERROR'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onImageInsertInEditor(fileUrl) {
|
||||||
|
const { tr } = this.editorView.state;
|
||||||
|
|
||||||
|
const insertData = findNodeToInsertImage(this.editorView.state, fileUrl);
|
||||||
|
|
||||||
|
if (insertData) {
|
||||||
|
this.editorView.dispatch(
|
||||||
|
tr.insert(insertData.pos, insertData.node).scrollIntoView()
|
||||||
|
);
|
||||||
|
this.focusEditorInputField();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
emitOnChange() {
|
emitOnChange() {
|
||||||
this.editorView.updateState(this.state);
|
this.editorView.updateState(this.state);
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import {
|
import {
|
||||||
fullSchema,
|
fullSchema,
|
||||||
wootArticleWriterSetup,
|
buildEditor,
|
||||||
EditorView,
|
EditorView,
|
||||||
ArticleMarkdownSerializer,
|
ArticleMarkdownSerializer,
|
||||||
ArticleMarkdownTransformer,
|
ArticleMarkdownTransformer,
|
||||||
@ -33,15 +33,17 @@ const createState = (
|
|||||||
content,
|
content,
|
||||||
placeholder,
|
placeholder,
|
||||||
plugins = [],
|
plugins = [],
|
||||||
onImageUpload = () => {}
|
methods = {},
|
||||||
|
enabledMenuOptions
|
||||||
) => {
|
) => {
|
||||||
return EditorState.create({
|
return EditorState.create({
|
||||||
doc: new ArticleMarkdownTransformer(fullSchema).parse(content),
|
doc: new ArticleMarkdownTransformer(fullSchema).parse(content),
|
||||||
plugins: wootArticleWriterSetup({
|
plugins: buildEditor({
|
||||||
schema: fullSchema,
|
schema: fullSchema,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
methods,
|
||||||
plugins,
|
plugins,
|
||||||
onImageUpload,
|
enabledMenuOptions,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -52,6 +54,7 @@ export default {
|
|||||||
value: { type: String, default: '' },
|
value: { type: String, default: '' },
|
||||||
editorId: { type: String, default: '' },
|
editorId: { type: String, default: '' },
|
||||||
placeholder: { type: String, default: '' },
|
placeholder: { type: String, default: '' },
|
||||||
|
enabledMenuOptions: { type: Array, default: () => [] },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -83,7 +86,8 @@ export default {
|
|||||||
this.value,
|
this.value,
|
||||||
this.placeholder,
|
this.placeholder,
|
||||||
this.plugins,
|
this.plugins,
|
||||||
this.openFileBrowser
|
{ onImageUpload: this.openFileBrowser },
|
||||||
|
this.enabledMenuOptions
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -152,7 +156,8 @@ export default {
|
|||||||
this.value,
|
this.value,
|
||||||
this.placeholder,
|
this.placeholder,
|
||||||
this.plugins,
|
this.plugins,
|
||||||
this.openFileBrowser
|
{ onImageUpload: this.openFileBrowser },
|
||||||
|
this.enabledMenuOptions
|
||||||
);
|
);
|
||||||
this.editorView.updateState(this.state);
|
this.editorView.updateState(this.state);
|
||||||
this.focusEditorInputField();
|
this.focusEditorInputField();
|
||||||
|
|||||||
@ -288,7 +288,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
showMessageSignatureButton() {
|
showMessageSignatureButton() {
|
||||||
return !this.isOnPrivateNote && this.isAnEmailChannel;
|
return !this.isOnPrivateNote;
|
||||||
},
|
},
|
||||||
sendWithSignature() {
|
sendWithSignature() {
|
||||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
const { send_with_signature: isEnabled } = this.uiSettings;
|
||||||
|
|||||||
@ -358,6 +358,7 @@ export default {
|
|||||||
applied_filters: this.appliedFilters.map(filter => ({
|
applied_filters: this.appliedFilters.map(filter => ({
|
||||||
key: filter.attribute_key,
|
key: filter.attribute_key,
|
||||||
operator: filter.filter_operator,
|
operator: filter.filter_operator,
|
||||||
|
query_operator: filter.query_operator,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,7 +8,11 @@
|
|||||||
<div
|
<div
|
||||||
class="flex justify-center items-center mr-4 rtl:mr-0 rtl:ml-4 min-w-0"
|
class="flex justify-center items-center mr-4 rtl:mr-0 rtl:ml-4 min-w-0"
|
||||||
>
|
>
|
||||||
<back-button v-if="showBackButton" :back-url="backButtonUrl" />
|
<back-button
|
||||||
|
v-if="showBackButton"
|
||||||
|
:back-url="backButtonUrl"
|
||||||
|
class="ltr:ml-0 rtl:mr-0 rtl:ml-4"
|
||||||
|
/>
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
:src="currentContact.thumbnail"
|
:src="currentContact.thumbnail"
|
||||||
:badge="inboxBadge"
|
:badge="inboxBadge"
|
||||||
|
|||||||
@ -53,6 +53,9 @@
|
|||||||
class="input"
|
class="input"
|
||||||
:placeholder="messagePlaceHolder"
|
:placeholder="messagePlaceHolder"
|
||||||
:min-height="4"
|
:min-height="4"
|
||||||
|
:signature="signatureToApply"
|
||||||
|
:allow-signature="true"
|
||||||
|
:send-with-signature="sendWithSignature"
|
||||||
@typing-off="onTypingOff"
|
@typing-off="onTypingOff"
|
||||||
@typing-on="onTypingOn"
|
@typing-on="onTypingOn"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@ -69,6 +72,8 @@
|
|||||||
:min-height="4"
|
:min-height="4"
|
||||||
:enable-variables="true"
|
:enable-variables="true"
|
||||||
:variables="messageVariables"
|
:variables="messageVariables"
|
||||||
|
:signature="signatureToApply"
|
||||||
|
:allow-signature="true"
|
||||||
@typing-off="onTypingOff"
|
@typing-off="onTypingOff"
|
||||||
@typing-on="onTypingOn"
|
@typing-on="onTypingOn"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@ -86,16 +91,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isSignatureEnabledForInbox"
|
v-if="isSignatureEnabledForInbox && !isSignatureAvailable"
|
||||||
v-tooltip="$t('CONVERSATION.FOOTER.MESSAGE_SIGN_TOOLTIP')"
|
|
||||||
class="message-signature-wrap"
|
class="message-signature-wrap"
|
||||||
>
|
>
|
||||||
<p
|
<p class="message-signature">
|
||||||
v-if="isSignatureAvailable"
|
|
||||||
v-dompurify-html="formatMessage(messageSignature)"
|
|
||||||
class="message-signature"
|
|
||||||
/>
|
|
||||||
<p v-else class="message-signature">
|
|
||||||
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
|
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
|
||||||
<router-link :to="profilePath">
|
<router-link :to="profilePath">
|
||||||
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
|
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
|
||||||
@ -184,6 +183,12 @@ import wootConstants from 'dashboard/constants/globals';
|
|||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||||
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||||
|
import {
|
||||||
|
appendSignature,
|
||||||
|
removeSignature,
|
||||||
|
replaceSignature,
|
||||||
|
extractTextFromMarkdown,
|
||||||
|
} from 'dashboard/helper/editorHelper';
|
||||||
|
|
||||||
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||||
|
|
||||||
@ -471,10 +476,10 @@ export default {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
isSignatureEnabledForInbox() {
|
isSignatureEnabledForInbox() {
|
||||||
return !this.isPrivate && this.isAnEmailChannel && this.sendWithSignature;
|
return !this.isPrivate && this.sendWithSignature;
|
||||||
},
|
},
|
||||||
isSignatureAvailable() {
|
isSignatureAvailable() {
|
||||||
return !!this.messageSignature;
|
return !!this.signatureToApply;
|
||||||
},
|
},
|
||||||
sendWithSignature() {
|
sendWithSignature() {
|
||||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
const { send_with_signature: isEnabled } = this.uiSettings;
|
||||||
@ -514,6 +519,12 @@ export default {
|
|||||||
});
|
});
|
||||||
return variables;
|
return variables;
|
||||||
},
|
},
|
||||||
|
// ensure that the signature is plain text depending on `showRichContentEditor`
|
||||||
|
signatureToApply() {
|
||||||
|
return this.showRichContentEditor
|
||||||
|
? this.messageSignature
|
||||||
|
: extractTextFromMarkdown(this.messageSignature);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentChat(conversation) {
|
currentChat(conversation) {
|
||||||
@ -581,6 +592,23 @@ export default {
|
|||||||
this.updateUISettings({
|
this.updateUISettings({
|
||||||
display_rich_content_editor: !this.showRichContentEditor,
|
display_rich_content_editor: !this.showRichContentEditor,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const plainTextSignature = extractTextFromMarkdown(this.messageSignature);
|
||||||
|
|
||||||
|
if (!this.showRichContentEditor && this.messageSignature) {
|
||||||
|
// remove the old signature -> extract text from markdown -> attach new signature
|
||||||
|
let message = removeSignature(this.message, this.messageSignature);
|
||||||
|
message = extractTextFromMarkdown(message);
|
||||||
|
message = appendSignature(message, plainTextSignature);
|
||||||
|
|
||||||
|
this.message = message;
|
||||||
|
} else {
|
||||||
|
this.message = replaceSignature(
|
||||||
|
this.message,
|
||||||
|
plainTextSignature,
|
||||||
|
this.messageSignature
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
saveDraft(conversationId, replyType) {
|
saveDraft(conversationId, replyType) {
|
||||||
if (this.message || this.message === '') {
|
if (this.message || this.message === '') {
|
||||||
@ -600,9 +628,22 @@ export default {
|
|||||||
getFromDraft() {
|
getFromDraft() {
|
||||||
if (this.conversationIdByRoute) {
|
if (this.conversationIdByRoute) {
|
||||||
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||||
this.message = this.$store.getters['draftMessages/get'](key) || '';
|
const messageFromStore =
|
||||||
|
this.$store.getters['draftMessages/get'](key) || '';
|
||||||
|
|
||||||
|
// ensure that the message has signature set based on the ui setting
|
||||||
|
this.message = this.toggleSignatureForDraft(messageFromStore);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
toggleSignatureForDraft(message) {
|
||||||
|
if (this.isPrivate) {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.sendWithSignature
|
||||||
|
? appendSignature(message, this.signatureToApply)
|
||||||
|
: removeSignature(message, this.signatureToApply);
|
||||||
|
},
|
||||||
removeFromDraft() {
|
removeFromDraft() {
|
||||||
if (this.conversationIdByRoute) {
|
if (this.conversationIdByRoute) {
|
||||||
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
const key = `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||||
@ -694,19 +735,14 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.showMentions) {
|
if (!this.showMentions) {
|
||||||
let newMessage = this.message;
|
|
||||||
if (this.isSignatureEnabledForInbox && this.messageSignature) {
|
|
||||||
newMessage += '\n\n' + this.messageSignature;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOnWhatsApp =
|
const isOnWhatsApp =
|
||||||
this.isATwilioWhatsAppChannel ||
|
this.isATwilioWhatsAppChannel ||
|
||||||
this.isAWhatsAppCloudChannel ||
|
this.isAWhatsAppCloudChannel ||
|
||||||
this.is360DialogWhatsAppChannel;
|
this.is360DialogWhatsAppChannel;
|
||||||
if (isOnWhatsApp && !this.isPrivate) {
|
if (isOnWhatsApp && !this.isPrivate) {
|
||||||
this.sendMessageAsMultipleMessages(newMessage);
|
this.sendMessageAsMultipleMessages(this.message);
|
||||||
} else {
|
} else {
|
||||||
const messagePayload = this.getMessagePayload(newMessage);
|
const messagePayload = this.getMessagePayload(this.message);
|
||||||
this.sendMessage(messagePayload);
|
this.sendMessage(messagePayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -725,6 +761,15 @@ export default {
|
|||||||
this.sendMessage(messagePayload);
|
this.sendMessage(messagePayload);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
sendMessageAnalyticsData(isPrivate) {
|
||||||
|
// Analytics data for message signature is enabled or not in channels
|
||||||
|
return isPrivate
|
||||||
|
? this.$track(CONVERSATION_EVENTS.SENT_PRIVATE_NOTE)
|
||||||
|
: this.$track(CONVERSATION_EVENTS.SENT_MESSAGE, {
|
||||||
|
channelType: this.channelType,
|
||||||
|
signatureEnabled: this.sendWithSignature,
|
||||||
|
});
|
||||||
|
},
|
||||||
async onSendReply() {
|
async onSendReply() {
|
||||||
const undefinedVariables = getUndefinedVariablesInMessage({
|
const undefinedVariables = getUndefinedVariablesInMessage({
|
||||||
message: this.message,
|
message: this.message,
|
||||||
@ -758,6 +803,7 @@ export default {
|
|||||||
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||||
bus.$emit(BUS_EVENTS.MESSAGE_SENT);
|
bus.$emit(BUS_EVENTS.MESSAGE_SENT);
|
||||||
this.removeFromDraft();
|
this.removeFromDraft();
|
||||||
|
this.sendMessageAnalyticsData(messagePayload.private);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
|
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
|
||||||
@ -818,6 +864,10 @@ export default {
|
|||||||
},
|
},
|
||||||
clearMessage() {
|
clearMessage() {
|
||||||
this.message = '';
|
this.message = '';
|
||||||
|
if (this.sendWithSignature && !this.isPrivate) {
|
||||||
|
// if signature is enabled, append it to the message
|
||||||
|
this.message = appendSignature(this.message, this.signatureToApply);
|
||||||
|
}
|
||||||
this.attachedFiles = [];
|
this.attachedFiles = [];
|
||||||
this.isRecordingAudio = false;
|
this.isRecordingAudio = false;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -95,7 +95,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.mention--box {
|
.mention--box {
|
||||||
@apply bg-white text-sm dark:bg-slate-700 rounded-md overflow-auto absolute w-full z-[100] pt-2 px-2 pb-0 shadow-md left-0 leading-[1.2] bottom-full max-h-[12.5rem] border-t border-solid border-slate-75 dark:border-slate-800;
|
@apply bg-white text-sm dark:bg-slate-700 rounded-md overflow-auto absolute w-full z-20 pt-2 px-2 pb-0 shadow-md left-0 leading-[1.2] bottom-full max-h-[12.5rem] border-t border-solid border-slate-75 dark:border-slate-800;
|
||||||
|
|
||||||
&.with-bottom-border {
|
&.with-bottom-border {
|
||||||
@apply border-b-[0.5rem] border-solid border-white dark:border-slate-600;
|
@apply border-b-[0.5rem] border-solid border-white dark:border-slate-600;
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export default {
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.mention--box {
|
.mention--box {
|
||||||
@apply bg-white dark:bg-slate-700 rounded-md overflow-auto absolute w-full z-[100] pt-2 px-2 pb-0 shadow-md left-0 bottom-full max-h-[9.75rem] border-t border-solid border-slate-75 dark:border-slate-800;
|
@apply bg-white dark:bg-slate-700 rounded-md overflow-auto absolute w-full z-20 pt-2 px-2 pb-0 shadow-md left-0 bottom-full max-h-[9.75rem] border-t border-solid border-slate-75 dark:border-slate-800;
|
||||||
|
|
||||||
.dropdown-menu__item:last-child {
|
.dropdown-menu__item:last-child {
|
||||||
@apply pb-1;
|
@apply pb-1;
|
||||||
|
|||||||
34
app/javascript/dashboard/constants/editor.js
Normal file
34
app/javascript/dashboard/constants/editor.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export const MESSAGE_EDITOR_MENU_OPTIONS = [
|
||||||
|
'strong',
|
||||||
|
'em',
|
||||||
|
'link',
|
||||||
|
'undo',
|
||||||
|
'redo',
|
||||||
|
'bulletList',
|
||||||
|
'orderedList',
|
||||||
|
'code',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MESSAGE_SIGNATURE_EDITOR_MENU_OPTIONS = [
|
||||||
|
'strong',
|
||||||
|
'em',
|
||||||
|
'link',
|
||||||
|
'undo',
|
||||||
|
'redo',
|
||||||
|
'imageUpload',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ARTICLE_EDITOR_MENU_OPTIONS = [
|
||||||
|
'strong',
|
||||||
|
'em',
|
||||||
|
'link',
|
||||||
|
'undo',
|
||||||
|
'redo',
|
||||||
|
'bulletList',
|
||||||
|
'orderedList',
|
||||||
|
'h1',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'imageUpload',
|
||||||
|
'code',
|
||||||
|
];
|
||||||
@ -15,6 +15,9 @@ export const ACCOUNT_EVENTS = Object.freeze({
|
|||||||
ADDED_A_CUSTOM_ATTRIBUTE: 'Added a custom attribute',
|
ADDED_A_CUSTOM_ATTRIBUTE: 'Added a custom attribute',
|
||||||
ADDED_AN_INBOX: 'Added an inbox',
|
ADDED_AN_INBOX: 'Added an inbox',
|
||||||
OPEN_MESSAGE_CONTEXT_MENU: 'Opened message context menu',
|
OPEN_MESSAGE_CONTEXT_MENU: 'Opened message context menu',
|
||||||
|
OPENED_NOTIFICATIONS: 'Opened notifications',
|
||||||
|
MARK_AS_READ_NOTIFICATIONS: 'Marked notifications as read',
|
||||||
|
OPEN_CONVERSATION_VIA_NOTIFICATION: 'Opened conversation via notification',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const LABEL_EVENTS = Object.freeze({
|
export const LABEL_EVENTS = Object.freeze({
|
||||||
|
|||||||
@ -120,9 +120,18 @@ export const generateConditionOptions = (options, key = 'id') => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add the "None" option to the agent list
|
||||||
|
export const agentList = agents => [
|
||||||
|
{
|
||||||
|
id: 'nil',
|
||||||
|
name: 'None',
|
||||||
|
},
|
||||||
|
...(agents || []),
|
||||||
|
];
|
||||||
|
|
||||||
export const getActionOptions = ({ agents, teams, labels, type }) => {
|
export const getActionOptions = ({ agents, teams, labels, type }) => {
|
||||||
const actionsMap = {
|
const actionsMap = {
|
||||||
assign_agent: agents,
|
assign_agent: agentList(agents),
|
||||||
assign_team: teams,
|
assign_team: teams,
|
||||||
send_email_to_team: teams,
|
send_email_to_team: teams,
|
||||||
add_label: generateConditionOptions(labels, 'title'),
|
add_label: generateConditionOptions(labels, 'title'),
|
||||||
|
|||||||
135
app/javascript/dashboard/helper/editorHelper.js
Normal file
135
app/javascript/dashboard/helper/editorHelper.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* The delimiter used to separate the signature from the rest of the body.
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
export const SIGNATURE_DELIMITER = '--';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trim the signature and remove all " \r" from the signature
|
||||||
|
* 1. Trim any extra lines or spaces at the start or end of the string
|
||||||
|
* 2. Converts all \r or \r\n to \f
|
||||||
|
*/
|
||||||
|
export function cleanSignature(signature) {
|
||||||
|
return signature.trim().replace(/\r\n?/g, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the signature delimiter to the beginning of the signature.
|
||||||
|
*
|
||||||
|
* @param {string} signature - The signature to add the delimiter to.
|
||||||
|
* @returns {string} - The signature with the delimiter added.
|
||||||
|
*/
|
||||||
|
function appendDelimiter(signature) {
|
||||||
|
return `${SIGNATURE_DELIMITER}\n\n${cleanSignature(signature)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there's an unedited signature at the end of the body
|
||||||
|
* If there is, return the index of the signature, If there isn't, return -1
|
||||||
|
*
|
||||||
|
* @param {string} body - The body to search for the signature.
|
||||||
|
* @param {string} signature - The signature to search for.
|
||||||
|
* @returns {number} - The index of the last occurrence of the signature in the body, or -1 if not found.
|
||||||
|
*/
|
||||||
|
export function findSignatureInBody(body, signature) {
|
||||||
|
const trimmedBody = body.trimEnd();
|
||||||
|
const cleanedSignature = cleanSignature(signature);
|
||||||
|
|
||||||
|
// check if body ends with signature
|
||||||
|
if (trimmedBody.endsWith(cleanedSignature)) {
|
||||||
|
return body.lastIndexOf(cleanedSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends the signature to the body, separated by the signature delimiter.
|
||||||
|
*
|
||||||
|
* @param {string} body - The body to append the signature to.
|
||||||
|
* @param {string} signature - The signature to append.
|
||||||
|
* @returns {string} - The body with the signature appended.
|
||||||
|
*/
|
||||||
|
export function appendSignature(body, signature) {
|
||||||
|
const cleanedSignature = cleanSignature(signature);
|
||||||
|
// if signature is already present, return body
|
||||||
|
if (findSignatureInBody(body, cleanedSignature) > -1) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${body.trimEnd()}\n\n${appendDelimiter(cleanedSignature)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the signature from the body, along with the signature delimiter.
|
||||||
|
*
|
||||||
|
* @param {string} body - The body to remove the signature from.
|
||||||
|
* @param {string} signature - The signature to remove.
|
||||||
|
* @returns {string} - The body with the signature removed.
|
||||||
|
*/
|
||||||
|
export function removeSignature(body, signature) {
|
||||||
|
// this will find the index of the signature if it exists
|
||||||
|
// Regardless of extra spaces or new lines after the signature, the index will be the same if present
|
||||||
|
const cleanedSignature = cleanSignature(signature);
|
||||||
|
const signatureIndex = findSignatureInBody(body, cleanedSignature);
|
||||||
|
|
||||||
|
// no need to trim the ends here, because it will simply be removed in the next method
|
||||||
|
let newBody = body;
|
||||||
|
|
||||||
|
// if signature is present, remove it and trim it
|
||||||
|
// trimming will ensure any spaces or new lines before the signature are removed
|
||||||
|
// This means we will have the delimiter at the end
|
||||||
|
if (signatureIndex > -1) {
|
||||||
|
newBody = newBody.substring(0, signatureIndex).trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
// let's find the delimiter and remove it
|
||||||
|
const delimiterIndex = newBody.lastIndexOf(SIGNATURE_DELIMITER);
|
||||||
|
if (
|
||||||
|
delimiterIndex !== -1 &&
|
||||||
|
delimiterIndex === newBody.length - SIGNATURE_DELIMITER.length // this will ensure the delimiter is at the end
|
||||||
|
) {
|
||||||
|
// if the delimiter is at the end, remove it
|
||||||
|
newBody = newBody.substring(0, delimiterIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the value
|
||||||
|
return newBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the old signature with the new signature.
|
||||||
|
* If the old signature is not present, it will append the new signature.
|
||||||
|
*
|
||||||
|
* @param {string} body - The body to replace the signature in.
|
||||||
|
* @param {string} oldSignature - The signature to replace.
|
||||||
|
* @param {string} newSignature - The signature to replace the old signature with.
|
||||||
|
* @returns {string} - The body with the old signature replaced with the new signature.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function replaceSignature(body, oldSignature, newSignature) {
|
||||||
|
const withoutSignature = removeSignature(body, oldSignature);
|
||||||
|
return appendSignature(withoutSignature, newSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract text from markdown, and remove all images, code blocks, links, headers, bold, italic, lists etc.
|
||||||
|
* Links will be converted to text, and not removed.
|
||||||
|
*
|
||||||
|
* @param {string} markdown - markdown text to be extracted
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function extractTextFromMarkdown(markdown) {
|
||||||
|
return markdown
|
||||||
|
.replace(/```[\s\S]*?```/g, '') // Remove code blocks
|
||||||
|
.replace(/`.*?`/g, '') // Remove inline code
|
||||||
|
.replace(/!\[.*?\]\(.*?\)/g, '') // Remove images before removing links
|
||||||
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Remove links but keep the text
|
||||||
|
.replace(/#+\s*|[*_-]{1,3}/g, '') // Remove headers, bold, italic, lists etc.
|
||||||
|
.split('\n')
|
||||||
|
.map(line => line.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n') // Trim each line & remove any lines only having spaces
|
||||||
|
.replace(/\n{2,}/g, '\n') // Remove multiple consecutive newlines (blank lines)
|
||||||
|
.trim(); // Trim any extra space
|
||||||
|
}
|
||||||
40
app/javascript/dashboard/helper/messageEditorHelper.js
Normal file
40
app/javascript/dashboard/helper/messageEditorHelper.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
/**
|
||||||
|
* Determines the appropriate node and position to insert an image in the editor.
|
||||||
|
*
|
||||||
|
* Based on the current editor state and the provided image URL, this function finds out the correct node (either
|
||||||
|
* a standalone image node or an image wrapped in a paragraph) and its respective position in the editor.
|
||||||
|
*
|
||||||
|
* 1. If the current node is a paragraph and doesn't contain an image or text, the image is inserted directly into it.
|
||||||
|
* 2. If the current node isn't a paragraph or it's a paragraph containing text, the image will be wrapped
|
||||||
|
* in a new paragraph and then inserted.
|
||||||
|
* 3. If the current node is a paragraph containing an image, the new image will be inserted directly into it.
|
||||||
|
*
|
||||||
|
* @param {Object} editorState - The current state of the editor. It provides necessary details like selection, schema, etc.
|
||||||
|
* @param {string} fileUrl - The URL of the image to be inserted into the editor.
|
||||||
|
* @returns {Object|null} An object containing details about the node to be inserted and its position. It returns null if no image node can be created.
|
||||||
|
* @property {Node} node - The ProseMirror node to be inserted (either an image node or a paragraph containing the image).
|
||||||
|
* @property {number} pos - The position where the new node should be inserted in the editor.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const findNodeToInsertImage = (editorState, fileUrl) => {
|
||||||
|
const { selection, schema } = editorState;
|
||||||
|
const { nodes } = schema;
|
||||||
|
const currentNode = selection.$from.node();
|
||||||
|
const {
|
||||||
|
type: { name: typeName },
|
||||||
|
content: { size, content },
|
||||||
|
} = currentNode;
|
||||||
|
|
||||||
|
const imageNode = nodes.image.create({ src: fileUrl });
|
||||||
|
|
||||||
|
if (!imageNode) return null;
|
||||||
|
|
||||||
|
const isInParagraph = typeName === 'paragraph';
|
||||||
|
const needsNewLine =
|
||||||
|
!content.some(n => n.type.name === 'image') && size !== 0 ? 1 : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: isInParagraph ? imageNode : nodes.paragraph.create({}, imageNode),
|
||||||
|
pos: selection.from + needsNewLine,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -49,3 +49,38 @@ export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => {
|
|||||||
// Proceed to the route if none of the above conditions are met
|
// Proceed to the route if none of the above conditions are met
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isAConversationRoute = routeName =>
|
||||||
|
[
|
||||||
|
'inbox_conversation',
|
||||||
|
'conversation_through_mentions',
|
||||||
|
'conversation_through_unattended',
|
||||||
|
'conversation_through_inbox',
|
||||||
|
'conversations_through_label',
|
||||||
|
'conversations_through_team',
|
||||||
|
'conversations_through_folders',
|
||||||
|
'conversation_through_participating',
|
||||||
|
].includes(routeName);
|
||||||
|
|
||||||
|
export const getConversationDashboardRoute = routeName => {
|
||||||
|
switch (routeName) {
|
||||||
|
case 'inbox_conversation':
|
||||||
|
return 'home';
|
||||||
|
case 'conversation_through_mentions':
|
||||||
|
return 'conversation_mentions';
|
||||||
|
case 'conversation_through_unattended':
|
||||||
|
return 'conversation_unattended';
|
||||||
|
case 'conversations_through_label':
|
||||||
|
return 'label_conversations';
|
||||||
|
case 'conversations_through_team':
|
||||||
|
return 'team_conversations';
|
||||||
|
case 'conversations_through_folders':
|
||||||
|
return 'folder_conversations';
|
||||||
|
case 'conversation_through_participating':
|
||||||
|
return 'conversation_participating';
|
||||||
|
case 'conversation_through_inbox':
|
||||||
|
return 'inbox_dashboard';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
157
app/javascript/dashboard/helper/specs/editorHelper.spec.js
Normal file
157
app/javascript/dashboard/helper/specs/editorHelper.spec.js
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import {
|
||||||
|
findSignatureInBody,
|
||||||
|
appendSignature,
|
||||||
|
removeSignature,
|
||||||
|
replaceSignature,
|
||||||
|
extractTextFromMarkdown,
|
||||||
|
} from '../editorHelper';
|
||||||
|
|
||||||
|
const NEW_SIGNATURE = 'This is a new signature';
|
||||||
|
|
||||||
|
const DOES_NOT_HAVE_SIGNATURE = {
|
||||||
|
'no signature': {
|
||||||
|
body: 'This is a test',
|
||||||
|
signature: 'This is a signature',
|
||||||
|
},
|
||||||
|
'text after signature': {
|
||||||
|
body: 'This is a test\n\n--\n\nThis is a signature\n\nThis is more text',
|
||||||
|
signature: 'This is a signature',
|
||||||
|
},
|
||||||
|
signature_has_images: {
|
||||||
|
body: 'This is a test',
|
||||||
|
signature:
|
||||||
|
'Testing \n',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const HAS_SIGNATURE = {
|
||||||
|
'signature at end': {
|
||||||
|
body: 'This is a test\n\n--\n\nThis is a signature',
|
||||||
|
signature: 'This is a signature',
|
||||||
|
},
|
||||||
|
'signature at end with spaces and new lines': {
|
||||||
|
body: 'This is a test\n\n--\n\nThis is a signature \n\n',
|
||||||
|
signature: 'This is a signature ',
|
||||||
|
},
|
||||||
|
'no text before signature': {
|
||||||
|
body: '\n\n--\n\nThis is a signature',
|
||||||
|
signature: 'This is a signature',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('findSignatureInBody', () => {
|
||||||
|
it('returns -1 if there is no signature', () => {
|
||||||
|
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
|
||||||
|
const { body, signature } = DOES_NOT_HAVE_SIGNATURE[key];
|
||||||
|
expect(findSignatureInBody(body, signature)).toBe(-1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('returns the index of the signature if there is one', () => {
|
||||||
|
Object.keys(HAS_SIGNATURE).forEach(key => {
|
||||||
|
const { body, signature } = HAS_SIGNATURE[key];
|
||||||
|
expect(findSignatureInBody(body, signature)).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('appendSignature', () => {
|
||||||
|
it('appends the signature if it is not present', () => {
|
||||||
|
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
|
||||||
|
const { body, signature } = DOES_NOT_HAVE_SIGNATURE[key];
|
||||||
|
expect(appendSignature(body, signature)).toBe(
|
||||||
|
`${body}\n\n--\n\n${signature}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('does not append signature if already present', () => {
|
||||||
|
Object.keys(HAS_SIGNATURE).forEach(key => {
|
||||||
|
const { body, signature } = HAS_SIGNATURE[key];
|
||||||
|
expect(appendSignature(body, signature)).toBe(body);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeSignature', () => {
|
||||||
|
it('does not remove signature if not present', () => {
|
||||||
|
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
|
||||||
|
const { body, signature } = DOES_NOT_HAVE_SIGNATURE[key];
|
||||||
|
expect(removeSignature(body, signature)).toBe(body);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('removes signature if present at the end', () => {
|
||||||
|
const { body, signature } = HAS_SIGNATURE['signature at end'];
|
||||||
|
expect(removeSignature(body, signature)).toBe('This is a test\n\n');
|
||||||
|
});
|
||||||
|
it('removes signature if present with spaces and new lines', () => {
|
||||||
|
const { body, signature } = HAS_SIGNATURE[
|
||||||
|
'signature at end with spaces and new lines'
|
||||||
|
];
|
||||||
|
expect(removeSignature(body, signature)).toBe('This is a test\n\n');
|
||||||
|
});
|
||||||
|
it('removes signature if present without text before it', () => {
|
||||||
|
const { body, signature } = HAS_SIGNATURE['no text before signature'];
|
||||||
|
expect(removeSignature(body, signature)).toBe('\n\n');
|
||||||
|
});
|
||||||
|
it('removes just the delimiter if no signature is present', () => {
|
||||||
|
expect(removeSignature('This is a test\n\n--', 'This is a signature')).toBe(
|
||||||
|
'This is a test\n\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('replaceSignature', () => {
|
||||||
|
it('appends the new signature if not present', () => {
|
||||||
|
Object.keys(DOES_NOT_HAVE_SIGNATURE).forEach(key => {
|
||||||
|
const { body, signature } = DOES_NOT_HAVE_SIGNATURE[key];
|
||||||
|
expect(replaceSignature(body, signature, NEW_SIGNATURE)).toBe(
|
||||||
|
`${body}\n\n--\n\n${NEW_SIGNATURE}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('removes signature if present at the end', () => {
|
||||||
|
const { body, signature } = HAS_SIGNATURE['signature at end'];
|
||||||
|
expect(replaceSignature(body, signature, NEW_SIGNATURE)).toBe(
|
||||||
|
`This is a test\n\n--\n\n${NEW_SIGNATURE}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('removes signature if present with spaces and new lines', () => {
|
||||||
|
const { body, signature } = HAS_SIGNATURE[
|
||||||
|
'signature at end with spaces and new lines'
|
||||||
|
];
|
||||||
|
expect(replaceSignature(body, signature, NEW_SIGNATURE)).toBe(
|
||||||
|
`This is a test\n\n--\n\n${NEW_SIGNATURE}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('removes signature if present without text before it', () => {
|
||||||
|
const { body, signature } = HAS_SIGNATURE['no text before signature'];
|
||||||
|
expect(replaceSignature(body, signature, NEW_SIGNATURE)).toBe(
|
||||||
|
`\n\n--\n\n${NEW_SIGNATURE}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extractTextFromMarkdown', () => {
|
||||||
|
it('should extract text from markdown and remove all images, code blocks, links, headers, bold, italic, lists etc.', () => {
|
||||||
|
const markdown = `
|
||||||
|
# Hello World
|
||||||
|
|
||||||
|
This is a **bold** text with a [link](https://example.com).
|
||||||
|
|
||||||
|
\`\`\`javascript
|
||||||
|
const foo = 'bar';
|
||||||
|
console.log(foo);
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Here's an image: 
|
||||||
|
|
||||||
|
- List item 1
|
||||||
|
- List item 2
|
||||||
|
|
||||||
|
*Italic text*
|
||||||
|
`;
|
||||||
|
|
||||||
|
const expected =
|
||||||
|
"Hello World\nThis is a bold text with a link.\nHere's an image:\nList item 1\nList item 2\nItalic text";
|
||||||
|
expect(extractTextFromMarkdown(markdown)).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,100 @@
|
|||||||
|
import { findNodeToInsertImage } from '../messageEditorHelper';
|
||||||
|
|
||||||
|
describe('findNodeToInsertImage', () => {
|
||||||
|
let mockEditorState;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockEditorState = {
|
||||||
|
selection: {
|
||||||
|
$from: {
|
||||||
|
node: jest.fn(() => ({})),
|
||||||
|
},
|
||||||
|
from: 0,
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
nodes: {
|
||||||
|
image: {
|
||||||
|
create: jest.fn(attrs => ({ type: { name: 'image' }, attrs })),
|
||||||
|
},
|
||||||
|
paragraph: {
|
||||||
|
create: jest.fn((_, node) => ({
|
||||||
|
type: { name: 'paragraph' },
|
||||||
|
content: [node],
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert image directly into an empty paragraph', () => {
|
||||||
|
const mockNode = {
|
||||||
|
type: { name: 'paragraph' },
|
||||||
|
content: { size: 0, content: [] },
|
||||||
|
};
|
||||||
|
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
|
||||||
|
|
||||||
|
const result = findNodeToInsertImage(mockEditorState, 'image-url');
|
||||||
|
expect(result).toEqual({
|
||||||
|
node: { type: { name: 'image' }, attrs: { src: 'image-url' } },
|
||||||
|
pos: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert image directly into a paragraph without an image but with other content', () => {
|
||||||
|
const mockNode = {
|
||||||
|
type: { name: 'paragraph' },
|
||||||
|
content: {
|
||||||
|
size: 1,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: { name: 'text' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
|
||||||
|
mockEditorState.selection.from = 1;
|
||||||
|
|
||||||
|
const result = findNodeToInsertImage(mockEditorState, 'image-url');
|
||||||
|
expect(result).toEqual({
|
||||||
|
node: { type: { name: 'image' }, attrs: { src: 'image-url' } },
|
||||||
|
pos: 2, // Because it should insert after the text, on a new line.
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should wrap image in a new paragraph when the current node isn't a paragraph", () => {
|
||||||
|
const mockNode = {
|
||||||
|
type: { name: 'not-a-paragraph' },
|
||||||
|
content: { size: 0, content: [] },
|
||||||
|
};
|
||||||
|
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
|
||||||
|
|
||||||
|
const result = findNodeToInsertImage(mockEditorState, 'image-url');
|
||||||
|
expect(result.node.type.name).toBe('paragraph');
|
||||||
|
expect(result.node.content[0].type.name).toBe('image');
|
||||||
|
expect(result.node.content[0].attrs.src).toBe('image-url');
|
||||||
|
expect(result.pos).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should insert a new image directly into the paragraph that already contains an image', () => {
|
||||||
|
const mockNode = {
|
||||||
|
type: { name: 'paragraph' },
|
||||||
|
content: {
|
||||||
|
size: 1,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: { name: 'image', attrs: { src: 'existing-image-url' } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
|
||||||
|
mockEditorState.selection.from = 1;
|
||||||
|
|
||||||
|
const result = findNodeToInsertImage(mockEditorState, 'image-url');
|
||||||
|
expect(result.node.type.name).toBe('image');
|
||||||
|
expect(result.node.attrs.src).toBe('image-url');
|
||||||
|
expect(result.pos).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
|
getConversationDashboardRoute,
|
||||||
getCurrentAccount,
|
getCurrentAccount,
|
||||||
getUserRole,
|
getUserRole,
|
||||||
|
isAConversationRoute,
|
||||||
routeIsAccessibleFor,
|
routeIsAccessibleFor,
|
||||||
validateLoggedInRoutes,
|
validateLoggedInRoutes,
|
||||||
} from '../routeHelpers';
|
} from '../routeHelpers';
|
||||||
@ -94,3 +96,41 @@ describe('#validateLoggedInRoutes', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isAConversationRoute', () => {
|
||||||
|
it('returns true if conversation route name is provided', () => {
|
||||||
|
expect(isAConversationRoute('inbox_conversation')).toBe(true);
|
||||||
|
expect(isAConversationRoute('conversation_through_inbox')).toBe(true);
|
||||||
|
expect(isAConversationRoute('conversations_through_label')).toBe(true);
|
||||||
|
expect(isAConversationRoute('conversations_through_team')).toBe(true);
|
||||||
|
expect(isAConversationRoute('dashboard')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getConversationDashboardRoute', () => {
|
||||||
|
it('returns dashboard route for conversation', () => {
|
||||||
|
expect(getConversationDashboardRoute('inbox_conversation')).toEqual('home');
|
||||||
|
expect(
|
||||||
|
getConversationDashboardRoute('conversation_through_mentions')
|
||||||
|
).toEqual('conversation_mentions');
|
||||||
|
expect(
|
||||||
|
getConversationDashboardRoute('conversation_through_unattended')
|
||||||
|
).toEqual('conversation_unattended');
|
||||||
|
expect(
|
||||||
|
getConversationDashboardRoute('conversations_through_label')
|
||||||
|
).toEqual('label_conversations');
|
||||||
|
expect(getConversationDashboardRoute('conversations_through_team')).toEqual(
|
||||||
|
'team_conversations'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
getConversationDashboardRoute('conversations_through_folders')
|
||||||
|
).toEqual('folder_conversations');
|
||||||
|
expect(
|
||||||
|
getConversationDashboardRoute('conversation_through_participating')
|
||||||
|
).toEqual('conversation_participating');
|
||||||
|
expect(getConversationDashboardRoute('conversation_through_inbox')).toEqual(
|
||||||
|
'inbox_dashboard'
|
||||||
|
);
|
||||||
|
expect(getConversationDashboardRoute('non_existent_route')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
53
app/javascript/dashboard/helper/specs/uploadHelper.spec.js
Normal file
53
app/javascript/dashboard/helper/specs/uploadHelper.spec.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { uploadFile } from '../uploadHelper';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
// Mocking axios using jest-mock-axios
|
||||||
|
global.axios = axios;
|
||||||
|
jest.mock('axios');
|
||||||
|
|
||||||
|
describe('#Upload Helpers', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
// Cleaning up the mock after each test
|
||||||
|
axios.post.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send a POST request with correct data', async () => {
|
||||||
|
const mockFile = new File(['dummy content'], 'example.png', {
|
||||||
|
type: 'image/png',
|
||||||
|
});
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
file_url: 'https://example.com/fileUrl',
|
||||||
|
blob_key: 'blobKey123',
|
||||||
|
blob_id: 'blobId456',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
axios.post.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const result = await uploadFile(mockFile);
|
||||||
|
|
||||||
|
expect(axios.post).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/upload',
|
||||||
|
expect.any(FormData),
|
||||||
|
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
fileUrl: 'https://example.com/fileUrl',
|
||||||
|
blobKey: 'blobKey123',
|
||||||
|
blobId: 'blobId456',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors', async () => {
|
||||||
|
const mockFile = new File(['dummy content'], 'example.png', {
|
||||||
|
type: 'image/png',
|
||||||
|
});
|
||||||
|
const mockError = new Error('Failed to upload');
|
||||||
|
|
||||||
|
axios.post.mockRejectedValueOnce(mockError);
|
||||||
|
|
||||||
|
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
|
||||||
|
});
|
||||||
|
});
|
||||||
41
app/javascript/dashboard/helper/uploadHelper.js
Normal file
41
app/javascript/dashboard/helper/uploadHelper.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
/* global axios */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants and Configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Version for the API endpoint.
|
||||||
|
const API_VERSION = 'v1';
|
||||||
|
|
||||||
|
// Default headers to be used in the axios request.
|
||||||
|
const HEADERS = {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a file to the server.
|
||||||
|
*
|
||||||
|
* This function sends a POST request to a given API endpoint and uploads the specified file.
|
||||||
|
* The function uses FormData to wrap the file and axios to send the request.
|
||||||
|
*
|
||||||
|
* @param {File} file - The file to be uploaded. It should be a File object (typically coming from a file input element).
|
||||||
|
* @returns {Promise} A promise that resolves with the server's response when the upload is successful, or rejects if there's an error.
|
||||||
|
*/
|
||||||
|
export async function uploadFile(file) {
|
||||||
|
// Create a new FormData instance.
|
||||||
|
let formData = new FormData();
|
||||||
|
|
||||||
|
// Append the file to the FormData instance under the key 'attachment'.
|
||||||
|
formData.append('attachment', file);
|
||||||
|
|
||||||
|
// Use axios to send a POST request to the upload endpoint.
|
||||||
|
const { data } = await axios.post(`/api/${API_VERSION}/upload`, formData, {
|
||||||
|
headers: HEADERS,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
fileUrl: data.file_url,
|
||||||
|
blobKey: data.blob_key,
|
||||||
|
blobId: data.blob_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -26,6 +26,7 @@ import pt_BR from './locale/pt_BR';
|
|||||||
import ro from './locale/ro';
|
import ro from './locale/ro';
|
||||||
import ru from './locale/ru';
|
import ru from './locale/ru';
|
||||||
import sk from './locale/sk';
|
import sk from './locale/sk';
|
||||||
|
import sr from './locale/sr';
|
||||||
import sv from './locale/sv';
|
import sv from './locale/sv';
|
||||||
import ta from './locale/ta';
|
import ta from './locale/ta';
|
||||||
import th from './locale/th';
|
import th from './locale/th';
|
||||||
@ -66,6 +67,7 @@ export default {
|
|||||||
ro,
|
ro,
|
||||||
ru,
|
ru,
|
||||||
sk,
|
sk,
|
||||||
|
sr,
|
||||||
sv,
|
sv,
|
||||||
ta,
|
ta,
|
||||||
th,
|
th,
|
||||||
|
|||||||
@ -56,6 +56,14 @@
|
|||||||
"EDIT": "%{agentName} updated a macro (#%{id})",
|
"EDIT": "%{agentName} updated a macro (#%{id})",
|
||||||
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
||||||
},
|
},
|
||||||
|
"INBOX_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
|
||||||
|
},
|
||||||
|
"TEAM_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
|
||||||
|
},
|
||||||
"ACCOUNT": {
|
"ACCOUNT": {
|
||||||
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
"SUBMIT": "Import",
|
"SUBMIT": "Import",
|
||||||
"CANCEL": "Cancel"
|
"CANCEL": "Cancel"
|
||||||
},
|
},
|
||||||
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
|
||||||
"ERROR_MESSAGE": "There was an error, please try again"
|
"ERROR_MESSAGE": "There was an error, please try again"
|
||||||
},
|
},
|
||||||
"EXPORT_CONTACTS": {
|
"EXPORT_CONTACTS": {
|
||||||
|
|||||||
@ -88,7 +88,8 @@
|
|||||||
"UPDATE": "Update",
|
"UPDATE": "Update",
|
||||||
"BUTTON_TEXT": "Connect channel",
|
"BUTTON_TEXT": "Connect channel",
|
||||||
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
||||||
"ATTENTION_REQUIRED": "Attention required"
|
"ATTENTION_REQUIRED": "Attention required",
|
||||||
|
"EXPIRED": "Your Slack integration has expired. To continue receiving messages on Slack, please delete the integration and connect your workspace again."
|
||||||
},
|
},
|
||||||
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
||||||
"UPDATE_SUCCESS": "The channel is connected successfully",
|
"UPDATE_SUCCESS": "The channel is connected successfully",
|
||||||
@ -124,6 +125,18 @@
|
|||||||
"CANCEL": "Cancel"
|
"CANCEL": "Cancel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CTA_MODAL": {
|
||||||
|
"TITLE": "Integrate with OpenAI",
|
||||||
|
"DESC": "Bring advanced AI features to your dashboard with OpenAI's GPT models. To begin, enter the API key from your OpenAI account.",
|
||||||
|
"KEY_PLACEHOLDER": "Enter your OpenAI API key",
|
||||||
|
"BUTTONS": {
|
||||||
|
"NEED_HELP": "Need help?",
|
||||||
|
"DISMISS": "Dismiss",
|
||||||
|
"FINISH": "Finish Setup"
|
||||||
|
},
|
||||||
|
"DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.",
|
||||||
|
"SUCCESS_MESSAGE": "OpenAI integration setup successfully"
|
||||||
|
},
|
||||||
"TITLE": "Improve With AI",
|
"TITLE": "Improve With AI",
|
||||||
"SUMMARY_TITLE": "Summary with AI",
|
"SUMMARY_TITLE": "Summary with AI",
|
||||||
"REPLY_TITLE": "Reply suggestion with AI",
|
"REPLY_TITLE": "Reply suggestion with AI",
|
||||||
|
|||||||
@ -39,7 +39,10 @@
|
|||||||
"NOTE": "Create a personal message signature that would be added to all the messages you send from your email inbox. Use the rich content editor to create a highly personalised signature.",
|
"NOTE": "Create a personal message signature that would be added to all the messages you send from your email inbox. Use the rich content editor to create a highly personalised signature.",
|
||||||
"BTN_TEXT": "Save message signature",
|
"BTN_TEXT": "Save message signature",
|
||||||
"API_ERROR": "Couldn't save signature! Try again",
|
"API_ERROR": "Couldn't save signature! Try again",
|
||||||
"API_SUCCESS": "Signature saved successfully"
|
"API_SUCCESS": "Signature saved successfully",
|
||||||
|
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
|
||||||
|
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
|
||||||
|
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB"
|
||||||
},
|
},
|
||||||
"MESSAGE_SIGNATURE": {
|
"MESSAGE_SIGNATURE": {
|
||||||
"LABEL": "Message Signature",
|
"LABEL": "Message Signature",
|
||||||
|
|||||||
@ -56,6 +56,14 @@
|
|||||||
"EDIT": "%{agentName} updated a macro (#%{id})",
|
"EDIT": "%{agentName} updated a macro (#%{id})",
|
||||||
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
||||||
},
|
},
|
||||||
|
"INBOX_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
|
||||||
|
},
|
||||||
|
"TEAM_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
|
||||||
|
},
|
||||||
"ACCOUNT": {
|
"ACCOUNT": {
|
||||||
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
"SUBMIT": "استيراد",
|
"SUBMIT": "استيراد",
|
||||||
"CANCEL": "إلغاء"
|
"CANCEL": "إلغاء"
|
||||||
},
|
},
|
||||||
"SUCCESS_MESSAGE": "تم حفظ جهة الاتصال بنجاح",
|
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
|
||||||
"ERROR_MESSAGE": "حدث خطأ، الرجاء المحاولة مرة أخرى"
|
"ERROR_MESSAGE": "حدث خطأ، الرجاء المحاولة مرة أخرى"
|
||||||
},
|
},
|
||||||
"EXPORT_CONTACTS": {
|
"EXPORT_CONTACTS": {
|
||||||
|
|||||||
@ -88,7 +88,8 @@
|
|||||||
"UPDATE": "تحديث",
|
"UPDATE": "تحديث",
|
||||||
"BUTTON_TEXT": "Connect channel",
|
"BUTTON_TEXT": "Connect channel",
|
||||||
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
||||||
"ATTENTION_REQUIRED": "Attention required"
|
"ATTENTION_REQUIRED": "Attention required",
|
||||||
|
"EXPIRED": "Your Slack integration has expired. To continue receiving messages on Slack, please delete the integration and connect your workspace again."
|
||||||
},
|
},
|
||||||
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
||||||
"UPDATE_SUCCESS": "The channel is connected successfully",
|
"UPDATE_SUCCESS": "The channel is connected successfully",
|
||||||
@ -124,6 +125,18 @@
|
|||||||
"CANCEL": "إلغاء"
|
"CANCEL": "إلغاء"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CTA_MODAL": {
|
||||||
|
"TITLE": "Integrate with OpenAI",
|
||||||
|
"DESC": "Bring advanced AI features to your dashboard with OpenAI's GPT models. To begin, enter the API key from your OpenAI account.",
|
||||||
|
"KEY_PLACEHOLDER": "Enter your OpenAI API key",
|
||||||
|
"BUTTONS": {
|
||||||
|
"NEED_HELP": "تحتاج مساعدة؟",
|
||||||
|
"DISMISS": "Dismiss",
|
||||||
|
"FINISH": "Finish Setup"
|
||||||
|
},
|
||||||
|
"DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.",
|
||||||
|
"SUCCESS_MESSAGE": "OpenAI integration setup successfully"
|
||||||
|
},
|
||||||
"TITLE": "Improve With AI",
|
"TITLE": "Improve With AI",
|
||||||
"SUMMARY_TITLE": "Summary with AI",
|
"SUMMARY_TITLE": "Summary with AI",
|
||||||
"REPLY_TITLE": "Reply suggestion with AI",
|
"REPLY_TITLE": "Reply suggestion with AI",
|
||||||
|
|||||||
@ -39,7 +39,10 @@
|
|||||||
"NOTE": "إنشاء توقيع رسالة شخصية يتم إضافتها إلى جميع الرسائل التي ترسلها من المنصة. استخدم محرر المحتوى الغني لإنشاء توقيع شديد التخصيص.",
|
"NOTE": "إنشاء توقيع رسالة شخصية يتم إضافتها إلى جميع الرسائل التي ترسلها من المنصة. استخدم محرر المحتوى الغني لإنشاء توقيع شديد التخصيص.",
|
||||||
"BTN_TEXT": "حفظ توقيع الرسالة",
|
"BTN_TEXT": "حفظ توقيع الرسالة",
|
||||||
"API_ERROR": "تعذر إرسال الرسالة! حاول مرة أخرى",
|
"API_ERROR": "تعذر إرسال الرسالة! حاول مرة أخرى",
|
||||||
"API_SUCCESS": "تم حفظ التوقيع بنجاح"
|
"API_SUCCESS": "تم حفظ التوقيع بنجاح",
|
||||||
|
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
|
||||||
|
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
|
||||||
|
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB"
|
||||||
},
|
},
|
||||||
"MESSAGE_SIGNATURE": {
|
"MESSAGE_SIGNATURE": {
|
||||||
"LABEL": "توقيع الرسالة",
|
"LABEL": "توقيع الرسالة",
|
||||||
|
|||||||
@ -56,6 +56,14 @@
|
|||||||
"EDIT": "%{agentName} updated a macro (#%{id})",
|
"EDIT": "%{agentName} updated a macro (#%{id})",
|
||||||
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
||||||
},
|
},
|
||||||
|
"INBOX_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
|
||||||
|
},
|
||||||
|
"TEAM_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
|
||||||
|
},
|
||||||
"ACCOUNT": {
|
"ACCOUNT": {
|
||||||
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
"SUBMIT": "Внасяне",
|
"SUBMIT": "Внасяне",
|
||||||
"CANCEL": "Отмени"
|
"CANCEL": "Отмени"
|
||||||
},
|
},
|
||||||
"SUCCESS_MESSAGE": "Успешно запазване на контактите",
|
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
|
||||||
"ERROR_MESSAGE": "Възникна грешка, моля опитайте отново"
|
"ERROR_MESSAGE": "Възникна грешка, моля опитайте отново"
|
||||||
},
|
},
|
||||||
"EXPORT_CONTACTS": {
|
"EXPORT_CONTACTS": {
|
||||||
|
|||||||
@ -88,7 +88,8 @@
|
|||||||
"UPDATE": "Обновяване",
|
"UPDATE": "Обновяване",
|
||||||
"BUTTON_TEXT": "Connect channel",
|
"BUTTON_TEXT": "Connect channel",
|
||||||
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
||||||
"ATTENTION_REQUIRED": "Attention required"
|
"ATTENTION_REQUIRED": "Attention required",
|
||||||
|
"EXPIRED": "Your Slack integration has expired. To continue receiving messages on Slack, please delete the integration and connect your workspace again."
|
||||||
},
|
},
|
||||||
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
||||||
"UPDATE_SUCCESS": "The channel is connected successfully",
|
"UPDATE_SUCCESS": "The channel is connected successfully",
|
||||||
@ -124,6 +125,18 @@
|
|||||||
"CANCEL": "Отмени"
|
"CANCEL": "Отмени"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CTA_MODAL": {
|
||||||
|
"TITLE": "Integrate with OpenAI",
|
||||||
|
"DESC": "Bring advanced AI features to your dashboard with OpenAI's GPT models. To begin, enter the API key from your OpenAI account.",
|
||||||
|
"KEY_PLACEHOLDER": "Enter your OpenAI API key",
|
||||||
|
"BUTTONS": {
|
||||||
|
"NEED_HELP": "Need help?",
|
||||||
|
"DISMISS": "Dismiss",
|
||||||
|
"FINISH": "Finish Setup"
|
||||||
|
},
|
||||||
|
"DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.",
|
||||||
|
"SUCCESS_MESSAGE": "OpenAI integration setup successfully"
|
||||||
|
},
|
||||||
"TITLE": "Improve With AI",
|
"TITLE": "Improve With AI",
|
||||||
"SUMMARY_TITLE": "Summary with AI",
|
"SUMMARY_TITLE": "Summary with AI",
|
||||||
"REPLY_TITLE": "Reply suggestion with AI",
|
"REPLY_TITLE": "Reply suggestion with AI",
|
||||||
|
|||||||
@ -39,7 +39,10 @@
|
|||||||
"NOTE": "Create a personal message signature that would be added to all the messages you send from your email inbox. Use the rich content editor to create a highly personalised signature.",
|
"NOTE": "Create a personal message signature that would be added to all the messages you send from your email inbox. Use the rich content editor to create a highly personalised signature.",
|
||||||
"BTN_TEXT": "Save message signature",
|
"BTN_TEXT": "Save message signature",
|
||||||
"API_ERROR": "Couldn't save signature! Try again",
|
"API_ERROR": "Couldn't save signature! Try again",
|
||||||
"API_SUCCESS": "Signature saved successfully"
|
"API_SUCCESS": "Signature saved successfully",
|
||||||
|
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
|
||||||
|
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
|
||||||
|
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB"
|
||||||
},
|
},
|
||||||
"MESSAGE_SIGNATURE": {
|
"MESSAGE_SIGNATURE": {
|
||||||
"LABEL": "Message Signature",
|
"LABEL": "Message Signature",
|
||||||
|
|||||||
@ -56,6 +56,14 @@
|
|||||||
"EDIT": "%{agentName} updated a macro (#%{id})",
|
"EDIT": "%{agentName} updated a macro (#%{id})",
|
||||||
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
||||||
},
|
},
|
||||||
|
"INBOX_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
|
||||||
|
},
|
||||||
|
"TEAM_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
|
||||||
|
},
|
||||||
"ACCOUNT": {
|
"ACCOUNT": {
|
||||||
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
"SUBMIT": "Importa",
|
"SUBMIT": "Importa",
|
||||||
"CANCEL": "Cancel·la"
|
"CANCEL": "Cancel·la"
|
||||||
},
|
},
|
||||||
"SUCCESS_MESSAGE": "Contactes desat correctament",
|
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
|
||||||
"ERROR_MESSAGE": "S'ha produït un error; tornau-ho a provar"
|
"ERROR_MESSAGE": "S'ha produït un error; tornau-ho a provar"
|
||||||
},
|
},
|
||||||
"EXPORT_CONTACTS": {
|
"EXPORT_CONTACTS": {
|
||||||
|
|||||||
@ -88,7 +88,8 @@
|
|||||||
"UPDATE": "Actualitza",
|
"UPDATE": "Actualitza",
|
||||||
"BUTTON_TEXT": "Connect channel",
|
"BUTTON_TEXT": "Connect channel",
|
||||||
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
||||||
"ATTENTION_REQUIRED": "Attention required"
|
"ATTENTION_REQUIRED": "Attention required",
|
||||||
|
"EXPIRED": "Your Slack integration has expired. To continue receiving messages on Slack, please delete the integration and connect your workspace again."
|
||||||
},
|
},
|
||||||
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
||||||
"UPDATE_SUCCESS": "The channel is connected successfully",
|
"UPDATE_SUCCESS": "The channel is connected successfully",
|
||||||
@ -124,6 +125,18 @@
|
|||||||
"CANCEL": "Cancel·la"
|
"CANCEL": "Cancel·la"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CTA_MODAL": {
|
||||||
|
"TITLE": "Integrate with OpenAI",
|
||||||
|
"DESC": "Bring advanced AI features to your dashboard with OpenAI's GPT models. To begin, enter the API key from your OpenAI account.",
|
||||||
|
"KEY_PLACEHOLDER": "Enter your OpenAI API key",
|
||||||
|
"BUTTONS": {
|
||||||
|
"NEED_HELP": "Need help?",
|
||||||
|
"DISMISS": "Dismiss",
|
||||||
|
"FINISH": "Finish Setup"
|
||||||
|
},
|
||||||
|
"DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.",
|
||||||
|
"SUCCESS_MESSAGE": "OpenAI integration setup successfully"
|
||||||
|
},
|
||||||
"TITLE": "Improve With AI",
|
"TITLE": "Improve With AI",
|
||||||
"SUMMARY_TITLE": "Summary with AI",
|
"SUMMARY_TITLE": "Summary with AI",
|
||||||
"REPLY_TITLE": "Reply suggestion with AI",
|
"REPLY_TITLE": "Reply suggestion with AI",
|
||||||
|
|||||||
@ -39,7 +39,10 @@
|
|||||||
"NOTE": "Create a personal message signature that would be added to all the messages you send from your email inbox. Use the rich content editor to create a highly personalised signature.",
|
"NOTE": "Create a personal message signature that would be added to all the messages you send from your email inbox. Use the rich content editor to create a highly personalised signature.",
|
||||||
"BTN_TEXT": "Save message signature",
|
"BTN_TEXT": "Save message signature",
|
||||||
"API_ERROR": "Couldn't save signature! Try again",
|
"API_ERROR": "Couldn't save signature! Try again",
|
||||||
"API_SUCCESS": "Signature saved successfully"
|
"API_SUCCESS": "Signature saved successfully",
|
||||||
|
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
|
||||||
|
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
|
||||||
|
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB"
|
||||||
},
|
},
|
||||||
"MESSAGE_SIGNATURE": {
|
"MESSAGE_SIGNATURE": {
|
||||||
"LABEL": "Message Signature",
|
"LABEL": "Message Signature",
|
||||||
|
|||||||
@ -56,6 +56,14 @@
|
|||||||
"EDIT": "%{agentName} updated a macro (#%{id})",
|
"EDIT": "%{agentName} updated a macro (#%{id})",
|
||||||
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
||||||
},
|
},
|
||||||
|
"INBOX_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
|
||||||
|
},
|
||||||
|
"TEAM_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
|
||||||
|
},
|
||||||
"ACCOUNT": {
|
"ACCOUNT": {
|
||||||
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
"SUBMIT": "Import",
|
"SUBMIT": "Import",
|
||||||
"CANCEL": "Zrušit"
|
"CANCEL": "Zrušit"
|
||||||
},
|
},
|
||||||
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
|
||||||
"ERROR_MESSAGE": "Došlo k chybě, zkuste to prosím znovu"
|
"ERROR_MESSAGE": "Došlo k chybě, zkuste to prosím znovu"
|
||||||
},
|
},
|
||||||
"EXPORT_CONTACTS": {
|
"EXPORT_CONTACTS": {
|
||||||
|
|||||||
@ -88,7 +88,8 @@
|
|||||||
"UPDATE": "Aktualizovat",
|
"UPDATE": "Aktualizovat",
|
||||||
"BUTTON_TEXT": "Connect channel",
|
"BUTTON_TEXT": "Connect channel",
|
||||||
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
||||||
"ATTENTION_REQUIRED": "Attention required"
|
"ATTENTION_REQUIRED": "Attention required",
|
||||||
|
"EXPIRED": "Your Slack integration has expired. To continue receiving messages on Slack, please delete the integration and connect your workspace again."
|
||||||
},
|
},
|
||||||
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
||||||
"UPDATE_SUCCESS": "The channel is connected successfully",
|
"UPDATE_SUCCESS": "The channel is connected successfully",
|
||||||
@ -124,6 +125,18 @@
|
|||||||
"CANCEL": "Zrušit"
|
"CANCEL": "Zrušit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CTA_MODAL": {
|
||||||
|
"TITLE": "Integrate with OpenAI",
|
||||||
|
"DESC": "Bring advanced AI features to your dashboard with OpenAI's GPT models. To begin, enter the API key from your OpenAI account.",
|
||||||
|
"KEY_PLACEHOLDER": "Enter your OpenAI API key",
|
||||||
|
"BUTTONS": {
|
||||||
|
"NEED_HELP": "Need help?",
|
||||||
|
"DISMISS": "Dismiss",
|
||||||
|
"FINISH": "Finish Setup"
|
||||||
|
},
|
||||||
|
"DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.",
|
||||||
|
"SUCCESS_MESSAGE": "OpenAI integration setup successfully"
|
||||||
|
},
|
||||||
"TITLE": "Improve With AI",
|
"TITLE": "Improve With AI",
|
||||||
"SUMMARY_TITLE": "Summary with AI",
|
"SUMMARY_TITLE": "Summary with AI",
|
||||||
"REPLY_TITLE": "Reply suggestion with AI",
|
"REPLY_TITLE": "Reply suggestion with AI",
|
||||||
|
|||||||
@ -39,7 +39,10 @@
|
|||||||
"NOTE": "Create a personal message signature that would be added to all the messages you send from your email inbox. Use the rich content editor to create a highly personalised signature.",
|
"NOTE": "Create a personal message signature that would be added to all the messages you send from your email inbox. Use the rich content editor to create a highly personalised signature.",
|
||||||
"BTN_TEXT": "Save message signature",
|
"BTN_TEXT": "Save message signature",
|
||||||
"API_ERROR": "Couldn't save signature! Try again",
|
"API_ERROR": "Couldn't save signature! Try again",
|
||||||
"API_SUCCESS": "Signature saved successfully"
|
"API_SUCCESS": "Signature saved successfully",
|
||||||
|
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
|
||||||
|
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
|
||||||
|
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB"
|
||||||
},
|
},
|
||||||
"MESSAGE_SIGNATURE": {
|
"MESSAGE_SIGNATURE": {
|
||||||
"LABEL": "Message Signature",
|
"LABEL": "Message Signature",
|
||||||
|
|||||||
@ -56,6 +56,14 @@
|
|||||||
"EDIT": "%{agentName} updated a macro (#%{id})",
|
"EDIT": "%{agentName} updated a macro (#%{id})",
|
||||||
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
||||||
},
|
},
|
||||||
|
"INBOX_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
|
||||||
|
},
|
||||||
|
"TEAM_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
|
||||||
|
},
|
||||||
"ACCOUNT": {
|
"ACCOUNT": {
|
||||||
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
"SUBMIT": "Importer",
|
"SUBMIT": "Importer",
|
||||||
"CANCEL": "Annuller"
|
"CANCEL": "Annuller"
|
||||||
},
|
},
|
||||||
"SUCCESS_MESSAGE": "Kontakter gemt",
|
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
|
||||||
"ERROR_MESSAGE": "Der opstod en fejl. Prøv venligst igen"
|
"ERROR_MESSAGE": "Der opstod en fejl. Prøv venligst igen"
|
||||||
},
|
},
|
||||||
"EXPORT_CONTACTS": {
|
"EXPORT_CONTACTS": {
|
||||||
|
|||||||
@ -88,7 +88,8 @@
|
|||||||
"UPDATE": "Opdater",
|
"UPDATE": "Opdater",
|
||||||
"BUTTON_TEXT": "Connect channel",
|
"BUTTON_TEXT": "Connect channel",
|
||||||
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
||||||
"ATTENTION_REQUIRED": "Attention required"
|
"ATTENTION_REQUIRED": "Attention required",
|
||||||
|
"EXPIRED": "Your Slack integration has expired. To continue receiving messages on Slack, please delete the integration and connect your workspace again."
|
||||||
},
|
},
|
||||||
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
||||||
"UPDATE_SUCCESS": "The channel is connected successfully",
|
"UPDATE_SUCCESS": "The channel is connected successfully",
|
||||||
@ -124,6 +125,18 @@
|
|||||||
"CANCEL": "Annuller"
|
"CANCEL": "Annuller"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CTA_MODAL": {
|
||||||
|
"TITLE": "Integrate with OpenAI",
|
||||||
|
"DESC": "Bring advanced AI features to your dashboard with OpenAI's GPT models. To begin, enter the API key from your OpenAI account.",
|
||||||
|
"KEY_PLACEHOLDER": "Enter your OpenAI API key",
|
||||||
|
"BUTTONS": {
|
||||||
|
"NEED_HELP": "Brug for hjælp?",
|
||||||
|
"DISMISS": "Dismiss",
|
||||||
|
"FINISH": "Finish Setup"
|
||||||
|
},
|
||||||
|
"DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.",
|
||||||
|
"SUCCESS_MESSAGE": "OpenAI integration setup successfully"
|
||||||
|
},
|
||||||
"TITLE": "Improve With AI",
|
"TITLE": "Improve With AI",
|
||||||
"SUMMARY_TITLE": "Summary with AI",
|
"SUMMARY_TITLE": "Summary with AI",
|
||||||
"REPLY_TITLE": "Reply suggestion with AI",
|
"REPLY_TITLE": "Reply suggestion with AI",
|
||||||
|
|||||||
@ -39,7 +39,10 @@
|
|||||||
"NOTE": "Opret en personlig besked signatur, der vil blive føjet til alle de meddelelser, du sender fra din e-mail indbakke. Brug den rige content editor til at oprette en meget personlig signatur.",
|
"NOTE": "Opret en personlig besked signatur, der vil blive føjet til alle de meddelelser, du sender fra din e-mail indbakke. Brug den rige content editor til at oprette en meget personlig signatur.",
|
||||||
"BTN_TEXT": "Gem beskedsignatur",
|
"BTN_TEXT": "Gem beskedsignatur",
|
||||||
"API_ERROR": "Kunne ikke gemme signatur! Prøv igen",
|
"API_ERROR": "Kunne ikke gemme signatur! Prøv igen",
|
||||||
"API_SUCCESS": "Signatur gemt"
|
"API_SUCCESS": "Signatur gemt",
|
||||||
|
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
|
||||||
|
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
|
||||||
|
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB"
|
||||||
},
|
},
|
||||||
"MESSAGE_SIGNATURE": {
|
"MESSAGE_SIGNATURE": {
|
||||||
"LABEL": "Besked Signatur",
|
"LABEL": "Besked Signatur",
|
||||||
|
|||||||
@ -56,6 +56,14 @@
|
|||||||
"EDIT": "%{agentName} updated a macro (#%{id})",
|
"EDIT": "%{agentName} updated a macro (#%{id})",
|
||||||
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
||||||
},
|
},
|
||||||
|
"INBOX_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
|
||||||
|
},
|
||||||
|
"TEAM_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
|
||||||
|
},
|
||||||
"ACCOUNT": {
|
"ACCOUNT": {
|
||||||
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
"SUBMIT": "Importieren",
|
"SUBMIT": "Importieren",
|
||||||
"CANCEL": "Abbrechen"
|
"CANCEL": "Abbrechen"
|
||||||
},
|
},
|
||||||
"SUCCESS_MESSAGE": "Kontakte erfolgreich gespeichert",
|
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
|
||||||
"ERROR_MESSAGE": "Es ist ein Fehler aufgetreten, bitte versuchen Sie es erneut"
|
"ERROR_MESSAGE": "Es ist ein Fehler aufgetreten, bitte versuchen Sie es erneut"
|
||||||
},
|
},
|
||||||
"EXPORT_CONTACTS": {
|
"EXPORT_CONTACTS": {
|
||||||
|
|||||||
@ -88,7 +88,8 @@
|
|||||||
"UPDATE": "Aktualisieren",
|
"UPDATE": "Aktualisieren",
|
||||||
"BUTTON_TEXT": "Connect channel",
|
"BUTTON_TEXT": "Connect channel",
|
||||||
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
||||||
"ATTENTION_REQUIRED": "Attention required"
|
"ATTENTION_REQUIRED": "Attention required",
|
||||||
|
"EXPIRED": "Your Slack integration has expired. To continue receiving messages on Slack, please delete the integration and connect your workspace again."
|
||||||
},
|
},
|
||||||
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
||||||
"UPDATE_SUCCESS": "The channel is connected successfully",
|
"UPDATE_SUCCESS": "The channel is connected successfully",
|
||||||
@ -124,6 +125,18 @@
|
|||||||
"CANCEL": "Stornieren"
|
"CANCEL": "Stornieren"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CTA_MODAL": {
|
||||||
|
"TITLE": "Integrate with OpenAI",
|
||||||
|
"DESC": "Bring advanced AI features to your dashboard with OpenAI's GPT models. To begin, enter the API key from your OpenAI account.",
|
||||||
|
"KEY_PLACEHOLDER": "Enter your OpenAI API key",
|
||||||
|
"BUTTONS": {
|
||||||
|
"NEED_HELP": "Brauchen Sie Hilfe?",
|
||||||
|
"DISMISS": "Verwerfen",
|
||||||
|
"FINISH": "Finish Setup"
|
||||||
|
},
|
||||||
|
"DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.",
|
||||||
|
"SUCCESS_MESSAGE": "OpenAI integration setup successfully"
|
||||||
|
},
|
||||||
"TITLE": "Mit KI verbessern",
|
"TITLE": "Mit KI verbessern",
|
||||||
"SUMMARY_TITLE": "Zusammenfassung mit KI",
|
"SUMMARY_TITLE": "Zusammenfassung mit KI",
|
||||||
"REPLY_TITLE": "Vorschlag mit KI beantworten",
|
"REPLY_TITLE": "Vorschlag mit KI beantworten",
|
||||||
|
|||||||
@ -39,7 +39,10 @@
|
|||||||
"NOTE": "Erstellen Sie eine persönliche Nachrichtensignatur, die allen Nachrichten hinzugefügt wird, die Sie aus Ihrem E-Mail-Posteingang senden. Verwenden Sie den Rich-Content-Editor, um eine stark personalisierte Signatur zu erstellen.",
|
"NOTE": "Erstellen Sie eine persönliche Nachrichtensignatur, die allen Nachrichten hinzugefügt wird, die Sie aus Ihrem E-Mail-Posteingang senden. Verwenden Sie den Rich-Content-Editor, um eine stark personalisierte Signatur zu erstellen.",
|
||||||
"BTN_TEXT": "Nachrichten-Signatur speichern",
|
"BTN_TEXT": "Nachrichten-Signatur speichern",
|
||||||
"API_ERROR": "Signatur konnte nicht gespeichert werden! Versuch es noch einmal",
|
"API_ERROR": "Signatur konnte nicht gespeichert werden! Versuch es noch einmal",
|
||||||
"API_SUCCESS": "Signatur erfolgreich gespeichert"
|
"API_SUCCESS": "Signatur erfolgreich gespeichert",
|
||||||
|
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
|
||||||
|
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
|
||||||
|
"IMAGE_UPLOAD_SIZE_ERROR": "Bildgröße sollte kleiner als {size}MB sein"
|
||||||
},
|
},
|
||||||
"MESSAGE_SIGNATURE": {
|
"MESSAGE_SIGNATURE": {
|
||||||
"LABEL": "Nachrichten-Signatur",
|
"LABEL": "Nachrichten-Signatur",
|
||||||
|
|||||||
@ -56,6 +56,14 @@
|
|||||||
"EDIT": "%{agentName} updated a macro (#%{id})",
|
"EDIT": "%{agentName} updated a macro (#%{id})",
|
||||||
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
"DELETE": "%{agentName} deleted a macro (#%{id})"
|
||||||
},
|
},
|
||||||
|
"INBOX_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the inbox(#%{inbox_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the inbox(#%{inbox_id})"
|
||||||
|
},
|
||||||
|
"TEAM_MEMBER": {
|
||||||
|
"ADD": "%{agentName} added %{user} to the team(#%{team_id})",
|
||||||
|
"REMOVE": "%{agentName} removed %{user} from the team(#%{team_id})"
|
||||||
|
},
|
||||||
"ACCOUNT": {
|
"ACCOUNT": {
|
||||||
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
"SUBMIT": "Εισαγωγή",
|
"SUBMIT": "Εισαγωγή",
|
||||||
"CANCEL": "Άκυρο"
|
"CANCEL": "Άκυρο"
|
||||||
},
|
},
|
||||||
"SUCCESS_MESSAGE": "Οι επαφές αποθηκεύτηκαν με επιτυχία",
|
"SUCCESS_MESSAGE": "Θα ειδοποιηθείτε μέσω email όταν ολοκληρωθεί η εισαγωγή.",
|
||||||
"ERROR_MESSAGE": "Υπήρξε ένα σφάλμα, παρακαλώ προσπαθήστε ξανά"
|
"ERROR_MESSAGE": "Υπήρξε ένα σφάλμα, παρακαλώ προσπαθήστε ξανά"
|
||||||
},
|
},
|
||||||
"EXPORT_CONTACTS": {
|
"EXPORT_CONTACTS": {
|
||||||
|
|||||||
@ -88,7 +88,8 @@
|
|||||||
"UPDATE": "Ενημέρωση",
|
"UPDATE": "Ενημέρωση",
|
||||||
"BUTTON_TEXT": "Connect channel",
|
"BUTTON_TEXT": "Connect channel",
|
||||||
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
||||||
"ATTENTION_REQUIRED": "Attention required"
|
"ATTENTION_REQUIRED": "Attention required",
|
||||||
|
"EXPIRED": "Your Slack integration has expired. To continue receiving messages on Slack, please delete the integration and connect your workspace again."
|
||||||
},
|
},
|
||||||
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
||||||
"UPDATE_SUCCESS": "The channel is connected successfully",
|
"UPDATE_SUCCESS": "The channel is connected successfully",
|
||||||
@ -124,6 +125,18 @@
|
|||||||
"CANCEL": "Άκυρο"
|
"CANCEL": "Άκυρο"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CTA_MODAL": {
|
||||||
|
"TITLE": "Integrate with OpenAI",
|
||||||
|
"DESC": "Bring advanced AI features to your dashboard with OpenAI's GPT models. To begin, enter the API key from your OpenAI account.",
|
||||||
|
"KEY_PLACEHOLDER": "Enter your OpenAI API key",
|
||||||
|
"BUTTONS": {
|
||||||
|
"NEED_HELP": "Χρειάζεστε βοήθεια;",
|
||||||
|
"DISMISS": "Dismiss",
|
||||||
|
"FINISH": "Finish Setup"
|
||||||
|
},
|
||||||
|
"DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.",
|
||||||
|
"SUCCESS_MESSAGE": "OpenAI integration setup successfully"
|
||||||
|
},
|
||||||
"TITLE": "Improve With AI",
|
"TITLE": "Improve With AI",
|
||||||
"SUMMARY_TITLE": "Summary with AI",
|
"SUMMARY_TITLE": "Summary with AI",
|
||||||
"REPLY_TITLE": "Reply suggestion with AI",
|
"REPLY_TITLE": "Reply suggestion with AI",
|
||||||
|
|||||||
@ -39,7 +39,10 @@
|
|||||||
"NOTE": "Δημιουργήστε μια προσωπική υπογραφή, η οποία θα προστεθεί σε όλα τα μηνύματα που στέλνετε από τα εισερχόμενα email σας. Χρησιμοποιήστε τον επεξεργαστή πλούσιου περιεχομένου για να δημιουργήσετε μια εξατομικευμένη υπογραφή.",
|
"NOTE": "Δημιουργήστε μια προσωπική υπογραφή, η οποία θα προστεθεί σε όλα τα μηνύματα που στέλνετε από τα εισερχόμενα email σας. Χρησιμοποιήστε τον επεξεργαστή πλούσιου περιεχομένου για να δημιουργήσετε μια εξατομικευμένη υπογραφή.",
|
||||||
"BTN_TEXT": "Αποθήκευση υπογραφής μηνύματος",
|
"BTN_TEXT": "Αποθήκευση υπογραφής μηνύματος",
|
||||||
"API_ERROR": "Δεν ήταν δυνατή η αποθήκευση της υπογραφής! Δοκιμάστε ξανά",
|
"API_ERROR": "Δεν ήταν δυνατή η αποθήκευση της υπογραφής! Δοκιμάστε ξανά",
|
||||||
"API_SUCCESS": "Η υπογραφή αποθηκεύτηκε με επιτυχία"
|
"API_SUCCESS": "Η υπογραφή αποθηκεύτηκε με επιτυχία",
|
||||||
|
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
|
||||||
|
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
|
||||||
|
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB"
|
||||||
},
|
},
|
||||||
"MESSAGE_SIGNATURE": {
|
"MESSAGE_SIGNATURE": {
|
||||||
"LABEL": "Υπογραφή μηνύματος",
|
"LABEL": "Υπογραφή μηνύματος",
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
"SUBMIT": "Import",
|
"SUBMIT": "Import",
|
||||||
"CANCEL": "Cancel"
|
"CANCEL": "Cancel"
|
||||||
},
|
},
|
||||||
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
|
||||||
"ERROR_MESSAGE": "There was an error, please try again"
|
"ERROR_MESSAGE": "There was an error, please try again"
|
||||||
},
|
},
|
||||||
"EXPORT_CONTACTS": {
|
"EXPORT_CONTACTS": {
|
||||||
|
|||||||
@ -85,7 +85,8 @@
|
|||||||
"UPDATE": "Update",
|
"UPDATE": "Update",
|
||||||
"BUTTON_TEXT": "Connect channel",
|
"BUTTON_TEXT": "Connect channel",
|
||||||
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
||||||
"ATTENTION_REQUIRED": "Attention required"
|
"ATTENTION_REQUIRED": "Attention required",
|
||||||
|
"EXPIRED": "Your Slack integration has expired. To continue receiving messages on Slack, please delete the integration and connect your workspace again."
|
||||||
},
|
},
|
||||||
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
||||||
"UPDATE_SUCCESS": "The channel is connected successfully",
|
"UPDATE_SUCCESS": "The channel is connected successfully",
|
||||||
|
|||||||
@ -39,7 +39,10 @@
|
|||||||
"NOTE": "Create a personal message signature that would be added to all the messages you send from your email inbox. Use the rich content editor to create a highly personalised signature.",
|
"NOTE": "Create a personal message signature that would be added to all the messages you send from your email inbox. Use the rich content editor to create a highly personalised signature.",
|
||||||
"BTN_TEXT": "Save message signature",
|
"BTN_TEXT": "Save message signature",
|
||||||
"API_ERROR": "Couldn't save signature! Try again",
|
"API_ERROR": "Couldn't save signature! Try again",
|
||||||
"API_SUCCESS": "Signature saved successfully"
|
"API_SUCCESS": "Signature saved successfully",
|
||||||
|
"IMAGE_UPLOAD_ERROR": "Couldn't upload image! Try again",
|
||||||
|
"IMAGE_UPLOAD_SUCCESS": "Image added successfully. Please click on save to save the signature",
|
||||||
|
"IMAGE_UPLOAD_SIZE_ERROR": "Image size should be less than {size}MB"
|
||||||
},
|
},
|
||||||
"MESSAGE_SIGNATURE": {
|
"MESSAGE_SIGNATURE": {
|
||||||
"LABEL": "Message Signature",
|
"LABEL": "Message Signature",
|
||||||
|
|||||||
@ -56,8 +56,16 @@
|
|||||||
"EDIT": "%{agentName} actualizó una macro (#%{id})",
|
"EDIT": "%{agentName} actualizó una macro (#%{id})",
|
||||||
"DELETE": "%{agentName} eliminó una macro (#%{id})"
|
"DELETE": "%{agentName} eliminó una macro (#%{id})"
|
||||||
},
|
},
|
||||||
|
"INBOX_MEMBER": {
|
||||||
|
"ADD": "%{agentName} agregó %{user} a la bandeja de entrada (#%{inbox_id})",
|
||||||
|
"REMOVE": "%{agentName} eliminó %{user} de la bandeja de entrada(#%{inbox_id})"
|
||||||
|
},
|
||||||
|
"TEAM_MEMBER": {
|
||||||
|
"ADD": "%{agentName} agregó %{user} al equipo (#%{team_id})",
|
||||||
|
"REMOVE": "%{agentName} eliminó %{user} del equipo (#%{team_id})"
|
||||||
|
},
|
||||||
"ACCOUNT": {
|
"ACCOUNT": {
|
||||||
"EDIT": "%{agentName} updated the account configuration (#%{id})"
|
"EDIT": "%{agentName} Actualizó la configuración de la cuenta (#%{id})"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,7 +71,7 @@
|
|||||||
"SUBMIT": "Importar",
|
"SUBMIT": "Importar",
|
||||||
"CANCEL": "Cancelar"
|
"CANCEL": "Cancelar"
|
||||||
},
|
},
|
||||||
"SUCCESS_MESSAGE": "Contacto guardado correctamente",
|
"SUCCESS_MESSAGE": "Se le notificará por correo electrónico cuando se complete la importación.",
|
||||||
"ERROR_MESSAGE": "Hubo un error, por favor inténtelo de nuevo"
|
"ERROR_MESSAGE": "Hubo un error, por favor inténtelo de nuevo"
|
||||||
},
|
},
|
||||||
"EXPORT_CONTACTS": {
|
"EXPORT_CONTACTS": {
|
||||||
|
|||||||
@ -13,8 +13,8 @@
|
|||||||
"NO_INBOX_AGENT": "¡Uh Oh! Parece que no eres parte de ninguna bandeja de entrada. Por favor, contacta con tu administrador",
|
"NO_INBOX_AGENT": "¡Uh Oh! Parece que no eres parte de ninguna bandeja de entrada. Por favor, contacta con tu administrador",
|
||||||
"SEARCH_MESSAGES": "Buscar mensajes en conversaciones",
|
"SEARCH_MESSAGES": "Buscar mensajes en conversaciones",
|
||||||
"EMPTY_STATE": {
|
"EMPTY_STATE": {
|
||||||
"CMD_BAR": "to open command menu",
|
"CMD_BAR": "para abrir el menú de comandos",
|
||||||
"KEYBOARD_SHORTCUTS": "to view keyboard shortcuts"
|
"KEYBOARD_SHORTCUTS": "para ver los atajos del teclado"
|
||||||
},
|
},
|
||||||
"SEARCH": {
|
"SEARCH": {
|
||||||
"TITLE": "Buscar mensajes",
|
"TITLE": "Buscar mensajes",
|
||||||
|
|||||||
@ -111,8 +111,8 @@
|
|||||||
"ADD_LABEL": "Añadir etiqueta a la conversación",
|
"ADD_LABEL": "Añadir etiqueta a la conversación",
|
||||||
"REMOVE_LABEL": "Eliminar etiqueta de la conversación",
|
"REMOVE_LABEL": "Eliminar etiqueta de la conversación",
|
||||||
"SETTINGS": "Ajustes",
|
"SETTINGS": "Ajustes",
|
||||||
"AI_ASSIST": "AI Assist",
|
"AI_ASSIST": "Asistencia AI",
|
||||||
"APPEARANCE": "Appearance"
|
"APPEARANCE": "Apariencia"
|
||||||
},
|
},
|
||||||
"COMMANDS": {
|
"COMMANDS": {
|
||||||
"GO_TO_CONVERSATION_DASHBOARD": "Ir al panel de conversaciones",
|
"GO_TO_CONVERSATION_DASHBOARD": "Ir al panel de conversaciones",
|
||||||
@ -134,7 +134,7 @@
|
|||||||
"GO_TO_NOTIFICATIONS": "Ir a Notificaciones",
|
"GO_TO_NOTIFICATIONS": "Ir a Notificaciones",
|
||||||
"ADD_LABELS_TO_CONVERSATION": "Añadir etiqueta a la conversación",
|
"ADD_LABELS_TO_CONVERSATION": "Añadir etiqueta a la conversación",
|
||||||
"ASSIGN_AN_AGENT": "Asignar un agente",
|
"ASSIGN_AN_AGENT": "Asignar un agente",
|
||||||
"AI_ASSIST": "AI Assist",
|
"AI_ASSIST": "Asistencia AI",
|
||||||
"ASSIGN_PRIORITY": "Asignar prioridad",
|
"ASSIGN_PRIORITY": "Asignar prioridad",
|
||||||
"ASSIGN_A_TEAM": "Asignar un equipo",
|
"ASSIGN_A_TEAM": "Asignar un equipo",
|
||||||
"MUTE_CONVERSATION": "Silenciar conversación",
|
"MUTE_CONVERSATION": "Silenciar conversación",
|
||||||
@ -150,9 +150,9 @@
|
|||||||
"UNTIL_NEXT_MONTH": "Hasta el mes próximo",
|
"UNTIL_NEXT_MONTH": "Hasta el mes próximo",
|
||||||
"AN_HOUR_FROM_NOW": "Hasta una hora a partir de ahora",
|
"AN_HOUR_FROM_NOW": "Hasta una hora a partir de ahora",
|
||||||
"CUSTOM": "Personalizar...",
|
"CUSTOM": "Personalizar...",
|
||||||
"CHANGE_APPEARANCE": "Change Appearance",
|
"CHANGE_APPEARANCE": "Cambiar apariencia",
|
||||||
"LIGHT_MODE": "Light",
|
"LIGHT_MODE": "Claro",
|
||||||
"DARK_MODE": "Dark",
|
"DARK_MODE": "Oscuro",
|
||||||
"SYSTEM_MODE": "Sistema"
|
"SYSTEM_MODE": "Sistema"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -160,7 +160,7 @@
|
|||||||
"LOADING_MESSAGE": "Cargando aplicación del tablero..."
|
"LOADING_MESSAGE": "Cargando aplicación del tablero..."
|
||||||
},
|
},
|
||||||
"COMMON": {
|
"COMMON": {
|
||||||
"OR": "Or",
|
"OR": "O",
|
||||||
"CLICK_HERE": "haz clic aquí"
|
"CLICK_HERE": "haz clic aquí"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,14 +112,14 @@
|
|||||||
"ERROR": "Este campo es obligatorio"
|
"ERROR": "Este campo es obligatorio"
|
||||||
},
|
},
|
||||||
"API_KEY": {
|
"API_KEY": {
|
||||||
"USE_API_KEY": "Use API Key Authentication",
|
"USE_API_KEY": "Usar autenticación de Clave API",
|
||||||
"LABEL": "API Key SID",
|
"LABEL": "API Key SID",
|
||||||
"PLACEHOLDER": "Please enter your API Key SID",
|
"PLACEHOLDER": "Por favor ingrese su API Key SID",
|
||||||
"ERROR": "Este campo es obligatorio"
|
"ERROR": "Este campo es obligatorio"
|
||||||
},
|
},
|
||||||
"API_KEY_SECRET": {
|
"API_KEY_SECRET": {
|
||||||
"LABEL": "API Key Secret",
|
"LABEL": "Clave API secreta",
|
||||||
"PLACEHOLDER": "Please enter your API Key Secret",
|
"PLACEHOLDER": "Por favor ingrese su Clave API secreta",
|
||||||
"ERROR": "Este campo es obligatorio"
|
"ERROR": "Este campo es obligatorio"
|
||||||
},
|
},
|
||||||
"MESSAGING_SERVICE_SID": {
|
"MESSAGING_SERVICE_SID": {
|
||||||
@ -403,21 +403,21 @@
|
|||||||
"DISABLED": "Deshabilitado"
|
"DISABLED": "Deshabilitado"
|
||||||
},
|
},
|
||||||
"SENDER_NAME_SECTION": {
|
"SENDER_NAME_SECTION": {
|
||||||
"TITLE": "Sender name",
|
"TITLE": "Nombre del remitente",
|
||||||
"SUB_TEXT": "Select the name shown to the your customer when they receive emails from your agents.",
|
"SUB_TEXT": "Seleccione el nombre que se muestra al cliente cuando reciba correos electrónicos de sus agentes.",
|
||||||
"FOR_EG": "For eg:",
|
"FOR_EG": "Por ejemplo:",
|
||||||
"FRIENDLY": {
|
"FRIENDLY": {
|
||||||
"TITLE": "Amigable",
|
"TITLE": "Amigable",
|
||||||
"FROM": "De",
|
"FROM": "De",
|
||||||
"SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly."
|
"SUBTITLE": "Añada el nombre del agente que envió la respuesta en el nombre del remitente para que sea amistoso."
|
||||||
},
|
},
|
||||||
"PROFESSIONAL": {
|
"PROFESSIONAL": {
|
||||||
"TITLE": "Profesional",
|
"TITLE": "Profesional",
|
||||||
"SUBTITLE": "Use only the configured business name as the sender name in the email header."
|
"SUBTITLE": "Utilice sólo el nombre del negocio configurado como nombre del remitente en el encabezado del correo electrónico."
|
||||||
},
|
},
|
||||||
"BUSINESS_NAME": {
|
"BUSINESS_NAME": {
|
||||||
"BUTTON_TEXT": "+ Configure your business name",
|
"BUTTON_TEXT": "+ Configura el nombre de tu negocio",
|
||||||
"PLACEHOLDER": "Enter your business name",
|
"PLACEHOLDER": "Introduce el nombre de tu negocio",
|
||||||
"SAVE_BUTTON_TEXT": "Guardar"
|
"SAVE_BUTTON_TEXT": "Guardar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -484,9 +484,9 @@
|
|||||||
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Activar o desactivar la caja de recolección de correo electrónico",
|
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "Activar o desactivar la caja de recolección de correo electrónico",
|
||||||
"AUTO_ASSIGNMENT": "Activar asignación automática",
|
"AUTO_ASSIGNMENT": "Activar asignación automática",
|
||||||
"ENABLE_CSAT": "Habilitar Encuesta de Satisfacción",
|
"ENABLE_CSAT": "Habilitar Encuesta de Satisfacción",
|
||||||
"SENDER_NAME_SECTION": "Enable Agent Name in Email",
|
"SENDER_NAME_SECTION": "Habilitar nombre del agente en el correo electrónico",
|
||||||
"ENABLE_CSAT_SUB_TEXT": "Habilitar/deshabilitar encuesta CSAT(satisfacción del cliente) después de resolver una conversación",
|
"ENABLE_CSAT_SUB_TEXT": "Habilitar/deshabilitar encuesta CSAT(satisfacción del cliente) después de resolver una conversación",
|
||||||
"SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name",
|
"SENDER_NAME_SECTION_TEXT": "Habilitar/Deshabilitar mostrando el nombre del agente en el correo electrónico, si está deshabilitado, mostrará el nombre del negocio",
|
||||||
"ENABLE_CONTINUITY_VIA_EMAIL": "Habilitar continuidad de conversación por correo electrónico",
|
"ENABLE_CONTINUITY_VIA_EMAIL": "Habilitar continuidad de conversación por correo electrónico",
|
||||||
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Las conversaciones continuarán por correo electrónico si la dirección de correo electrónico de contacto está disponible.",
|
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Las conversaciones continuarán por correo electrónico si la dirección de correo electrónico de contacto está disponible.",
|
||||||
"LOCK_TO_SINGLE_CONVERSATION": "Bloquear a una sola conversación",
|
"LOCK_TO_SINGLE_CONVERSATION": "Bloquear a una sola conversación",
|
||||||
|
|||||||
@ -75,24 +75,25 @@
|
|||||||
"SLACK": {
|
"SLACK": {
|
||||||
"DELETE": "Eliminar",
|
"DELETE": "Eliminar",
|
||||||
"DELETE_CONFIRMATION": {
|
"DELETE_CONFIRMATION": {
|
||||||
"TITLE": "Delete the integration",
|
"TITLE": "Eliminar la integración",
|
||||||
"MESSAGE": "Are you sure you want to delete the integration? Doing so will result in the loss of access to conversations on your Slack workspace."
|
"MESSAGE": "¿Está seguro que desea eliminar la integración? Si lo hace, perderá el acceso a las conversaciones en su espacio de trabajo Slack."
|
||||||
},
|
},
|
||||||
"HELP_TEXT": {
|
"HELP_TEXT": {
|
||||||
"TITLE": "Utilizando integración de Slack",
|
"TITLE": "¿Cómo utilizar la Integración Slack?",
|
||||||
"BODY": "<br/><p>Chatwoot ahora sincronizará todas las conversaciones entrantes en el canal <b><i>de conversaciones del cliente</i></b> dentro de tu lugar de trabajo slack.</p><p>Respondiendo a un tema de conversación en <b><i>conversaciones de clientes</i></b> canal de slack creará una respuesta al cliente a través de chatwoot.</p><p>Inicie las respuestas con <b><i>nota:</i></b> para crear notas privadas en lugar de respuestas.</p><p>Si el respondente de slack tiene un perfil de agente en el chatwoot bajo el mismo correo electrónico, las respuestas se asociarán en consecuencia.</p><p>Cuando el replicador no tiene un perfil de agente asociado, las respuestas se harán con el perfil del bot.</p>",
|
"BODY": "<br/><p>Chatwoot ahora sincronizará todas las conversaciones entrantes en el canal <b><i>de conversaciones del cliente</i></b> dentro de tu lugar de trabajo slack.</p><p>Respondiendo a un tema de conversación en <b><i>conversaciones de clientes</i></b> canal de slack creará una respuesta al cliente a través de chatwoot.</p><p>Inicie las respuestas con <b><i>nota:</i></b> para crear notas privadas en lugar de respuestas.</p><p>Si el respondente de slack tiene un perfil de agente en el chatwoot bajo el mismo correo electrónico, las respuestas se asociarán en consecuencia.</p><p>Cuando el replicador no tiene un perfil de agente asociado, las respuestas se harán con el perfil del bot.</p>",
|
||||||
"SELECTED": "selected"
|
"SELECTED": "seleccionado"
|
||||||
},
|
},
|
||||||
"SELECT_CHANNEL": {
|
"SELECT_CHANNEL": {
|
||||||
"OPTION_LABEL": "Select a channel",
|
"OPTION_LABEL": "Seleccione un canal",
|
||||||
"UPDATE": "Actualizar",
|
"UPDATE": "Actualizar",
|
||||||
"BUTTON_TEXT": "Connect channel",
|
"BUTTON_TEXT": "Conectar canal",
|
||||||
"DESCRIPTION": "Your Slack workspace is now linked with Chatwoot. However, the integration is currently inactive. To activate the integration and connect a channel to Chatwoot, please click the button below.\n\n**Note:** If you are attempting to connect a private channel, add the Chatwoot app to the Slack channel before proceeding with this step.",
|
"DESCRIPTION": "Su espacio de trabajo de Slack ahora esta enlazado con Chatwoot. Sin embarbo, la integración esta actualmente inactiva. Para activar la integración y conectar un canal a Chatwoot, por favor haga clic en el siguiente boton.\n\n**Nota.** Si esta intentando conectar un canal privado, agregue la aplicación Chatwoot al canal de Slack antes de proceder con este paso.",
|
||||||
"ATTENTION_REQUIRED": "Attention required"
|
"ATTENTION_REQUIRED": "Atención requerida",
|
||||||
|
"EXPIRED": "Su integración de Slack ha caducado. Para continuar recibiendo mensajes en Slack, por favor elimine la integración y vuelva a conectar su área de trabajo."
|
||||||
},
|
},
|
||||||
"UPDATE_ERROR": "There was an error updating the integration, please try again",
|
"UPDATE_ERROR": "Se presento un error actualizando la integración, por favor inténtelo nuevamente",
|
||||||
"UPDATE_SUCCESS": "The channel is connected successfully",
|
"UPDATE_SUCCESS": "El canal está conectado correctamente",
|
||||||
"FAILED_TO_FETCH_CHANNELS": "There was an error fetching the channels from Slack, please try again"
|
"FAILED_TO_FETCH_CHANNELS": "Hubo un error obteniendo los canales de Slack, por favor inténtalo de nuevo"
|
||||||
},
|
},
|
||||||
"DYTE": {
|
"DYTE": {
|
||||||
"CLICK_HERE_TO_JOIN": "Haga clic aquí para unirse",
|
"CLICK_HERE_TO_JOIN": "Haga clic aquí para unirse",
|
||||||
@ -102,28 +103,40 @@
|
|||||||
"CREATE_ERROR": "Hubo un error al crear un enlace de reunión, por favor inténtelo de nuevo"
|
"CREATE_ERROR": "Hubo un error al crear un enlace de reunión, por favor inténtelo de nuevo"
|
||||||
},
|
},
|
||||||
"OPEN_AI": {
|
"OPEN_AI": {
|
||||||
"AI_ASSIST": "AI Assist",
|
"AI_ASSIST": "Asistencia AI",
|
||||||
"WITH_AI": " %{option} with AI ",
|
"WITH_AI": " %{option} con IA ",
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"REPLY_SUGGESTION": "Reply Suggestion",
|
"REPLY_SUGGESTION": "Responder sugerencia",
|
||||||
"SUMMARIZE": "Summarize",
|
"SUMMARIZE": "Resumir",
|
||||||
"REPHRASE": "Improve Writing",
|
"REPHRASE": "Mejorar escritura",
|
||||||
"FIX_SPELLING_GRAMMAR": "Fix Spelling and Grammar",
|
"FIX_SPELLING_GRAMMAR": "Corregir ortografía y gramática",
|
||||||
"SHORTEN": "Shorten",
|
"SHORTEN": "Acortar",
|
||||||
"EXPAND": "Expand",
|
"EXPAND": "Expandir",
|
||||||
"MAKE_FRIENDLY": "Change message tone to friendly",
|
"MAKE_FRIENDLY": "Cambiar tono de mensaje a amigable",
|
||||||
"MAKE_FORMAL": "Use formal tone",
|
"MAKE_FORMAL": "Usar tono formal",
|
||||||
"SIMPLIFY": "Simplify"
|
"SIMPLIFY": "Simplificar"
|
||||||
},
|
},
|
||||||
"ASSISTANCE_MODAL": {
|
"ASSISTANCE_MODAL": {
|
||||||
"DRAFT_TITLE": "Draft content",
|
"DRAFT_TITLE": "Contenido de borrador",
|
||||||
"GENERATED_TITLE": "Generated content",
|
"GENERATED_TITLE": "Contenido generado",
|
||||||
"AI_WRITING": "AI is writing",
|
"AI_WRITING": "AI está escribiendo",
|
||||||
"BUTTONS": {
|
"BUTTONS": {
|
||||||
"APPLY": "Use this suggestion",
|
"APPLY": "Usar esta sugerencia",
|
||||||
"CANCEL": "Cancelar"
|
"CANCEL": "Cancelar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CTA_MODAL": {
|
||||||
|
"TITLE": "Integrar con OpenAI",
|
||||||
|
"DESC": "Trae las características avanzadas de IA a tu panel de control con los modelos GPT de OpenAI. Para empezar, introduce la clave API desde tu cuenta OpenAI.",
|
||||||
|
"KEY_PLACEHOLDER": "Introduzca su clave API OpenAI",
|
||||||
|
"BUTTONS": {
|
||||||
|
"NEED_HELP": "¿Necesitas ayuda?",
|
||||||
|
"DISMISS": "Descartar",
|
||||||
|
"FINISH": "Finalizar configuración"
|
||||||
|
},
|
||||||
|
"DISMISS_MESSAGE": "Puedes configurar la integración OpenAI más tarde cuando quieras.",
|
||||||
|
"SUCCESS_MESSAGE": "Configuración de integración OpenAI exitosa"
|
||||||
|
},
|
||||||
"TITLE": "Mejorar con IA",
|
"TITLE": "Mejorar con IA",
|
||||||
"SUMMARY_TITLE": "Resumen con IA",
|
"SUMMARY_TITLE": "Resumen con IA",
|
||||||
"REPLY_TITLE": "Responder sugerencia con IA",
|
"REPLY_TITLE": "Responder sugerencia con IA",
|
||||||
@ -140,7 +153,7 @@
|
|||||||
"GENERATING": "Generando...",
|
"GENERATING": "Generando...",
|
||||||
"CANCEL": "Cancelar"
|
"CANCEL": "Cancelar"
|
||||||
},
|
},
|
||||||
"GENERATE_ERROR": "Hubo un error al procesar el contenido, por favor inténtelo de nuevo"
|
"GENERATE_ERROR": "Hubo un error al procesar el contenido, por favor verifique su clave API OpenAI e inténtelo de nuevo"
|
||||||
},
|
},
|
||||||
"DELETE": {
|
"DELETE": {
|
||||||
"BUTTON_TEXT": "Eliminar",
|
"BUTTON_TEXT": "Eliminar",
|
||||||
|
|||||||
@ -40,16 +40,16 @@
|
|||||||
},
|
},
|
||||||
"SUGGESTIONS": {
|
"SUGGESTIONS": {
|
||||||
"TOOLTIP": {
|
"TOOLTIP": {
|
||||||
"SINGLE_SUGGESTION": "Add label to conversation",
|
"SINGLE_SUGGESTION": "Añadir etiqueta a la conversación",
|
||||||
"MULTIPLE_SUGGESTION": "Select this label",
|
"MULTIPLE_SUGGESTION": "Seleccionar esta etiqueta",
|
||||||
"DESELECT": "Deselect label",
|
"DESELECT": "Deseleccionar etiqueta",
|
||||||
"DISMISS": "Dismiss suggestion"
|
"DISMISS": "Descartar sugerencia"
|
||||||
},
|
},
|
||||||
"POWERED_BY": "Chatwoot AI",
|
"POWERED_BY": "Chatwoot AI",
|
||||||
"DISMISS": "Descartar",
|
"DISMISS": "Descartar",
|
||||||
"ADD_SELECTED_LABELS": "Add selected labels",
|
"ADD_SELECTED_LABELS": "Asignar etiquetas seleccionadas",
|
||||||
"ADD_SELECTED_LABEL": "Add selected label",
|
"ADD_SELECTED_LABEL": "Añadir etiqueta seleccionada",
|
||||||
"ADD_ALL_LABELS": "Add all labels"
|
"ADD_ALL_LABELS": "Añadir todas las etiquetas"
|
||||||
},
|
},
|
||||||
"ADD": {
|
"ADD": {
|
||||||
"TITLE": "Añadir etiqueta",
|
"TITLE": "Añadir etiqueta",
|
||||||
|
|||||||
@ -12,11 +12,11 @@
|
|||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"INCOMING_MESSAGES": {
|
"INCOMING_MESSAGES": {
|
||||||
"NAME": "Mensajes entrantes",
|
"NAME": "Mensajes recibidos",
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"OUTGOING_MESSAGES": {
|
"OUTGOING_MESSAGES": {
|
||||||
"NAME": "Mensajes salientes",
|
"NAME": "Mensajes enviados",
|
||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"FIRST_RESPONSE_TIME": {
|
"FIRST_RESPONSE_TIME": {
|
||||||
@ -36,8 +36,8 @@
|
|||||||
"DESC": "( Total )"
|
"DESC": "( Total )"
|
||||||
},
|
},
|
||||||
"REPLY_TIME": {
|
"REPLY_TIME": {
|
||||||
"NAME": "Customer waiting time",
|
"NAME": "Tiempo de espera del cliente",
|
||||||
"TOOLTIP_TEXT": "Waiting time is %{metricValue} (based on %{conversationCount} conversations)"
|
"TOOLTIP_TEXT": "El tiempo de espera es %{metricValue} (basado en %{conversationCount} conversaciones)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"DATE_RANGE_OPTIONS": {
|
"DATE_RANGE_OPTIONS": {
|
||||||
@ -117,10 +117,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"GROUP_BY_YEAR_OPTIONS": [
|
"GROUP_BY_YEAR_OPTIONS": [
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"groupBy": "Día"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": 2,
|
"id": 2,
|
||||||
"groupBy": "Semana"
|
"groupBy": "Semana"
|
||||||
@ -128,6 +124,10 @@
|
|||||||
{
|
{
|
||||||
"id": 3,
|
"id": 3,
|
||||||
"groupBy": "Mes"
|
"groupBy": "Mes"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"groupBy": "Mes"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"BUSINESS_HOURS": "Horarios"
|
"BUSINESS_HOURS": "Horarios"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user