Merge branch 'release/2.15.0'

This commit is contained in:
Sojan 2023-03-15 19:58:19 +05:30
commit 3e6d23e071
894 changed files with 15913 additions and 1878 deletions

View File

@ -102,8 +102,8 @@ jobs:
echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'." echo "ERROR: The swagger.json file is not in sync with the yaml specification. Run 'rake swagger:build' and commit 'swagger/swagger.json'."
exit 1 exit 1
fi fi
curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/5.3.0/openapi-generator-cli-5.3.0.jar > ~/tmp/openapi-generator-cli-5.3.0.jar curl -L https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/6.3.0/openapi-generator-cli-6.3.0.jar > ~/tmp/openapi-generator-cli-6.3.0.jar
java -jar ~/tmp/openapi-generator-cli-5.3.0.jar validate -i swagger/swagger.json java -jar ~/tmp/openapi-generator-cli-6.3.0.jar validate -i swagger/swagger.json
# Database setup # Database setup
- run: yarn install --check-files - run: yarn install --check-files

View File

@ -30,7 +30,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
npm npm
# Install rbenv and ruby # Install rbenv and ruby
ARG RUBY_VERSION="3.0.4" ARG RUBY_VERSION="3.1.3"
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \ RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \ && echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc && echo 'eval "$(rbenv init -)"' >> ~/.bashrc

View File

@ -25,7 +25,7 @@
// 1025,8025 mailhog // 1025,8025 mailhog
"forwardPorts": [8025, 3000, 3035], "forwardPorts": [8025, 3000, 3035],
"postCreateCommand": ".devcontainer/scripts/setup.sh && bundle exec rake db:chatwoot_prepare && yarn", "postCreateCommand": ".devcontainer/scripts/setup.sh && POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rake db:chatwoot_prepare && yarn",
"portsAttributes": { "portsAttributes": {
"3000": { "3000": {
"label": "Rails Server" "label": "Rails Server"

View File

@ -52,6 +52,8 @@ POSTGRES_HOST=postgres
POSTGRES_USERNAME=postgres POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD= POSTGRES_PASSWORD=
RAILS_ENV=development RAILS_ENV=development
# Changes the Postgres query timeout limit. The default is 14 seconds. Modify only when required.
# POSTGRES_STATEMENT_TIMEOUT=14s
RAILS_MAX_THREADS=5 RAILS_MAX_THREADS=5
# The email from which all outgoing emails are sent # The email from which all outgoing emails are sent
@ -169,6 +171,9 @@ USE_INBOX_AVATAR_FOR_BOT=true
## Sentry ## Sentry
# SENTRY_DSN= # SENTRY_DSN=
## LogRocket
# LOG_ROCKET_PROJECT_ID=xxxxx/some-project
## Scout ## Scout
## https://scoutapm.com/docs/ruby/configuration ## https://scoutapm.com/docs/ruby/configuration
# SCOUT_KEY=YOURKEY # SCOUT_KEY=YOURKEY
@ -186,11 +191,7 @@ USE_INBOX_AVATAR_FOR_BOT=true
## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables ## https://github.com/DataDog/dd-trace-rb/blob/master/docs/GettingStarted.md#environment-variables
# DD_TRACE_AGENT_URL= # DD_TRACE_AGENT_URL=
## IP look up configuration # MaxMindDB API key to download GeoLite2 City database
## ref https://github.com/alexreisner/geocoder/blob/master/README_API_GUIDE.md
## works only on accounts with ip look up feature enabled
# IP_LOOKUP_SERVICE=geoip2
# maxmindb api key to use geoip2 service
# IP_LOOKUP_API_KEY= # IP_LOOKUP_API_KEY=
## Rack Attack configuration ## Rack Attack configuration

View File

@ -44,3 +44,9 @@ jobs:
# sudo systemctl restart chatwoot.target # sudo systemctl restart chatwoot.target
# curl http://localhost:3000/api # curl http://localhost:3000/api
- name: Upload chatwoot setup log file as an artifact
uses: actions/upload-artifact@v3
if: always()
with:
name: chatwoot-setup-log-file
path: /var/log/chatwoot-setup.log

View File

@ -15,7 +15,7 @@ on:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-20.04
services: services:
postgres: postgres:
image: postgres:10.8 image: postgres:10.8
@ -49,6 +49,10 @@ jobs:
with: with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v3
with:
node-version: 16
- name: yarn - name: yarn
run: yarn install run: yarn install
@ -70,3 +74,10 @@ 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
- name: Upload rails log folder
uses: actions/upload-artifact@v3
if: always()
with:
name: rails-log-folder
path: log

View File

@ -5,7 +5,7 @@
npx --no-install lint-staged npx --no-install lint-staged
# lint only staged ruby files # lint only staged ruby files
git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop -a git diff --name-only --cached | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bundle exec rubocop --force-exclusion -a
# stage rubocop changes to files # stage rubocop changes to files
git diff --name-only --cached | xargs git add git diff --name-only --cached | xargs git add

View File

@ -1 +1,6 @@
{} {
"cSpell.words": [
"chatwoot",
"dompurify"
]
}

21
Gemfile
View File

@ -4,7 +4,7 @@ ruby '3.1.3'
##-- base gems for rails --## ##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors' gem 'rack-cors', require: 'rack/cors'
gem 'rails', '~> 6.1', '>= 6.1.7.1' gem 'rails', '~> 6.1', '>= 6.1.7.3'
# 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
@ -96,13 +96,16 @@ gem 'slack-ruby-client'
gem 'google-cloud-dialogflow' gem 'google-cloud-dialogflow'
##-- apm and error monitoring ---# ##-- apm and error monitoring ---#
gem 'ddtrace' # loaded only when environment variables are set.
gem 'elastic-apm' # ref application.rb
gem 'newrelic_rpm' gem 'ddtrace', require: false
gem 'scout_apm' gem 'elastic-apm', require: false
gem 'sentry-rails', '~> 5.3', '>= 5.3.1' gem 'newrelic_rpm', require: false
gem 'sentry-ruby', '~> 5.3' gem 'newrelic-sidekiq-metrics', require: false
gem 'sentry-sidekiq', '~> 5.3', '>= 5.3.1' gem 'scout_apm', require: false
gem 'sentry-rails', require: false
gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', require: false
##-- background job processing --## ##-- background job processing --##
gem 'sidekiq', '~> 6.4.2' gem 'sidekiq', '~> 6.4.2'
@ -203,6 +206,8 @@ end
# worked with microsoft refresh token # worked with microsoft refresh token
gem 'omniauth-oauth2' gem 'omniauth-oauth2'
gem 'audited', '~> 5.2'
# need for google auth # need for google auth
gem 'omniauth' gem 'omniauth'
gem 'omniauth-google-oauth2' gem 'omniauth-google-oauth2'

View File

@ -9,63 +9,63 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (6.1.7.1) actioncable (6.1.7.3)
actionpack (= 6.1.7.1) actionpack (= 6.1.7.3)
activesupport (= 6.1.7.1) activesupport (= 6.1.7.3)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
actionmailbox (6.1.7.1) actionmailbox (6.1.7.3)
actionpack (= 6.1.7.1) actionpack (= 6.1.7.3)
activejob (= 6.1.7.1) activejob (= 6.1.7.3)
activerecord (= 6.1.7.1) activerecord (= 6.1.7.3)
activestorage (= 6.1.7.1) activestorage (= 6.1.7.3)
activesupport (= 6.1.7.1) activesupport (= 6.1.7.3)
mail (>= 2.7.1) mail (>= 2.7.1)
actionmailer (6.1.7.1) actionmailer (6.1.7.3)
actionpack (= 6.1.7.1) actionpack (= 6.1.7.3)
actionview (= 6.1.7.1) actionview (= 6.1.7.3)
activejob (= 6.1.7.1) activejob (= 6.1.7.3)
activesupport (= 6.1.7.1) activesupport (= 6.1.7.3)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0) rails-dom-testing (~> 2.0)
actionpack (6.1.7.1) actionpack (6.1.7.3)
actionview (= 6.1.7.1) actionview (= 6.1.7.3)
activesupport (= 6.1.7.1) activesupport (= 6.1.7.3)
rack (~> 2.0, >= 2.0.9) rack (~> 2.0, >= 2.0.9)
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 (6.1.7.1) actiontext (6.1.7.3)
actionpack (= 6.1.7.1) actionpack (= 6.1.7.3)
activerecord (= 6.1.7.1) activerecord (= 6.1.7.3)
activestorage (= 6.1.7.1) activestorage (= 6.1.7.3)
activesupport (= 6.1.7.1) activesupport (= 6.1.7.3)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (6.1.7.1) actionview (6.1.7.3)
activesupport (= 6.1.7.1) activesupport (= 6.1.7.3)
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 (6.1.7.1) activejob (6.1.7.3)
activesupport (= 6.1.7.1) activesupport (= 6.1.7.3)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (6.1.7.1) activemodel (6.1.7.3)
activesupport (= 6.1.7.1) activesupport (= 6.1.7.3)
activerecord (6.1.7.1) activerecord (6.1.7.3)
activemodel (= 6.1.7.1) activemodel (= 6.1.7.3)
activesupport (= 6.1.7.1) activesupport (= 6.1.7.3)
activerecord-import (1.4.0) activerecord-import (1.4.0)
activerecord (>= 4.2) activerecord (>= 4.2)
activestorage (6.1.7.1) activestorage (6.1.7.3)
actionpack (= 6.1.7.1) actionpack (= 6.1.7.3)
activejob (= 6.1.7.1) activejob (= 6.1.7.3)
activerecord (= 6.1.7.1) activerecord (= 6.1.7.3)
activesupport (= 6.1.7.1) activesupport (= 6.1.7.3)
marcel (~> 1.0) marcel (~> 1.0)
mini_mime (>= 1.1.0) mini_mime (>= 1.1.0)
activesupport (6.1.7.1) activesupport (6.1.7.3)
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)
@ -90,6 +90,8 @@ GEM
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
ast (2.4.2) ast (2.4.2)
attr_extras (6.2.5) attr_extras (6.2.5)
audited (5.2.0)
activerecord (>= 5.0, < 7.1)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.605.0) aws-partitions (1.605.0)
aws-sdk-core (3.131.2) aws-sdk-core (3.131.2)
@ -136,7 +138,7 @@ GEM
climate_control (1.1.1) climate_control (1.1.1)
coderay (1.1.3) coderay (1.1.3)
commonmarker (0.23.7) commonmarker (0.23.7)
concurrent-ruby (1.1.10) concurrent-ruby (1.2.2)
connection_pool (2.2.5) connection_pool (2.2.5)
crack (0.4.5) crack (0.4.5)
rexml rexml
@ -187,7 +189,7 @@ GEM
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
http (>= 3.0) http (>= 3.0)
email_reply_trimmer (0.1.13) email_reply_trimmer (0.1.13)
erubi (1.10.0) erubi (1.12.0)
et-orbi (1.2.7) et-orbi (1.2.7)
tzinfo tzinfo
execjs (2.8.1) execjs (2.8.1)
@ -248,7 +250,7 @@ GEM
grpc (~> 1.36) grpc (~> 1.36)
geocoder (1.8.0) geocoder (1.8.0)
gli (2.21.0) gli (2.21.0)
globalid (1.0.1) globalid (1.1.0)
activesupport (>= 5.0) activesupport (>= 5.0)
gmail_xoauth (0.4.2) gmail_xoauth (0.4.2)
oauth (>= 0.3.6) oauth (>= 0.3.6)
@ -353,7 +355,7 @@ GEM
mini_mime (>= 1.0.0) mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2) multi_xml (>= 0.5.2)
httpclient (2.8.3) httpclient (2.8.3)
i18n (1.11.0) i18n (1.12.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2) image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
@ -417,8 +419,11 @@ GEM
loofah (2.19.1) loofah (2.19.1)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.1) mail (2.8.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
net-imap
net-pop
net-smtp
marcel (1.0.2) marcel (1.0.2)
maxminddb (0.1.22) maxminddb (0.1.22)
memoist (0.16.2) memoist (0.16.2)
@ -428,8 +433,8 @@ GEM
mime-types-data (3.2022.0105) mime-types-data (3.2022.0105)
mini_magick (4.11.0) mini_magick (4.11.0)
mini_mime (1.1.2) mini_mime (1.1.2)
mini_portile2 (2.8.0) mini_portile2 (2.8.1)
minitest (5.16.2) minitest (5.18.0)
mock_redis (0.32.0) mock_redis (0.32.0)
ruby2_keywords ruby2_keywords
momentjs-rails (2.29.1.1) momentjs-rails (2.29.1.1)
@ -449,16 +454,19 @@ GEM
net-smtp (0.3.3) net-smtp (0.3.3)
net-protocol net-protocol
netrc (0.11.0) netrc (0.11.0)
newrelic-sidekiq-metrics (1.6.1)
newrelic_rpm (~> 8)
sidekiq
newrelic_rpm (8.15.0) newrelic_rpm (8.15.0)
nio4r (2.5.8) nio4r (2.5.8)
nokogiri (1.13.10) nokogiri (1.14.2)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.10-arm64-darwin) nokogiri (1.14.2-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.10-x86_64-darwin) nokogiri (1.14.2-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.10-x86_64-linux) nokogiri (1.14.2-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
oauth (0.5.10) oauth (0.5.10)
oauth2 (2.0.9) oauth2 (2.0.9)
@ -506,8 +514,8 @@ GEM
pundit (2.2.0) pundit (2.2.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.6.1) racc (1.6.2)
rack (2.2.6.2) rack (2.2.6.4)
rack-attack (6.6.1) rack-attack (6.6.1)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
rack-cors (1.1.1) rack-cors (1.1.1)
@ -516,32 +524,32 @@ GEM
rack rack
rack-proxy (0.7.2) rack-proxy (0.7.2)
rack rack
rack-test (2.0.2) rack-test (2.1.0)
rack (>= 1.3) rack (>= 1.3)
rack-timeout (0.6.3) rack-timeout (0.6.3)
rails (6.1.7.1) rails (6.1.7.3)
actioncable (= 6.1.7.1) actioncable (= 6.1.7.3)
actionmailbox (= 6.1.7.1) actionmailbox (= 6.1.7.3)
actionmailer (= 6.1.7.1) actionmailer (= 6.1.7.3)
actionpack (= 6.1.7.1) actionpack (= 6.1.7.3)
actiontext (= 6.1.7.1) actiontext (= 6.1.7.3)
actionview (= 6.1.7.1) actionview (= 6.1.7.3)
activejob (= 6.1.7.1) activejob (= 6.1.7.3)
activemodel (= 6.1.7.1) activemodel (= 6.1.7.3)
activerecord (= 6.1.7.1) activerecord (= 6.1.7.3)
activestorage (= 6.1.7.1) activestorage (= 6.1.7.3)
activesupport (= 6.1.7.1) activesupport (= 6.1.7.3)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 6.1.7.1) railties (= 6.1.7.3)
sprockets-rails (>= 2.0.0) sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.4.4) rails-html-sanitizer (1.5.0)
loofah (~> 2.19, >= 2.19.1) loofah (~> 2.19, >= 2.19.1)
railties (6.1.7.1) railties (6.1.7.3)
actionpack (= 6.1.7.1) actionpack (= 6.1.7.3)
activesupport (= 6.1.7.1) activesupport (= 6.1.7.3)
method_source method_source
rake (>= 12.2) rake (>= 12.2)
thor (~> 1.0) thor (~> 1.0)
@ -676,9 +684,9 @@ GEM
spring-watcher-listen (2.0.1) spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0) listen (>= 2.7, < 4.0)
spring (>= 1.2, < 3.0) spring (>= 1.2, < 3.0)
sprockets (4.1.1) sprockets (4.2.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (> 1, < 3) rack (>= 2.2.4, < 4)
sprockets-rails (3.4.2) sprockets-rails (3.4.2)
actionpack (>= 5.2) actionpack (>= 5.2)
activesupport (>= 5.2) activesupport (>= 5.2)
@ -701,7 +709,7 @@ GEM
nokogiri (>= 1.6, < 2.0) nokogiri (>= 1.6, < 2.0)
twitty (0.1.4) twitty (0.1.4)
oauth oauth
tzinfo (2.0.4) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
tzinfo-data (1.2022.1) tzinfo-data (1.2022.1)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
@ -746,7 +754,7 @@ GEM
working_hours (1.4.1) working_hours (1.4.1)
activesupport (>= 3.2) activesupport (>= 3.2)
tzinfo tzinfo
zeitwerk (2.6.0) zeitwerk (2.6.7)
PLATFORMS PLATFORMS
arm64-darwin-20 arm64-darwin-20
@ -763,6 +771,7 @@ DEPENDENCIES
administrate administrate
annotate annotate
attr_extras attr_extras
audited (~> 5.2)
aws-sdk-s3 aws-sdk-s3
azure-storage-blob azure-storage-blob
barnes barnes
@ -817,6 +826,7 @@ DEPENDENCIES
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
newrelic-sidekiq-metrics
newrelic_rpm newrelic_rpm
omniauth omniauth
omniauth-google-oauth2 omniauth-google-oauth2
@ -831,7 +841,7 @@ DEPENDENCIES
rack-attack rack-attack
rack-cors rack-cors
rack-timeout rack-timeout
rails (~> 6.1, >= 6.1.7.1) rails (~> 6.1, >= 6.1.7.3)
redis redis
redis-namespace redis-namespace
responders responders
@ -844,9 +854,9 @@ DEPENDENCIES
rubocop-rspec rubocop-rspec
scout_apm scout_apm
seed_dump seed_dump
sentry-rails (~> 5.3, >= 5.3.1) sentry-rails
sentry-ruby (~> 5.3) sentry-ruby
sentry-sidekiq (~> 5.3, >= 5.3.1) sentry-sidekiq
shoulda-matchers shoulda-matchers
sidekiq (~> 6.4.2) sidekiq (~> 6.4.2)
sidekiq-cron (~> 1.6, >= 1.6.0) sidekiq-cron (~> 1.6, >= 1.6.0)

View File

@ -1,3 +1,3 @@
release: bundle exec rails db:chatwoot_prepare release: POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rails db:chatwoot_prepare
web: bin/rails server -p $PORT -e $RAILS_ENV web: bundle exec rails ip_lookup:setup && bin/rails server -p $PORT -e $RAILS_ENV
worker: bundle exec sidekiq -C config/sidekiq.yml worker: bundle exec rails ip_lookup:setup && bundle exec sidekiq -C config/sidekiq.yml

View File

@ -2,3 +2,4 @@
//= link administrate/application.css //= link administrate/application.css
//= link administrate/application.js //= link administrate/application.js
//= link dashboardChart.js //= link dashboardChart.js
//= link secretField.js

View File

@ -0,0 +1,34 @@
// eslint-disable-next-line
function toggleSecretField(e) {
e.preventDefault();
e.stopPropagation();
const toggler = e.currentTarget;
const secretField = toggler.parentElement;
const textElement = secretField.querySelector('[data-secret-masked]');
if (!textElement) return;
if (textElement.dataset.secretMasked === 'false') {
textElement.textContent = '•'.repeat(10);
textElement.dataset.secretMasked = 'true';
toggler.querySelector('svg use').setAttribute('xlink:href', '#eye-show');
return;
}
textElement.textContent = secretField.dataset.secretText;
textElement.dataset.secretMasked = 'false';
toggler.querySelector('svg use').setAttribute('xlink:href', '#eye-hide');
}
// eslint-disable-next-line
function copySecretField(e) {
e.preventDefault();
e.stopPropagation();
const toggler = e.currentTarget;
const secretField = toggler.parentElement;
navigator.clipboard.writeText(secretField.dataset.secretText);
}

View File

@ -6,7 +6,6 @@
@import 'utilities/text-color'; @import 'utilities/text-color';
@import 'selectize'; @import 'selectize';
@import 'datetime_picker';
@import 'library/clearfix'; @import 'library/clearfix';
@import 'library/data-label'; @import 'library/data-label';

View File

@ -43,3 +43,20 @@
.cell-label--number { .cell-label--number {
text-align: right; text-align: right;
} }
.cell-data__secret-field {
align-items: center;
display: flex;
span {
flex: 1;
}
button {
margin-left: 5px;
svg {
fill: currentColor;
}
}
}

View File

@ -116,7 +116,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
result = {} result = {}
# OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user # OAuthException, code: 100, error_subcode: 2018218, message: (#100) No profile available for this user
# We don't need to capture this error as we don't care about contact params in case of echo messages # We don't need to capture this error as we don't care about contact params in case of echo messages
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception unless @outgoing_echo if e.message.include?('2018218')
Rails.logger.warn e
else
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception unless @outgoing_echo
end
rescue StandardError => e rescue StandardError => e
result = {} result = {}
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception

View File

@ -74,7 +74,7 @@ class V2::ReportBuilder
:created_at, :created_at,
default_value: 0, default_value: 0,
range: range, range: range,
permit: %w[day week month year], permit: %w[day week month year hour],
time_zone: @timezone time_zone: @timezone
) )
end end

View File

@ -55,9 +55,9 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
def article_params def article_params
params.require(:article).permit( params.require(:article).permit(
:title, :slug, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title, :title, :slug, :position, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title,
:description, :description,
{ tags: [] }] { tags: [] }]
) )
end end

View File

@ -142,6 +142,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
# and deprecate the support of passing only source_id as the param # and deprecate the support of passing only source_id as the param
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id]) @contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
authorize @contact_inbox.inbox, :show? authorize @contact_inbox.inbox, :show?
rescue ActiveRecord::RecordNotUnique
render json: { error: 'source_id should be unique' }, status: :unprocessable_entity
end end
def build_contact_inbox def build_contact_inbox

View File

@ -1,5 +1,5 @@
class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseController class Api::V1::Accounts::Integrations::AppsController < Api::V1::Accounts::BaseController
before_action :check_admin_authorization? before_action :check_admin_authorization?, except: [:index, :show]
before_action :fetch_apps, only: [:index] before_action :fetch_apps, only: [:index]
before_action :fetch_app, only: [:show] before_action :fetch_app, only: [:show]

View File

@ -0,0 +1,28 @@
class Api::V1::Accounts::SearchController < Api::V1::Accounts::BaseController
def index
@result = search('all')
end
def conversations
@result = search('Conversation')
end
def contacts
@result = search('Contact')
end
def messages
@result = search('Message')
end
private
def search(search_type)
SearchService.new(
current_user: Current.user,
current_account: Current.account,
search_type: search_type,
params: params
).perform
end
end

View File

@ -17,7 +17,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
@message.update!(submitted_email: contact_email) @message.update!(submitted_email: contact_email)
ContactIdentifyAction.new( ContactIdentifyAction.new(
contact: @contact, contact: @contact,
params: { email: contact_email } params: { email: contact_email, name: contact_name }
).perform ).perform
else else
@message.update!(message_update_params[:message]) @message.update!(message_update_params[:message])

View File

@ -45,13 +45,18 @@ class Microsoft::CallbacksController < ApplicationController
channel_email.inbox channel_email.inbox
end end
# Fallback name, for when name field is missing from users_data
def fallback_name
users_data['email'].split('@').first.parameterize.titleize
end
def create_microsoft_channel_with_inbox def create_microsoft_channel_with_inbox
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
channel_email = Channel::Email.create!(email: users_data['email'], account: account) channel_email = Channel::Email.create!(email: users_data['email'], account: account)
account.inboxes.create!( account.inboxes.create!(
account: account, account: account,
channel: channel_email, channel: channel_email,
name: users_data['name'] name: users_data['name'] || fallback_name
) )
channel_email channel_email
end end

View File

@ -12,6 +12,8 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
end end
def update def update
render json: { error: 'You cannot update the CSAT survey after 14 days' }, status: :unprocessable_entity and return if check_csat_locked
@message.update!(message_update_params) @message.update!(message_update_params)
rescue StandardError => e rescue StandardError => e
render json: { error: @contact.errors, message: e.message }.to_json, status: :internal_server_error render json: { error: @contact.errors, message: e.message }.to_json, status: :internal_server_error
@ -43,7 +45,7 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
end end
def message_update_params def message_update_params
params.permit(submitted_values: [:name, :title, :value]) params.permit(submitted_values: [:name, :title, :value, { csat_survey_response: [:feedback_message, :rating] }])
end end
def permitted_params def permitted_params
@ -64,4 +66,8 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
message_type: :incoming message_type: :incoming
} }
end end
def check_csat_locked
(Time.zone.now.to_date - @message.created_at.to_date).to_i > 14 and @message.content_type == 'input_csat'
end
end end

View File

@ -1,4 +1,4 @@
class Public::Api::V1::Portals::ArticlesController < PublicController class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::BaseController
before_action :ensure_custom_domain_request, only: [:show, :index] before_action :ensure_custom_domain_request, only: [:show, :index]
before_action :portal before_action :portal
before_action :set_category, except: [:index] before_action :set_category, except: [:index]
@ -8,6 +8,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
def index def index
@articles = @portal.articles @articles = @portal.articles
@articles = @articles.search(list_params) if list_params.present? @articles = @articles.search(list_params) if list_params.present?
@articles.order(position: :asc)
end end
def show; end def show; end
@ -15,21 +16,30 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
private private
def set_article def set_article
@article = @category.articles.find(params[:id]) @article = @category.articles.find(permitted_params[:id])
@article.increment_view_count @article.increment_view_count
@parsed_content = render_article_content(@article.content) @parsed_content = render_article_content(@article.content)
end end
def set_category def set_category
@category = @portal.categories.find_by!(slug: params[:category_slug]) if params[:category_slug].present? return if permitted_params[:category_slug].blank?
@category = @portal.categories.find_by!(
slug: permitted_params[:category_slug],
locale: permitted_params[:locale]
)
end end
def portal def portal
@portal ||= Portal.find_by!(slug: params[:slug], archived: false) @portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false)
end end
def list_params def list_params
params.permit(:query) params.permit(:query, :locale)
end
def permitted_params
params.permit(:slug, :category_slug, :locale, :id)
end end
def render_article_content(content) def render_article_content(content)

View File

@ -0,0 +1,22 @@
class Public::Api::V1::Portals::BaseController < PublicController
around_action :set_locale
private
def set_locale(&)
switch_locale_with_portal(&) if params[:locale].present?
end
def switch_locale_with_portal(&)
locale_without_variant = params[:locale].split('_')[0]
is_locale_available = I18n.available_locales.map(&:to_s).include?(params[:locale])
is_locale_variant_available = I18n.available_locales.map(&:to_s).include?(locale_without_variant)
if is_locale_available
@locale = params[:locale]
elsif is_locale_variant_available
@locale = locale_without_variant
end
I18n.with_locale(@locale, &)
end
end

View File

@ -1,11 +1,11 @@
class Public::Api::V1::Portals::CategoriesController < PublicController class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals::BaseController
before_action :ensure_custom_domain_request, only: [:show, :index] before_action :ensure_custom_domain_request, only: [:show, :index]
before_action :portal before_action :portal
before_action :set_category, only: [:show] before_action :set_category, only: [:show]
layout 'portal' layout 'portal'
def index def index
@categories = @portal.categories @categories = @portal.categories.order(position: :asc)
end end
def show; end def show; end

View File

@ -1,4 +1,4 @@
class Public::Api::V1::PortalsController < PublicController class Public::Api::V1::PortalsController < Public::Api::V1::Portals::BaseController
before_action :ensure_custom_domain_request, only: [:show] before_action :ensure_custom_domain_request, only: [:show]
before_action :portal before_action :portal
before_action :redirect_to_portal_with_locale, only: [:show] before_action :redirect_to_portal_with_locale, only: [:show]

View File

@ -5,6 +5,7 @@ class WidgetsController < ActionController::Base
before_action :set_global_config before_action :set_global_config
before_action :set_web_widget before_action :set_web_widget
before_action :ensure_account_is_active before_action :ensure_account_is_active
before_action :ensure_location_is_supported
before_action :set_token before_action :set_token
before_action :set_contact before_action :set_contact
before_action :build_contact before_action :build_contact
@ -18,6 +19,9 @@ class WidgetsController < ActionController::Base
def set_web_widget def set_web_widget
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token]) @web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
rescue ActiveRecord::RecordNotFound
Rails.logger.error('web widget does not exist')
render json: { error: 'web widget does not exist' }, status: :not_found
end end
def set_token def set_token
@ -51,6 +55,8 @@ class WidgetsController < ActionController::Base
render json: { error: 'Account is suspended' }, status: :unauthorized unless @web_widget.inbox.account.active? render json: { error: 'Account is suspended' }, status: :unauthorized unless @web_widget.inbox.account.active?
end end
def ensure_location_is_supported; end
def additional_attributes def additional_attributes
if @web_widget.inbox.account.feature_enabled?('ip_lookup') if @web_widget.inbox.account.feature_enabled?('ip_lookup')
{ created_at_ip: request.remote_ip } { created_at_ip: request.remote_ip }
@ -67,3 +73,5 @@ class WidgetsController < ActionController::Base
response.headers.delete('X-Frame-Options') response.headers.delete('X-Frame-Options')
end end
end end
WidgetsController.prepend_mod_with('WidgetsController')

View File

@ -10,7 +10,7 @@ class AccessTokenDashboard < Administrate::BaseDashboard
ATTRIBUTE_TYPES = { ATTRIBUTE_TYPES = {
owner: Field::Polymorphic, owner: Field::Polymorphic,
id: Field::Number, id: Field::Number,
token: Field::String, token: SecretField,
created_at: Field::DateTime, created_at: Field::DateTime,
updated_at: Field::DateTime updated_at: Field::DateTime
}.freeze }.freeze

View File

@ -0,0 +1,4 @@
require 'administrate/field/base'
class SecretField < Administrate::Field::String
end

View File

@ -1,4 +1,6 @@
class EmailChannelFinder class EmailChannelFinder
include EmailHelper
def initialize(email_object) def initialize(email_object)
@email_object = email_object @email_object = email_object
end end
@ -7,7 +9,8 @@ class EmailChannelFinder
channel = nil channel = nil
recipient_mails = @email_object.to.to_a + @email_object.cc.to_a recipient_mails = @email_object.to.to_a + @email_object.cc.to_a
recipient_mails.each do |email| recipient_mails.each do |email|
channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', email.downcase, email.downcase) normalized_email = normalize_email_with_plus_addressing(email)
channel = Channel::Email.find_by('lower(email) = ? OR lower(forward_to_email) = ?', normalized_email, normalized_email)
break if channel.present? break if channel.present?
end end
channel channel

View File

@ -3,4 +3,11 @@ module EmailHelper
domain = email.split('@').last domain = email.split('@').last
domain.split('.').first domain.split('.').first
end end
# ref: https://www.rfc-editor.org/rfc/rfc5233.html
# This is not a mandatory requirement for email addresses, but it is a common practice.
# john+test@xyc.com is the same as john@xyc.com
def normalize_email_with_plus_addressing(email)
"#{email.split('@').first.split('+').first}@#{email.split('@').last}".downcase
end
end end

View File

@ -17,6 +17,15 @@ module ReportingEventHelper
from_in_inbox_timezone.working_time_until(to_in_inbox_timezone) from_in_inbox_timezone.working_time_until(to_in_inbox_timezone)
end end
def last_non_human_activity(conversation)
# check if a handoff event already exists
handoff_event = ReportingEvent.where(conversation_id: conversation.id, name: 'conversation_bot_handoff').last
# if a handoff exists, last non human activity is when the handoff ended,
# otherwise it's when the conversation was created
handoff_event&.event_end_time || conversation.created_at
end
private private
def configure_working_hours(working_hours) def configure_working_hours(working_hours)

View File

@ -1,5 +1,10 @@
<template> <template>
<div v-if="!authUIFlags.isFetching" id="app" class="app-wrapper app-root"> <div
v-if="!authUIFlags.isFetching"
id="app"
class="app-wrapper app-root"
:class="{ 'app-rtl--wrapper': isRTLView }"
>
<update-banner :latest-chatwoot-version="latestChatwootVersion" /> <update-banner :latest-chatwoot-version="latestChatwootVersion" />
<transition name="fade" mode="out-in"> <transition name="fade" mode="out-in">
<router-view /> <router-view />
@ -22,6 +27,7 @@ import NetworkNotification from './components/NetworkNotification';
import UpdateBanner from './components/app/UpdateBanner.vue'; import UpdateBanner from './components/app/UpdateBanner.vue';
import vueActionCable from './helper/actionCable'; import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer'; import WootSnackbarBox from './components/SnackbarContainer';
import rtlMixin from 'shared/mixins/rtlMixin';
import { import {
registerSubscription, registerSubscription,
verifyServiceWorkerExistence, verifyServiceWorkerExistence,
@ -38,6 +44,8 @@ export default {
WootSnackbarBox, WootSnackbarBox,
}, },
mixins: [rtlMixin],
data() { data() {
return { return {
showAddAccountModal: false, showAddAccountModal: false,
@ -96,6 +104,7 @@ export default {
} = this.getAccount(this.currentAccountId); } = this.getAccount(this.currentAccountId);
const { pubsub_token: pubsubToken } = this.currentUser || {}; const { pubsub_token: pubsubToken } = this.currentUser || {};
this.setLocale(locale); this.setLocale(locale);
this.updateRTLDirectionView(locale);
this.latestChatwootVersion = latestChatwootVersion; this.latestChatwootVersion = latestChatwootVersion;
vueActionCable.init(pubsubToken); vueActionCable.init(pubsubToken);
}, },

View File

@ -8,20 +8,20 @@ class ReportsAPI extends ApiClient {
super('reports', { accountScoped: true, apiVersion: 'v2' }); super('reports', { accountScoped: true, apiVersion: 'v2' });
} }
getReports( getReports({
metric, metric,
since, from,
until, to,
type = 'account', type = 'account',
id, id,
group_by, group_by,
business_hours business_hours,
) { }) {
return axios.get(`${this.url}`, { return axios.get(`${this.url}`, {
params: { params: {
metric, metric,
since, since: from,
until, until: to,
type, type,
id, id,
group_by, group_by,

View File

@ -0,0 +1,18 @@
/* global axios */
import ApiClient from './ApiClient';
class SearchAPI extends ApiClient {
constructor() {
super('search', { accountScoped: true });
}
get({ q }) {
return axios.get(this.url, {
params: {
q,
},
});
}
}
export default new SearchAPI();

View File

@ -20,7 +20,11 @@ describe('#Reports API', () => {
}); });
describeWithAPIMock('API calls', context => { describeWithAPIMock('API calls', context => {
it('#getAccountReports', () => { it('#getAccountReports', () => {
reportsAPI.getReports('conversations_count', 1621103400, 1621621800); reportsAPI.getReports({
metric: 'conversations_count',
from: 1621103400,
to: 1621621800,
});
expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', { expect(context.axiosMock.get).toHaveBeenCalledWith('/api/v2/reports', {
params: { params: {
metric: 'conversations_count', metric: 'conversations_count',

View File

@ -14,11 +14,11 @@
} }
.mx-input { .mx-input {
border: 1px solid var(--color-border); border: 1px solid var(--s-200);
border-radius: var(--border-radius-normal); border-radius: var(--border-radius-normal);
box-shadow: none; box-shadow: none;
display: flex; display: flex;
height: 4.6rem; height: 4.0rem;
} }
.mx-input:disabled, .mx-input:disabled,

View File

@ -7,6 +7,10 @@
} }
} }
select {
height: 4.0rem;
}
.card { .card {
margin-bottom: var(--space-small); margin-bottom: var(--space-small);
padding: var(--space-normal); padding: var(--space-normal);
@ -64,7 +68,3 @@ code {
.padding-right-small { .padding-right-small {
padding-right: var(--space-one); padding-right: var(--space-one);
} }
.margin-right-small {
margin-right: var(--space-small);
}

View File

@ -16,12 +16,12 @@ body {
} }
.banner + .app-wrapper { .banner + .app-wrapper {
.button--fixed-right-top { .button--fixed-top {
top: 5.6 * $space-one; top: 5.6 * $space-one;
} }
.off-canvas-content { .off-canvas-content {
.button--fixed-right-top { .button--fixed-top {
top: $space-small; top: $space-small;
} }
} }

View File

@ -0,0 +1,499 @@
.app-rtl--wrapper {
direction: rtl;
// Primary sidebar
.primary--sidebar {
border-left: 1px solid var(--s-50);
border-right: 0;
.options-menu.dropdown-pane {
right: var(--space-smaller);
.auto-offline--toggle {
padding: var(--space-smaller) var(--space-one) var(--space-smaller)
var(--space-smaller);
}
.status-items .button {
text-align: right;
}
}
}
// Secondary sidebar
.secondary-sidebar {
.secondary-menu {
border-left: 1px solid var(--s-50);
border-right: 0;
.nested.vertical.menu {
.badge--icon {
margin-left: var(--space-smaller);
margin-right: unset;
}
.menu-label {
text-align: right;
}
}
.secondary-menu--icon {
margin-left: var(--space-smaller);
margin-right: unset;
}
.account-context--group .account-context--switch-group {
--overlay-shadow: linear-gradient(
to left,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 50%
);
background-image: var(--overlay-shadow);
}
// Help center sidebar
.sidebar-header--wrap .header-title--wrap {
margin-left: unset;
margin-right: var(--space-small);
}
}
}
// Woot button
.button {
.icon--emoji + .button__content {
padding-left: 0;
padding-right: var(--space-small);
}
.icon--font + .button__content {
padding-left: 0;
padding-right: var(--space-small);
}
.icon + .button__content {
padding-left: 0;
padding-right: var(--space-small);
}
}
// Settings header
.settings-header {
.header--icon {
margin-left: var(--space-small);
margin-right: var(--space-smaller);
}
}
.settings.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
.tabs-title {
&:first-child {
margin-left: var(--space-small);
margin-right: unset;
}
&:last-child {
margin-left: unset;
margin-right: var(--space-small);
}
}
// woot tables
table,
thead,
th {
text-align: right;
}
// Table footer
.footer {
.page-meta {
direction: initial;
}
}
// Wizard box
.wizard-box {
direction: initial;
}
// Conversation details
.conversation-details-wrap {
.conv-header {
.user {
margin-left: var(--space-normal);
margin-right: unset;
.user--profile__meta {
margin-left: unset;
margin-right: var(--space-small);
}
}
.actions--container .resolve-actions {
margin-left: unset;
margin-right: var(--space-small);
}
}
.conversation-panel {
// Message text
.text-content {
p {
unicode-bidi: plaintext;
}
ul {
padding-left: unset;
padding-right: var(--space-two);
}
li {
text-align: right;
}
}
// Message items and actions
li {
&.right {
.sender--info {
padding: var(--space-small) var(--space-smaller)
var(--space-smaller) 0;
}
.context-menu-wrap {
margin-left: 0;
margin-right: auto;
}
}
}
}
// Conversation footer
.conversation-footer {
.preview-item {
direction: initial;
}
}
// Custom attributes section in conversation sidebar
.conversation-sidebar-wrap .checkbox-wrap {
.checkbox {
margin-left: var(--space-small);
}
}
// Conversation sidebar toggle button
.sidebar-toggle--button {
transform: rotate(180deg);
}
// Conversation sidebar close button
.close-button--rtl {
transform: rotate(180deg);
}
// Resolve actions button
.resolve-actions {
.button-group .button:first-child {
border-bottom-left-radius: 0;
border-bottom-right-radius: var(--border-radius-normal);
border-top-left-radius: 0;
border-top-right-radius: var(--border-radius-normal);
}
.button-group .button:last-child {
border-bottom-left-radius: var(--border-radius-normal);
border-bottom-right-radius: 0;
border-top-left-radius: var(--border-radius-normal);
border-top-right-radius: 0;
}
}
}
// Conversation list
.conversations-list-wrap {
border-right: 0;
.conversation {
.conversation--meta {
left: $space-normal;
right: unset;
.unread {
margin-left: unset;
margin-right: auto;
}
}
.assignee-label {
margin-left: 0;
margin-right: var(--space-one);
}
.show-more--button {
margin: unset;
transform: rotate(180deg);
}
}
.search-header--wrap {
.search--input {
text-align: right;
}
.layout-switch__container {
transform: rotate(180deg);
}
}
// Card label
.label-container {
.label {
margin-left: var(--space-smaller);
margin-right: 0;
}
}
// Secondary sidebar toggle button
.toggle-sidebar {
margin-left: 0;
margin-right: var(--space-minus-small);
transform: rotate(180deg);
}
// Bulk actions
.bulk-action__container {
.triangle {
left: var(--triangle-position);
right: unset;
}
.bulk-action__agents {
left: var(--space-small);
right: unset;
}
.labels-container {
left: var(--space-small);
right: unset;
.label-checkbox {
margin: 0 0 0 var(--space-one);
}
}
.actions-container {
left: var(--space-small);
right: unset;
}
.bulk-action__teams {
left: var(--space-small);
right: unset;
}
}
}
// Contact notes
.card.note-wrap {
.time-stamp {
unicode-bidi: plaintext;
}
}
// Notification panel
.notification-wrap {
left: 0;
right: var(--space-jumbo);
.action-button {
margin-left: var(--space-small);
margin-right: 0;
}
.notification-content--wrap {
margin-left: 0;
margin-right: var(--space-small);
}
}
// Help center
.article-container .row--article-block {
td:last-child {
direction: initial;
}
}
// scss-lint:disable SelectorDepth
.container .header-wrap .header-left-wrap .header-left-wrap > .page-title {
margin-right: var(--space-small);
}
.portal-container .container {
margin-left: unset !important;
margin-right: var(--space-small);
.configuration-items--wrap {
margin-left: var(--space-mega);
margin-right: unset !important;
}
thead th {
padding-left: var(--space-small);
padding-right: 0;
}
tbody td {
padding-left: var(--space-small);
padding-right: 0;
}
}
.portal-popover__container .portal {
.actions-container {
margin-left: unset;
margin-right: var(--space-one);
}
}
.edit-article--container {
.header-right--wrap {
.button-group .button:first-child {
border-bottom-left-radius: 0;
border-bottom-right-radius: var(--border-radius-normal);
border-top-left-radius: 0;
border-top-right-radius: var(--border-radius-normal);
}
.button-group .button:last-child {
border-bottom-left-radius: var(--border-radius-normal);
border-bottom-right-radius: 0;
border-top-left-radius: var(--border-radius-normal);
border-top-right-radius: 0;
}
}
.header-left--wrap {
.back-button {
direction: initial;
}
}
.article--buttons {
.dropdown-pane {
left: 0;
position: absolute;
right: unset;
}
}
.sidebar-button {
transform: rotate(180deg);
}
}
.article-settings--container {
border-left: 0;
border-right: 1px solid var(--color-border-light);
flex-direction: row-reverse;
margin-left: 0;
margin-right: var(--space-normal);
padding-left: 0;
padding-right: var(--space-normal);
}
.category-list--container .header-left--wrap {
direction: initial;
justify-content: flex-end;
}
// Toggle switch
.toggle-button {
&.small {
span {
&.active {
transform: translate(var(--space-minus-small), var(--space-zero));
}
}
}
span {
--minus-space-one-point-five: -1.5rem;
&.active {
transform: translate(
var(--minus-space-one-point-five),
var(--space-zero)
);
}
}
}
// Widget builder
.widget-builder-container .widget-preview {
direction: initial;
}
// Modal
.modal-container {
text-align: right;
.modal-footer {
button {
margin-left: 0;
margin-right: var(--space-small);
}
}
}
// Other changes
.account-selector--wrap {
direction: initial;
}
.inbox--name .inbox--icon {
margin-left: var(--space-micro);
margin-right: 0;
}
.colorpicker--chrome {
direction: initial;
}
.mention--box {
direction: initial;
}
.contact--details .contact--bio {
direction: ltr;
}
.merge-contacts .child-contact-wrap {
direction: ltr;
}
.contact--form .input-group {
direction: initial;
}
// scss-lint:disable QualifyingElement
.dropdown-menu--header > span.title {
text-align: right;
}
}

View File

@ -1,7 +1,3 @@
.margin-right-small {
margin-right: var(--space-small);
}
.margin-bottom-small { .margin-bottom-small {
margin-bottom: var(--space-small); margin-bottom: var(--space-small);
} }
@ -14,6 +10,10 @@
margin-left: var(--space-minus-slab); margin-left: var(--space-minus-slab);
} }
.margin-right-minus-slab {
margin-right: var(--space-minus-slab);
}
.fs-small { .fs-small {
font-size: var(--font-size-small); font-size: var(--font-size-small);
} }
@ -75,3 +75,9 @@
align-items: center; align-items: center;
display: flex; display: flex;
} }
.button--fixed-top {
position: fixed;
right: var(--space-small);
top: var(--space-small);
}

View File

@ -41,6 +41,7 @@
@import 'layout'; @import 'layout';
@import 'animations'; @import 'animations';
@import 'foundation-custom'; @import 'foundation-custom';
@import 'rtl';
@import 'widgets/buttons'; @import 'widgets/buttons';
@import 'widgets/conv-header'; @import 'widgets/conv-header';

View File

@ -48,7 +48,7 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
width: 100%; width: fit-content;
} }
p { p {
@ -169,7 +169,7 @@
} }
.multiselect-wrap--small { .multiselect-wrap--small {
$multiselect-height: 3.8rem; $multiselect-height: 4.0rem;
.multiselect__tags, .multiselect__tags,
.multiselect__input { .multiselect__input {

View File

@ -9,7 +9,6 @@
.integration--image { .integration--image {
display: flex; display: flex;
height: 10rem; height: 10rem;
margin-right: $space-normal;
width: 10rem; width: 10rem;
img { img {
@ -18,12 +17,15 @@
} }
} }
.integration--title { .integration--type {
font-size: $font-size-large; display: flex;
flex-direction: column;
justify-content: center;
margin: 0 var(--space-normal);
} }
.integration--description { .integration--title {
padding-right: $space-medium; font-size: var(--font-size-large);
} }
.button-wrap { .button-wrap {

View File

@ -50,22 +50,22 @@ $default-button-height: 4.0rem;
&.secondary { &.secondary {
border-color: var(--s-200); border-color: var(--s-200);
color: var(--s-700) color: var(--s-700);
} }
&.success { &.success {
border-color: var(--s-200); border-color: var(--s-200);
color: var(--g-700) color: var(--g-700);
} }
&.alert { &.alert {
border-color: var(--s-200); border-color: var(--s-200);
color: var(--r-700) color: var(--r-700);
} }
&.warning { &.warning {
border-color: var(--s-200); border-color: var(--s-200);
color: var(--y-700) color: var(--y-700);
} }
&:hover { &:hover {
@ -116,19 +116,19 @@ $default-button-height: 4.0rem;
color: var(--w-700); color: var(--w-700);
&.secondary { &.secondary {
color: var(--s-700) color: var(--s-700);
} }
&.success { &.success {
color: var(--g-700) color: var(--g-700);
} }
&.alert { &.alert {
color: var(--r-700) color: var(--r-700);
} }
&.warning { &.warning {
color: var(--y-700) color: var(--y-700);
} }
&:hover { &:hover {
@ -205,11 +205,3 @@ $default-button-height: 4.0rem;
} }
} }
// @TDOD move to utility file
.button--fixed-right-top {
position: fixed;
right: $space-small;
top: $space-small;
}

View File

@ -47,7 +47,7 @@ $resolve-button-width: 13.2rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
margin-left: $space-slab; margin-left: var(--space-small);
min-width: 0; min-width: 0;
} }
} }

View File

@ -13,18 +13,18 @@
@include flex; @include flex;
@include flex-shrink; @include flex-shrink;
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
border-left: $space-micro solid transparent; border-left: var(--space-micro) solid transparent;
border-top: 1px solid transparent; border-top: 1px solid transparent;
cursor: pointer; cursor: pointer;
padding: 0 0 0 $space-normal; padding: 0 var(--space-normal);
position: relative; position: relative;
&.active { &.active {
animation: left-shift-animation 0.25s $swift-ease-out-function; animation: left-shift-animation 0.25s $swift-ease-out-function;
background: $color-background; background: var(--color-background);
border-bottom-color: $color-border-light; border-bottom-color: var(--color-border-light);
border-left-color: $color-woot; border-left-color: var(--color-woot);
border-top-color: $color-border-light; border-top-color: var(--color-border-light);
.conversation--details { .conversation--details {
border-top-color: transparent; border-top-color: transparent;
@ -43,7 +43,7 @@
&:last-child { &:last-child {
.conversation--details { .conversation--details {
border-bottom-color: $color-border-light; border-bottom-color: var(--color-border-light);
} }
} }
@ -51,33 +51,32 @@
@include border-light-bottom; @include border-light-bottom;
@include border-light-top; @include border-light-top;
border-bottom-color: transparent; border-bottom-color: transparent;
margin: 0 0 0 $space-one; padding: var(--space-slab) 0;
padding: $space-slab 0;
} }
.conversation--user { .conversation--user {
font-size: $font-size-small; font-size: var(--font-size-small);
margin-bottom: 0; margin: 0 var(--space-small);
text-transform: capitalize; text-transform: capitalize;
.label { .label {
left: $space-micro; left: var(--space-one);
max-width: $space-jumbo; max-width: var(--space-jumbo);
overflow: hidden; overflow: hidden;
position: relative; position: relative;
text-overflow: ellipsis; text-overflow: ellipsis;
top: $space-micro; top: var(--space-one);
white-space: nowrap; white-space: nowrap;
} }
} }
.conversation--message { .conversation--message {
color: $color-body; color: var(--color-body);
font-size: $font-size-small; font-size: var(--font-size-small);
font-weight: $font-weight-normal; font-weight: var(--font-weight-normal);
height: $space-medium; height: var(--space-medium);
line-height: $space-medium; line-height: var(--space-medium);
margin: 0; margin: 0 var(--space-small);
max-width: 96%; max-width: 96%;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -89,32 +88,31 @@
@include flex; @include flex;
flex-direction: column; flex-direction: column;
position: absolute; position: absolute;
right: $space-normal; right: var(--space-normal);
top: $space-normal; top: var(--space-normal);
.unread { .unread {
$unread-size: $space-normal;
@include round-corner; @include round-corner;
@include light-shadow; @include light-shadow;
background: darken($success-color, 3%); background: darken($success-color, 3%);
color: $color-white; color: var(--white);
display: none; display: none;
font-size: $font-size-micro; font-size: var(--font-size-micro);
font-weight: $font-weight-black; font-weight: var(--font-weight-black);
height: $unread-size; height: var(--space-normal);
line-height: $unread-size; line-height: var(--space-normal);
margin-left: auto; margin-left: auto;
margin-top: $space-smaller; margin-top: var(--space-smaller);
min-width: $unread-size; min-width: var(--space-normal);
padding: 0 $space-smaller; padding: 0 var(--space-smaller);
text-align: center; text-align: center;
} }
.timestamp { .timestamp {
color: $dark-gray; color: $dark-gray;
font-size: $font-size-micro; font-size: var(--font-size-micro);
font-weight: $font-weight-normal; font-weight: var(--font-weight-normal);
line-height: $space-normal; line-height: var(--space-normal);
margin-left: auto; margin-left: auto;
} }
} }
@ -125,11 +123,11 @@
} }
.conversation--message { .conversation--message {
font-weight: $font-weight-bold; font-weight: var(--font-weight-bold);
} }
.conversation--user { .conversation--user {
font-weight: $font-weight-bold; font-weight: var(--font-weight-bold);
} }
} }

View File

@ -67,6 +67,7 @@
.conversations-list-wrap { .conversations-list-wrap {
@include flex; @include flex;
border-right: 1px solid var(--s-50);
flex-direction: column; flex-direction: column;
.load-more-conversations { .load-more-conversations {
@ -81,8 +82,8 @@
} }
.conversations-list { .conversations-list {
overflow-y: auto;
flex: 1 1; flex: 1 1;
overflow-y: auto;
@include breakpoint(large up) { @include breakpoint(large up) {
@include scroll-on-hover; @include scroll-on-hover;
@ -93,22 +94,10 @@
@include flex; @include flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: $zero $zero $space-micro; padding: 0 var(--space-normal);
.page-title { .page-title {
margin-bottom: $zero; margin-bottom: 0;
margin-left: $space-normal;
}
.status--filter {
background-color: $color-background-light;
border: 1px solid $color-border;
float: right;
font-size: $font-size-mini;
height: $space-medium;
margin: 0;
padding: $zero $space-medium $zero $space-normal;
width: auto;
} }
} }
@ -391,14 +380,6 @@
text-decoration: underline; text-decoration: underline;
} }
blockquote {
border-left-color: var(--s-300);
p {
color: var(--s-300);
}
}
p:last-child { p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
@ -419,19 +400,8 @@
text-decoration: underline; text-decoration: underline;
} }
blockquote {
border-left-color: var(--w-100);
p {
color: var(--w-100);
}
}
pre code {
background: var(--color-background);
}
p:last-child { p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
} }

View File

@ -3,23 +3,23 @@
cursor: pointer; cursor: pointer;
margin: 0; margin: 0;
padding: $space-normal $space-small $space-normal $space-two; padding: var(--space-normal);
&.active { &.active {
@include custom-border-top(3px, $color-woot); @include custom-border-top(3px, var(--color-woot));
@include background-white; @include background-white;
.heading, .heading,
.metric { .metric {
color: $color-woot; color: var(--color-woot);
} }
} }
.heading { .heading {
align-items: center; align-items: center;
color: $color-heading; color: var(--color-heading);
display: flex; display: flex;
font-size: $font-size-small; font-size: var(--font-size-small);
font-weight: $font-weight-bold; font-weight: var(--font-weight-bold);
margin: 0; margin: 0;
} }
@ -29,19 +29,19 @@
} }
.metric-wrap { .metric-wrap {
align-items: baseline; align-items: center;
display: flex; display: flex;
} }
.metric { .metric {
font-size: $font-size-big; font-size: var(--font-size-big);
font-weight: $font-weight-feather; font-weight: var(--font-weight-feather);
margin-top: $space-smaller; margin-top: var(--space-smaller);
} }
.metric-trend { .metric-trend {
font-size: $font-size-small; font-size: var(--font-size-small);
margin-left: $space-small; margin: 0 var(--space-small);
} }
.metric-up { .metric-up {
@ -53,7 +53,7 @@
} }
.desc { .desc {
font-size: $font-size-small; font-size: var(--font-size-small);
margin: 0; margin: 0;
text-transform: capitalize; text-transform: capitalize;
} }
@ -62,8 +62,8 @@
.report-bar { .report-bar {
@include background-white; @include background-white;
@include border-light; @include border-light;
margin: -1px $zero; margin: var(--space-minus-micro) 0;
padding: $space-small $space-medium; padding: var(--space-small) var(--space-medium);
.chart-container { .chart-container {
@include flex; @include flex;
@ -76,12 +76,12 @@
.empty-state { .empty-state {
color: $color-gray; color: $color-gray;
font-size: $font-size-default; font-size: var(--font-size-default);
margin: $space-jumbo; margin: var(--space-jumbo);
} }
.business-hours { .business-hours {
margin: $space-normal; margin: var(--space-normal);
text-align: center; text-align: center;
} }
} }

View File

@ -1,11 +1,3 @@
.date-picker {
margin-left: var(--space-smaller);
}
.margin-left-small {
margin-left: var(--space-smaller);
}
.reports-option__rounded--item { .reports-option__rounded--item {
border-radius: 100%; border-radius: 100%;
height: var(--space-two); height: var(--space-two);
@ -26,17 +18,21 @@
display: flex; display: flex;
} }
.reports-option__title {
margin: 0 var(--space-small);
}
.business-hours { .business-hours {
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: end; justify-content: flex-start;
margin-bottom: var(--space-normal);
margin-left: auto; margin-left: auto;
padding-right: var(--space-normal); padding-right: var(--space-normal);
} }
.business-hours-text { .business-hours-text {
font-size: var(--font-size-small); font-size: var(--font-size-small);
margin: 0 var(--space-small);
} }
.switch { .switch {

View File

@ -1,7 +1,7 @@
.side-menu { .side-menu {
i { i {
margin-right: $space-smaller; margin-right: var(--space-smaller);
min-width: $space-two; min-width: var(--space-two);
} }
} }
@ -18,14 +18,14 @@
.nested { .nested {
a { a {
font-size: $font-size-small; font-size: var(--font-size-small);
margin-bottom: $space-micro; margin-bottom: var(--space-micro);
margin-top: $space-micro; margin-top: var(--space-micro);
.inbox-icon { .inbox-icon {
display: inline-block; display: inline-block;
margin-right: $space-micro; margin-right: var(--space-micro);
min-width: $space-normal; min-width: var(--space-normal);
text-align: center; text-align: center;
} }
} }
@ -38,7 +38,8 @@
@include space-between-column; @include space-between-column;
@include border-normal-top; @include border-normal-top;
flex-direction: column; flex-direction: column;
padding: $space-one $space-normal $space-one $space-one; padding: var(--space-one) var(--space-normal) var(--space-one)
var(--space-one);
position: relative; position: relative;
&:hover { &:hover {
@ -60,14 +61,18 @@
.hamburger--menu { .hamburger--menu {
cursor: pointer; cursor: pointer;
display: block; display: block;
margin-right: $space-normal; margin-right: var(--space-normal);
} }
.header--icon { .header--icon {
display: block; display: block;
margin-right: $space-normal; margin: 0 var(--space-small) 0 var(--space-smaller);
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
display: none; display: none;
} }
} }
.header-title {
margin: 0 var(--space-small);
}

View File

@ -12,7 +12,7 @@
border-top-width: 0; border-top-width: 0;
display: flex; display: flex;
min-width: var(--space-mega); min-width: var(--space-mega);
padding: $zero $space-normal; padding: 0 var(--space-normal);
} }
.tabs--with-scroll { .tabs--with-scroll {
@ -37,26 +37,26 @@
.tabs-title { .tabs-title {
a { a {
font-size: $font-size-default; font-size: var(--font-size-default);
font-weight: $font-weight-medium; font-weight: var(--font-weight-medium);
padding-bottom: $space-slab; padding-bottom: var(--space-slab);
padding-top: $space-slab; padding-top: var(--space-slab);
} }
} }
} }
.tabs-title { .tabs-title {
flex-shrink: 0; flex-shrink: 0;
margin: $zero $space-slab; margin: 0 var(--space-small);
.badge { .badge {
background: $color-background; background: var(--color-background);
border-radius: $space-small; border-radius: var(--space-small);
color: $color-gray; color: var(--color-gray);
font-size: $font-size-micro; font-size: var(--font-size-micro);
font-weight: $font-weight-black; font-weight: var(--font-weight-black);
margin-left: $space-smaller; margin: 0 var(--space-smaller);
padding: $space-smaller; padding: var(--space-smaller);
} }
&:first-child { &:first-child {
@ -80,7 +80,7 @@
color: $medium-gray; color: $medium-gray;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
font-size: $font-size-small; font-size: var(--font-size-small);
position: relative; position: relative;
top: 1px; top: 1px;
transition: border-color 0.15s $swift-ease-out-function; transition: border-color 0.15s $swift-ease-out-function;
@ -88,13 +88,13 @@
&.is-active { &.is-active {
a { a {
border-bottom-color: $color-woot; border-bottom-color: var(--color-woot);
color: $color-woot; color: var(--color-woot);
} }
.badge { .badge {
background: $color-extra-light-blue; background: $color-extra-light-blue;
color: $color-woot; color: var(--color-woot);
} }
} }
} }

View File

@ -1,21 +1,22 @@
table { table {
border-spacing: 0; border-spacing: 0;
font-size: $font-size-small; font-size: var(--font-size-small);
thead { thead {
th { th {
font-weight: $font-weight-bold; font-weight: var(--font-weight-bold);
text-align: left;
text-transform: uppercase; text-transform: uppercase;
} }
} }
tbody { tbody {
tr { tr {
border-bottom: 1px solid $color-border-light; border-bottom: 1px solid var(--color-border-light);
} }
td { td {
padding: $space-one $space-small; padding: var(--space-small);
} }
} }
} }
@ -36,7 +37,7 @@ table {
.agent-name { .agent-name {
display: block; display: block;
font-weight: $font-weight-medium; font-weight: var(--font-weight-medium);
text-transform: capitalize; text-transform: capitalize;
} }

View File

@ -53,11 +53,10 @@
<woot-button <woot-button
v-else v-else
v-tooltip.right="$t('FILTER.TOOLTIP_LABEL')" v-tooltip.right="$t('FILTER.TOOLTIP_LABEL')"
variant="clear" variant="smooth"
color-scheme="secondary" color-scheme="secondary"
icon="filter" icon="filter"
size="small" size="tiny"
class="btn-filter"
@click="onToggleAdvanceFiltersModal" @click="onToggleAdvanceFiltersModal"
/> />
</div> </div>
@ -838,10 +837,6 @@ export default {
align-items: center; align-items: center;
} }
.btn-filter {
margin: 0 var(--space-smaller);
}
.filter__applied { .filter__applied {
padding: 0 0 var(--space-slab) 0 !important; padding: 0 0 var(--space-slab) 0 !important;
border-bottom: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border);
@ -850,4 +845,14 @@ export default {
.delete-custom-view__button { .delete-custom-view__button {
margin-right: var(--space-normal); margin-right: var(--space-normal);
} }
.tab--chat-type {
padding: 0 var(--space-normal);
::v-deep {
.tabs {
padding: 0;
}
}
}
</style> </style>

View File

@ -0,0 +1,63 @@
<template>
<div class="text--container">
<woot-button size="small" class=" button--text" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</woot-button>
<woot-button
variant="clear"
size="small"
class="button--visibility"
color-scheme="secondary"
:icon="masked ? 'eye-show' : 'eye-hide'"
@click.prevent="toggleMasked"
/>
<highlightjs v-if="value" :code="masked ? '•'.repeat(10) : value" />
</div>
</template>
<script>
import 'highlight.js/styles/default.css';
import { copyTextToClipboard } from 'shared/helpers/clipboard';
export default {
props: {
value: {
type: String,
default: '',
},
},
data() {
return {
masked: true,
};
},
methods: {
async onCopy(e) {
e.preventDefault();
await copyTextToClipboard(this.value);
bus.$emit('newToastMessage', this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
},
toggleMasked() {
this.masked = !this.masked;
},
},
};
</script>
<style lang="scss" scoped>
.text--container {
position: relative;
text-align: left;
.button--text,
.button--visibility {
margin-top: 0;
position: absolute;
right: 0;
}
.button--visibility {
right: 60px;
}
}
</style>

View File

@ -22,7 +22,6 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.toggle-sidebar { .toggle-sidebar {
margin-right: var(--space-small);
margin-left: var(--space-minus-small); margin-left: var(--space-minus-small);
} }
</style> </style>

View File

@ -181,7 +181,7 @@ export default {
align-items: center; align-items: center;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: var(--space-smaller) 0 var(--space-smaller) var(--space-small); padding: var(--space-smaller);
margin: 0; margin: 0;
.info-wrap { .info-wrap {

View File

@ -26,6 +26,7 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
export default { export default {
data() { data() {
return { showSwitchButton: false }; return { showSwitchButton: false };
@ -69,7 +70,7 @@ export default {
} }
.switch-button { .switch-button {
margin-right: var(--space-small); margin: 0 var(--space-small);
} }
.account-context--switch-group { .account-context--switch-group {

View File

@ -3,7 +3,7 @@
<div <div
v-if="show" v-if="show"
v-on-clickaway="onClickAway" v-on-clickaway="onClickAway"
class="dropdown-pane" class="options-menu dropdown-pane"
:class="{ 'dropdown-pane--open': show }" :class="{ 'dropdown-pane--open': show }"
> >
<availability-status /> <availability-status />
@ -150,10 +150,10 @@ export default {
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.dropdown-pane { .options-menu.dropdown-pane {
left: var(--space-slab); left: var(--space-slab);
bottom: var(--space-larger); bottom: var(--space-larger);
min-width: 22rem; min-width: var(--space-giga);
top: unset; top: unset;
z-index: var(--z-index-low); z-index: var(--z-index-low);
} }

View File

@ -331,11 +331,11 @@ export default {
.beta { .beta {
padding-right: var(--space-smaller) !important; padding-right: var(--space-smaller) !important;
padding-left: var(--space-smaller) !important; padding-left: var(--space-smaller) !important;
margin-left: var(--space-smaller) !important; margin: 0 var(--space-smaller) !important;
display: inline-block; display: inline-block;
font-size: var(--font-size-micro); font-size: var(--font-size-micro);
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
line-height: 18px; line-height: 14px;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 2em; border-radius: 2em;
color: var(--g-800); color: var(--g-800);
@ -348,7 +348,7 @@ export default {
color: var(--s-600); color: var(--s-600);
font-size: var(--font-size-micro); font-size: var(--font-size-micro);
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
margin-left: var(--space-smaller); margin: 0 var(--space-smaller);
padding: var(--space-zero) var(--space-smaller); padding: var(--space-zero) var(--space-smaller);
} }

View File

@ -12,7 +12,7 @@
<a v-else :href="href" :style="anchorStyle">{{ title }}</a> <a v-else :href="href" :style="anchorStyle">{{ title }}</a>
<button <button
v-if="showClose" v-if="showClose"
class="label-close--button " class="label-close--button"
:style="{ color: textColor }" :style="{ color: textColor }"
@click="onClick" @click="onClick"
> >
@ -108,6 +108,7 @@ export default {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
gap: var(--space-smaller);
margin-right: var(--space-smaller); margin-right: var(--space-smaller);
margin-bottom: var(--space-smaller); margin-bottom: var(--space-smaller);
padding: var(--space-smaller); padding: var(--space-smaller);
@ -126,9 +127,6 @@ export default {
.label--icon { .label--icon {
cursor: pointer; cursor: pointer;
} }
.label-color-dot {
margin-right: var(--space-smaller);
}
&.small .label--icon, &.small .label--icon,
&.small .close--icon { &.small .close--icon {
@ -209,7 +207,6 @@ export default {
.label-close--button { .label-close--button {
color: var(--s-800); color: var(--s-800);
margin-bottom: var(--space-minus-micro); margin-bottom: var(--space-minus-micro);
margin-left: var(--space-smaller);
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
cursor: pointer; cursor: pointer;
@ -232,7 +229,6 @@ export default {
width: var(--space-slab); width: var(--space-slab);
height: var(--space-slab); height: var(--space-slab);
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
margin-right: var(--space-smaller);
box-shadow: var(--shadow-small); box-shadow: var(--shadow-small);
} }
.label.small .label-color-dot { .label.small .label-color-dot {

View File

@ -1,5 +1,5 @@
<template> <template>
<button class="back-button" @click.capture="goBack"> <button class="settings back-button" @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>
@ -29,4 +29,3 @@ export default {
}, },
}; };
</script> </script>
<style scoped></style>

View File

@ -25,13 +25,15 @@ export default {
</script> </script>
<style scoped> <style scoped>
.inbox--name { .inbox--name {
align-items: center;
display: inline-flex; display: inline-flex;
padding: var(--space-micro) 0; padding: var(--space-micro) 0;
line-height: var(--space-slab); line-height: var(--space-slab);
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
background: none; background: none;
color: var(--s-500); color: var(--s-600);
font-size: var(--font-size-mini); font-size: var(--font-size-mini);
margin: 0 var(--space-one);
} }
.inbox--icon { .inbox--icon {

View File

@ -24,7 +24,7 @@
<fluent-icon <fluent-icon
icon="chevron-left" icon="chevron-left"
size="18" size="18"
class="margin-left-minus-slab" :class="pageFooterIconClass"
/> />
</woot-button> </woot-button>
<woot-button <woot-button
@ -65,7 +65,7 @@
<fluent-icon <fluent-icon
icon="chevron-right" icon="chevron-right"
size="18" size="18"
class="margin-left-minus-slab" :class="pageFooterIconClass"
/> />
</woot-button> </woot-button>
</div> </div>
@ -74,8 +74,11 @@
</template> </template>
<script> <script>
import rtlMixin from 'shared/mixins/rtlMixin';
export default { export default {
components: {}, components: {},
mixins: [rtlMixin],
props: { props: {
currentPage: { currentPage: {
type: Number, type: Number,
@ -91,6 +94,11 @@ export default {
}, },
}, },
computed: { computed: {
pageFooterIconClass() {
return this.isRTLView
? 'margin-right-minus-slab'
: 'margin-left-minus-slab';
},
isFooterVisible() { isFooterVisible() {
return this.totalCount && !(this.firstIndex > this.totalCount); return this.totalCount && !(this.firstIndex > this.totalCount);
}, },

View File

@ -6,7 +6,7 @@
:username="user.name" :username="user.name"
:status="user.availability_status" :status="user.availability_status"
/> />
<h6 class="text-block-title text-truncate text-capitalize"> <h6 class="text-block-title user-name text-truncate text-capitalize">
{{ user.name }} {{ user.name }}
</h6> </h6>
</div> </div>
@ -38,11 +38,7 @@ export default {
text-align: left; text-align: left;
.user-name { .user-name {
margin: 0; margin: 0 var(--space-small);
text-transform: capitalize;
}
.user-thumbnail-box {
margin-right: var(--space-small);
} }
} }
</style> </style>

View File

@ -44,3 +44,14 @@ export default {
}, },
}; };
</script> </script>
<style lang="scss" scoped>
.status--filter {
background-color: var(--color-background-light);
border: 1px solid var(--color-border);
font-size: var(--font-size-mini);
height: var(--space-medium);
margin: 0 var(--space-smaller);
padding: 0 var(--space-medium) 0 var(--space-small);
width: auto;
}
</style>

View File

@ -1,7 +1,7 @@
<template> <template>
<div <div
class="conversation-details-wrap" class="conversation-details-wrap"
:class="{ 'with-border-left': !isOnExpandedLayout }" :class="{ 'with-border-right': !isOnExpandedLayout }"
> >
<conversation-header <conversation-header
v-if="currentChat.id" v-if="currentChat.id"
@ -144,8 +144,8 @@ export default {
width: 100%; width: 100%;
background: var(--color-background-light); background: var(--color-background-light);
&.with-border-left { &.with-border-right {
border-left: 1px solid var(--color-border); border-right: 1px solid var(--color-border);
} }
} }
@ -164,6 +164,7 @@ export default {
} }
.conversation-sidebar-wrap { .conversation-sidebar-wrap {
border-right: 1px solid var(--color-border);
height: auto; height: auto;
flex: 0 0; flex: 0 0;
z-index: var(--z-index-low); z-index: var(--z-index-low);

View File

@ -419,7 +419,6 @@ export default {
.conversation--metadata { .conversation--metadata {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding-right: var(--space-normal);
.label { .label {
background: none; background: none;
@ -432,6 +431,7 @@ export default {
.assignee-label { .assignee-label {
display: inline-flex; display: inline-flex;
margin-left: var(--space-small);
max-width: 50%; max-width: 50%;
} }
} }

View File

@ -10,30 +10,30 @@
:status="currentContact.availability_status" :status="currentContact.availability_status"
/> />
<div class="user--profile__meta"> <div class="user--profile__meta">
<h3 class="user--name text-truncate"> <woot-button
<span class="margin-right-smaller">{{ currentContact.name }}</span> variant="link"
<fluent-icon color-scheme="secondary"
v-if="!isHMACVerified" class="text-truncate"
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')" @click.prevent="$emit('contact-panel-toggle')"
size="14" >
class="hmac-warning__icon" <h3 class="sub-block-title user--name text-truncate">
icon="warning" <span>{{ currentContact.name }}</span>
/> <fluent-icon
</h3> v-if="!isHMACVerified"
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
size="14"
class="hmac-warning__icon"
icon="warning"
/>
</h3>
</woot-button>
<div class="conversation--header--actions text-truncate"> <div class="conversation--header--actions text-truncate">
<inbox-name <inbox-name v-if="hasMultipleInboxes" :inbox="inbox" />
v-if="hasMultipleInboxes" <span v-if="isSnoozed" class="snoozed--display-text">
:inbox="inbox"
class="margin-right-small"
/>
<span
v-if="isSnoozed"
class="snoozed--display-text margin-right-small"
>
{{ snoozedDisplayText }} {{ snoozedDisplayText }}
</span> </span>
<woot-button <woot-button
class="user--profile__button margin-right-small" class="user--profile__button"
size="small" size="small"
variant="link" variant="link"
@click="$emit('contact-panel-toggle')" @click="$emit('contact-panel-toggle')"
@ -204,17 +204,21 @@ export default {
.user--name { .user--name {
display: inline-block; display: inline-block;
font-size: var(--font-size-medium); line-height: 1.2;
line-height: 1.3;
margin: 0;
text-transform: capitalize; text-transform: capitalize;
width: 100%; margin: 0;
padding: 0;
} }
.conversation--header--actions { .conversation--header--actions {
align-items: center; align-items: center;
display: flex; display: flex;
font-size: var(--font-size-mini); font-size: var(--font-size-mini);
gap: var(--space-small);
::v-deep .inbox--name {
margin: 0;
}
.user--profile__button { .user--profile__button {
padding: 0; padding: 0;
@ -228,5 +232,6 @@ export default {
.hmac-warning__icon { .hmac-warning__icon {
color: var(--y-600); color: var(--y-600);
margin: 0 var(--space-micro);
} }
</style> </style>

View File

@ -243,24 +243,20 @@ export default {
return this.contentAttributes.translations || {}; return this.contentAttributes.translations || {};
}, },
displayQuotedButton() { displayQuotedButton() {
if (!this.isIncoming) {
return false;
}
if (this.emailMessageContent.includes('<blockquote')) { if (this.emailMessageContent.includes('<blockquote')) {
return true; return true;
} }
if (!this.isIncoming) {
return false;
}
return false; return false;
}, },
translationsAvailable() { translationsAvailable() {
return !!Object.keys(this.translations).length; return !!Object.keys(this.translations).length;
}, },
message() { message() {
if (this.contentType === 'input_csat') {
return this.$t('CONVERSATION.CSAT_REPLY_MESSAGE');
}
// If the message is an email, emailMessageContent would be present // If the message is an email, emailMessageContent would be present
// In that case, we would use letter package to render the email // In that case, we would use letter package to render the email
if (this.emailMessageContent && this.isIncoming) { if (this.emailMessageContent && this.isIncoming) {
@ -278,6 +274,11 @@ export default {
}, },
} }
); );
if (this.contentType === 'input_csat') {
return this.$t('CONVERSATION.CSAT_REPLY_MESSAGE') + botMessageContent;
}
return ( return (
this.formatMessage( this.formatMessage(
this.data.content, this.data.content,
@ -659,4 +660,71 @@ li.right {
.context-menu { .context-menu {
position: relative; position: relative;
} }
/* Markdown styling */
.bubble .text-content {
p code {
background-color: var(--s-75);
display: inline-block;
line-height: 1;
border-radius: var(--border-radius-small);
padding: var(--space-smaller);
}
pre {
background-color: var(--s-75);
border-color: var(--s-75);
color: var(--s-800);
border-radius: var(--border-radius-normal);
padding: var(--space-small);
margin-top: var(--space-smaller);
margin-bottom: var(--space-small);
display: block;
line-height: 1.7;
white-space: pre-wrap;
code {
background-color: transparent;
color: var(--s-800);
padding: 0;
}
}
blockquote {
border-left: var(--space-micro) solid var(--s-75);
color: var(--s-800);
padding: var(--space-smaller) var(--space-small);
margin: var(--space-smaller) 0;
padding: var(--space-small) var(--space-small) 0 var(--space-normal);
}
}
.right .bubble .text-content {
p code {
background-color: var(--w-600);
color: var(--white);
}
pre {
background-color: var(--w-800);
border-color: var(--w-700);
color: var(--white);
code {
background-color: transparent;
color: var(--white);
}
}
blockquote {
border-left: var(--space-micro) solid var(--w-400);
color: var(--white);
p {
color: var(--w-75);
}
}
}
</style> </style>

View File

@ -27,7 +27,7 @@
<emoji-input <emoji-input
v-if="showEmojiPicker" v-if="showEmojiPicker"
v-on-clickaway="hideEmojiPicker" v-on-clickaway="hideEmojiPicker"
:class="emojiDialogClassOnExpanedLayout" :class="emojiDialogClassOnExpandedLayoutAndRTLView"
:on-click="emojiOnClick" :on-click="emojiOnClick"
/> />
<reply-email-head <reply-email-head
@ -179,6 +179,7 @@ import { trimContent, debounce } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants'; import wootConstants from 'dashboard/constants';
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';
const EmojiInput = () => import('shared/components/emoji/EmojiInput'); const EmojiInput = () => import('shared/components/emoji/EmojiInput');
@ -202,6 +203,7 @@ export default {
uiSettingsMixin, uiSettingsMixin,
alertMixin, alertMixin,
messageFormatterMixin, messageFormatterMixin,
rtlMixin,
], ],
props: { props: {
selectedTweet: { selectedTweet: {
@ -422,10 +424,14 @@ export default {
} = this.uiSettings; } = this.uiSettings;
return conversationDisplayType !== CONDENSED; return conversationDisplayType !== CONDENSED;
}, },
emojiDialogClassOnExpanedLayout() { emojiDialogClassOnExpandedLayoutAndRTLView() {
return this.isOnExpandedLayout || this.popoutReplyBox if (this.isOnExpandedLayout || this.popoutReplyBox) {
? 'emoji-dialog--expanded' return 'emoji-dialog--expanded';
: ''; }
if (this.isRTLView) {
return 'emoji-dialog--rtl';
}
return '';
}, },
replyToUserLength() { replyToUserLength() {
const selectedTweet = this.selectedTweet || {}; const selectedTweet = this.selectedTweet || {};
@ -881,6 +887,11 @@ export default {
toggleTyping(status) { toggleTyping(status) {
const conversationId = this.currentChat.id; const conversationId = this.currentChat.id;
const isPrivate = this.isPrivate; const isPrivate = this.isPrivate;
if (!conversationId) {
return;
}
this.$store.dispatch('conversationTypingStatus/toggleTyping', { this.$store.dispatch('conversationTypingStatus/toggleTyping', {
status, status,
conversationId, conversationId,
@ -968,15 +979,9 @@ export default {
}, },
getMessagePayloadForWhatsapp(message) { getMessagePayloadForWhatsapp(message) {
const multipleMessagePayload = []; const multipleMessagePayload = [];
const messagePayload = {
conversationId: this.currentChat.id,
message,
private: false,
};
multipleMessagePayload.push(messagePayload);
if (this.attachedFiles && this.attachedFiles.length) { if (this.attachedFiles && this.attachedFiles.length) {
let caption = message;
this.attachedFiles.forEach(attachment => { this.attachedFiles.forEach(attachment => {
const attachedFile = this.globalConfig.directUploadsEnabled const attachedFile = this.globalConfig.directUploadsEnabled
? attachment.blobSignedId ? attachment.blobSignedId
@ -985,10 +990,18 @@ export default {
conversationId: this.currentChat.id, conversationId: this.currentChat.id,
files: [attachedFile], files: [attachedFile],
private: false, private: false,
message: '', message: caption,
}; };
multipleMessagePayload.push(attachmentPayload); multipleMessagePayload.push(attachmentPayload);
caption = '';
}); });
} else {
const messagePayload = {
conversationId: this.currentChat.id,
message,
private: false,
};
multipleMessagePayload.push(messagePayload);
} }
return multipleMessagePayload; return multipleMessagePayload;
@ -1096,7 +1109,7 @@ export default {
.emoji-dialog { .emoji-dialog {
top: unset; top: unset;
bottom: var(--space-normal); bottom: -40px;
left: -320px; left: -320px;
right: unset; right: unset;
@ -1107,6 +1120,19 @@ export default {
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08)); filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
} }
} }
.emoji-dialog--rtl {
left: unset;
right: -320px;
&::before {
left: var(--space-minus-normal);
transform: rotate(90deg);
right: 0;
bottom: var(--space-small);
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
}
}
.emoji-dialog--expanded { .emoji-dialog--expanded {
left: unset; left: unset;
bottom: var(--space-jumbo); bottom: var(--space-jumbo);

View File

@ -739,4 +739,9 @@ export const getLanguageName = (languageCode = '') => {
return languageObj.name || ''; return languageObj.name || '';
}; };
export const getLanguageDirection = (languageCode = '') => {
const rtlLanguageIds = ['ar', 'as', 'fa', 'he', 'ku', 'ur'];
return rtlLanguageIds.includes(languageCode);
};
export default languages; export default languages;

View File

@ -1,4 +1,4 @@
import { getLanguageName } from '../languages'; import { getLanguageName, getLanguageDirection } from '../languages';
describe('#getLanguageName', () => { describe('#getLanguageName', () => {
it('Returns correct language name', () => { it('Returns correct language name', () => {
@ -8,3 +8,10 @@ describe('#getLanguageName', () => {
expect(getLanguageName('')).toEqual(''); expect(getLanguageName('')).toEqual('');
}); });
}); });
describe('#getLanguageDirection', () => {
it('Returns correct language direction', () => {
expect(getLanguageDirection('es')).toEqual(false);
expect(getLanguageDirection('ar')).toEqual(true);
});
});

View File

@ -2,14 +2,14 @@
<div <div
class="message-text__wrap" class="message-text__wrap"
:class="{ :class="{
'show--quoted': showQuotedContent, 'show--quoted': isQuotedContentPresent,
'hide--quoted': !showQuotedContent, 'hide--quoted': !isQuotedContentPresent,
}" }"
> >
<div v-if="!isEmail" v-dompurify-html="message" class="text-content" /> <div v-if="!isEmail" v-dompurify-html="message" class="text-content" />
<letter v-else class="text-content" :html="message" /> <letter v-else class="text-content" :html="message" />
<button <button
v-if="displayQuotedButton" v-if="showQuoteToggle"
class="quoted-text--button" class="quoted-text--button"
@click="toggleQuotedContent" @click="toggleQuotedContent"
> >
@ -49,6 +49,20 @@ export default {
showQuotedContent: false, showQuotedContent: false,
}; };
}, },
computed: {
isQuotedContentPresent() {
if (!this.isEmail) {
return this.message.includes('<blockquote');
}
return this.showQuotedContent;
},
showQuoteToggle() {
if (!this.isEmail) {
return false;
}
return this.displayQuotedButton;
},
},
methods: { methods: {
toggleQuotedContent() { toggleQuotedContent() {
this.showQuotedContent = !this.showQuotedContent; this.showQuotedContent = !this.showQuotedContent;

View File

@ -51,15 +51,11 @@ export default {
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
overflow: hidden; overflow: hidden;
.menu-label { .menu-label {
margin: 0; margin: 0 var(--space-small);
font-size: var(--font-size-mini); font-size: var(--font-size-mini);
flex-shrink: 0; flex-shrink: 0;
} }
.menu-icon {
margin-right: var(--space-small);
}
&:hover { &:hover {
background-color: var(--w-500); background-color: var(--w-500);
color: var(--white); color: var(--white);
@ -68,7 +64,6 @@ export default {
.agent-thumbnail { .agent-thumbnail {
margin-top: 0 !important; margin-top: 0 !important;
margin-right: var(--space-small);
} }
.label-pill { .label-pill {
@ -77,6 +72,5 @@ export default {
border-radius: var(--border-radius-rounded); border-radius: var(--border-radius-rounded);
border: 1px solid var(--s-50); border: 1px solid var(--s-50);
flex-shrink: 0; flex-shrink: 0;
margin-right: var(--space-small);
} }
</style> </style>

View File

@ -43,13 +43,9 @@ export default {
align-items: center; align-items: center;
.menu-label { .menu-label {
margin: 0; margin: 0 var(--space-small);
font-size: var(--font-size-mini); font-size: var(--font-size-mini);
} }
.menu-icon {
margin-right: var(--space-small);
}
} }
.submenu { .submenu {

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="bulk-action__agents"> <div v-on-clickaway="onCloseAgentList" class="bulk-action__agents">
<div class="triangle" :style="cssVars"> <div class="triangle" :style="cssVars">
<svg height="12" viewBox="0 0 24 12" width="24"> <svg height="12" viewBox="0 0 24 12" width="24">
<path <path
@ -50,7 +50,6 @@
:status="agent.availability_status" :status="agent.availability_status"
:username="agent.name" :username="agent.name"
size="22px" size="22px"
class="margin-right-small"
/> />
<span class="reports-option__title">{{ agent.name }}</span> <span class="reports-option__title">{{ agent.name }}</span>
</div> </div>
@ -127,6 +126,7 @@ export default {
return { return {
query: '', query: '',
selectedAgent: null, selectedAgent: null,
goBackToAgentList: false,
}; };
}, },
computed: { computed: {
@ -170,6 +170,7 @@ export default {
this.$emit('select', this.selectedAgent); this.$emit('select', this.selectedAgent);
}, },
goBack() { goBack() {
this.goBackToAgentList = true;
this.selectedAgent = null; this.selectedAgent = null;
}, },
assignAgent(agent) { assignAgent(agent) {
@ -178,6 +179,12 @@ export default {
onClose() { onClose() {
this.$emit('close'); this.$emit('close');
}, },
onCloseAgentList() {
if (this.selectedAgent === null && !this.goBackToAgentList) {
this.onClose();
}
this.goBackToAgentList = false;
},
}, },
}; };
</script> </script>

View File

@ -184,9 +184,18 @@ export default {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
// For RTL direction view
.app-rtl--wrapper {
.bulk-action__actions {
::v-deep .button--only-icon:last-child {
margin-right: var(--space-smaller);
}
}
}
.bulk-action__container { .bulk-action__container {
border-bottom: 1px solid var(--s-100); border-bottom: 1px solid var(--s-100);
padding: var(--space-normal) var(--space-one); padding: var(--space-normal);
position: relative; position: relative;
} }
@ -195,7 +204,7 @@ export default {
span { span {
font-size: var(--font-size-mini); font-size: var(--font-size-mini);
margin-left: var(--space-smaller); margin: 0 var(--space-smaller);
} }
input[type='checkbox'] { input[type='checkbox'] {

View File

@ -97,7 +97,7 @@ export default {
} }
.label-container { .label-container {
margin-top: var(--space-micro); margin: var(--space-micro) var(--space-small) 0;
} }
.labels-wrap { .labels-wrap {

View File

@ -5,7 +5,7 @@ export const CONVERSATION_EVENTS = Object.freeze({
INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response', INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response',
INSERTED_A_VARIABLE: 'Inserted a variable', INSERTED_A_VARIABLE: 'Inserted a variable',
USED_MENTIONS: 'Used mentions', USED_MENTIONS: 'Used mentions',
SEARCH_CONVERSATION: 'Searched conversations',
APPLY_FILTER: 'Applied filters in the conversation list', APPLY_FILTER: 'Applied filters in the conversation list',
}); });

View File

@ -1,4 +1,5 @@
import AnalyticsHelper from './AnalyticsHelper'; import AnalyticsHelper from './AnalyticsHelper';
import LogRocket from 'logrocket';
import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper'; import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper';
export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER'; export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER';
@ -10,6 +11,12 @@ export const ANALYTICS_RESET = 'ANALYTICS_RESET';
export const initializeAnalyticsEvents = () => { export const initializeAnalyticsEvents = () => {
window.bus.$on(ANALYTICS_IDENTITY, ({ user }) => { window.bus.$on(ANALYTICS_IDENTITY, ({ user }) => {
AnalyticsHelper.identify(user); AnalyticsHelper.identify(user);
if (window.logRocketProjectId) {
LogRocket.identify(user.id, {
email: user.email,
name: user.name,
});
}
}); });
window.bus.$on(ANALYTICS_RESET, () => {}); window.bus.$on(ANALYTICS_RESET, () => {});
}; };

View File

@ -33,6 +33,7 @@ import uk from './locale/uk';
import vi from './locale/vi'; import vi from './locale/vi';
import zh_CN from './locale/zh_CN'; import zh_CN from './locale/zh_CN';
import zh_TW from './locale/zh_TW'; import zh_TW from './locale/zh_TW';
import is from './locale/is';
export default { export default {
ar, ar,
@ -70,4 +71,5 @@ export default {
vi, vi,
zh_CN, zh_CN,
zh_TW, zh_TW,
is,
}; };

View File

@ -35,7 +35,7 @@
"REMOVE_SELECTION": "إزالة التحديد", "REMOVE_SELECTION": "إزالة التحديد",
"DOWNLOAD": "تنزيل", "DOWNLOAD": "تنزيل",
"UNKNOWN_FILE_TYPE": "ملف غير معروف", "UNKNOWN_FILE_TYPE": "ملف غير معروف",
"SAVE_CONTACT": "Save", "SAVE_CONTACT": "حفظ",
"UPLOADING_ATTACHMENTS": "جاري تحميل المرفقات...", "UPLOADING_ATTACHMENTS": "جاري تحميل المرفقات...",
"SUCCESS_DELETE_MESSAGE": "تم حذف الرسالة بنجاح", "SUCCESS_DELETE_MESSAGE": "تم حذف الرسالة بنجاح",
"FAIL_DELETE_MESSSAGE": "تعذر حذف الرسالة! حاول مرة أخرى", "FAIL_DELETE_MESSSAGE": "تعذر حذف الرسالة! حاول مرة أخرى",
@ -43,8 +43,8 @@
"RATING_TITLE": "التقييم", "RATING_TITLE": "التقييم",
"FEEDBACK_TITLE": "الملاحظات", "FEEDBACK_TITLE": "الملاحظات",
"CARD": { "CARD": {
"SHOW_LABELS": "Show labels", "SHOW_LABELS": "إظهار السمات",
"HIDE_LABELS": "Hide labels" "HIDE_LABELS": "إخفاء السمات"
}, },
"HEADER": { "HEADER": {
"RESOLVE_ACTION": "إغلاق المحادثة", "RESOLVE_ACTION": "إغلاق المحادثة",
@ -69,7 +69,7 @@
"CARD_CONTEXT_MENU": { "CARD_CONTEXT_MENU": {
"PENDING": "تحديد كمعلق", "PENDING": "تحديد كمعلق",
"RESOLVED": "تحديد كمحلولة", "RESOLVED": "تحديد كمحلولة",
"MARK_AS_UNREAD": "Mark as unread", "MARK_AS_UNREAD": "وضع علامة كغير مقروء",
"REOPEN": "إعادة فتح المحادثة", "REOPEN": "إعادة فتح المحادثة",
"SNOOZE": { "SNOOZE": {
"TITLE": "غفوة", "TITLE": "غفوة",
@ -135,8 +135,8 @@
} }
}, },
"UNDEFINED_VARIABLES": { "UNDEFINED_VARIABLES": {
"TITLE": "Undefined variables", "TITLE": "متغيرات غير معرفة",
"MESSAGE": "You have {undefinedVariablesCount} undefined variables in your message: {undefinedVariables}. Would you like to send the message anyway?", "MESSAGE": "لديك {undefinedVariablesCount} متغيرات غير محددة في رسالتك: {undefinedVariables}. هل ترغب في إرسال الرسالة على أي حال؟",
"CONFIRM": { "CONFIRM": {
"YES": "إرسال", "YES": "إرسال",
"CANCEL": "إلغاء" "CANCEL": "إلغاء"
@ -165,7 +165,8 @@
"CONTEXT_MENU": { "CONTEXT_MENU": {
"COPY": "نسخ", "COPY": "نسخ",
"DELETE": "حذف", "DELETE": "حذف",
"CREATE_A_CANNED_RESPONSE": "إضافة إلى الردود السريعة" "CREATE_A_CANNED_RESPONSE": "إضافة إلى الردود السريعة",
"TRANSLATE": "ترجم"
} }
}, },
"EMAIL_TRANSCRIPT": { "EMAIL_TRANSCRIPT": {
@ -253,5 +254,29 @@
"BCC": "Bcc", "BCC": "Bcc",
"CC": "Cc", "CC": "Cc",
"SUBJECT": "الموضوع" "SUBJECT": "الموضوع"
},
"CONVERSATION_PARTICIPANTS": {
"SIDEBAR_MENU_TITLE": "شارك",
"SIDEBAR_TITLE": "المشاركون في المحادثة",
"NO_RECORDS_FOUND": "لم يتم العثور على النتائج",
"ADD_PARTICIPANTS": "اختر المشاركين",
"REMANING_PARTICIPANTS_TEXT": "+%{count} أخرى",
"REMANING_PARTICIPANT_TEXT": "+%{count} أخرى",
"TOTAL_PARTICIPANTS_TEXT": "%{count} شخص مشارك.",
"TOTAL_PARTICIPANT_TEXT": "%{count} شخص مشارك.",
"NO_PARTICIPANTS_TEXT": "لا أحد يشارك !.",
"WATCH_CONVERSATION": "الانضمام إلى المحادثة",
"YOU_ARE_WATCHING": "أنت مشترك",
"API": {
"ERROR_MESSAGE": "تعذر التحديث، حاول مرة أخرى!",
"SUCCESS_MESSAGE": "تم تحديث المشاركين!"
}
},
"TRANSLATE_MODAL": {
"TITLE": "عرض المحتوى المترجم",
"DESC": "يمكنك عرض المحتوى المترجم في كل لغة.",
"ORIGINAL_CONTENT": "المحتوى الأصلي",
"TRANSLATED_CONTENT": "المحتوى المترجم",
"NO_TRANSLATIONS_AVAILABLE": "لا توجد ترجمات لهذا المحتوى"
} }
} }

View File

@ -82,6 +82,7 @@
"conversation_creation": "محادثة جديدة", "conversation_creation": "محادثة جديدة",
"conversation_assignment": "تم تعيين المحادثة", "conversation_assignment": "تم تعيين المحادثة",
"assigned_conversation_new_message": "رسالة جديدة", "assigned_conversation_new_message": "رسالة جديدة",
"participating_conversation_new_message": "رسالة جديدة",
"conversation_mention": "إشارة" "conversation_mention": "إشارة"
} }
}, },

View File

@ -28,6 +28,17 @@
"SAVING": "جاري الحفظ...", "SAVING": "جاري الحفظ...",
"SAVED": "تم الحفظ" "SAVED": "تم الحفظ"
}, },
"ARTICLE_EDITOR": {
"IMAGE_UPLOAD": {
"TITLE": "رفع صورة",
"UPLOADING": "جاري الرفع...",
"SUCCESS": "Image uploaded successfully",
"ERROR": "Error while uploading image",
"ERROR_FILE_SIZE": "Image size should be less than {size}MB",
"ERROR_FILE_FORMAT": "Image format should be jpg, jpeg or png",
"ERROR_FILE_DIMENSIONS": "Image dimensions should be less than 2000 x 2000"
}
},
"ARTICLE_SETTINGS": { "ARTICLE_SETTINGS": {
"TITLE": "إعدادات المقالة", "TITLE": "إعدادات المقالة",
"FORM": { "FORM": {

View File

@ -476,7 +476,8 @@
"WHATSAPP_SECTION_TITLE": "مفتاح API", "WHATSAPP_SECTION_TITLE": "مفتاح API",
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key", "WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here", "WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
"WHATSAPP_SECTION_UPDATE_BUTTON": "تحديث" "WHATSAPP_SECTION_UPDATE_BUTTON": "تحديث",
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
}, },
"AUTO_ASSIGNMENT": { "AUTO_ASSIGNMENT": {
"MAX_ASSIGNMENT_LIMIT": "حد الإسناد التلقائي", "MAX_ASSIGNMENT_LIMIT": "حد الإسناد التلقائي",

View File

@ -23,6 +23,7 @@ import { default as _settings } from './settings.json';
import { default as _signup } from './signup.json'; import { default as _signup } from './signup.json';
import { default as _teamsSettings } from './teamsSettings.json'; import { default as _teamsSettings } from './teamsSettings.json';
import { default as _whatsappTemplates } from './whatsappTemplates.json'; import { default as _whatsappTemplates } from './whatsappTemplates.json';
import { default as _helpCenter } from './helpCenter.json';
export default { export default {
..._advancedFilters, ..._advancedFilters,
@ -50,4 +51,5 @@ export default {
..._teamsSettings, ..._teamsSettings,
..._whatsappTemplates, ..._whatsappTemplates,
..._bulkActions, ..._bulkActions,
..._helpCenter,
}; };

View File

@ -14,6 +14,11 @@
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً", "ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً",
"UNAUTH": "اسم المستخدم / كلمة المرور غير صحيحة. الرجاء المحاولة مرة أخرى" "UNAUTH": "اسم المستخدم / كلمة المرور غير صحيحة. الرجاء المحاولة مرة أخرى"
}, },
"OAUTH": {
"GOOGLE_LOGIN": "Login with Google",
"BUSINESS_ACCOUNTS_ONLY": "Please use your company email address to login",
"NO_ACCOUNT_FOUND": "We couldn't find an account for your email address."
},
"FORGOT_PASSWORD": "نسيت كلمة المرور؟", "FORGOT_PASSWORD": "نسيت كلمة المرور؟",
"CREATE_NEW_ACCOUNT": "إنشاء حساب جديد", "CREATE_NEW_ACCOUNT": "إنشاء حساب جديد",
"SUBMIT": "تسجيل الدخول" "SUBMIT": "تسجيل الدخول"

View File

@ -426,6 +426,12 @@
"UNATTENDED": "بدون حضور", "UNATTENDED": "بدون حضور",
"UNASSIGNED": "غير مسند" "UNASSIGNED": "غير مسند"
}, },
"CONVERSATION_HEATMAP": {
"HEADER": "Conversation Traffic",
"NO_CONVERSATIONS": "No conversations",
"CONVERSATION": "%{count} conversation",
"CONVERSATIONS": "%{count} conversations"
},
"AGENT_CONVERSATIONS": { "AGENT_CONVERSATIONS": {
"HEADER": "المحادثات من قبل الوكلاء", "HEADER": "المحادثات من قبل الوكلاء",
"LOADING_MESSAGE": "جاري تحميل مقاييس الوكيل...", "LOADING_MESSAGE": "جاري تحميل مقاييس الوكيل...",
@ -443,5 +449,14 @@
"BUSY": "مشغول", "BUSY": "مشغول",
"OFFLINE": "غير متصل" "OFFLINE": "غير متصل"
} }
},
"DAYS_OF_WEEK": {
"SUNDAY": "Sunday",
"MONDAY": "Monday",
"TUESDAY": "Tuesday",
"WEDNESDAY": "Wednesday",
"THURSDAY": "Thursday",
"FRIDAY": "Friday",
"SATURDAY": "Saturday"
} }
} }

View File

@ -0,0 +1,23 @@
{
"SEARCH": {
"TABS": {
"ALL": "الكل",
"CONTACTS": "جهات الاتصال",
"CONVERSATIONS": "المحادثات",
"MESSAGES": "الرسائل"
},
"SECTION": {
"CONTACTS": "جهات الاتصال",
"CONVERSATIONS": "المحادثات",
"MESSAGES": "الرسائل"
},
"EMPTY_STATE": "No %{item} found for query '%{query}'",
"EMPTY_STATE_FULL": "No results found for query '%{query}'",
"PLACEHOLDER_KEYBINDING": "/ to focus",
"INPUT_PLACEHOLDER": "Search messages, contacts or conversations",
"EMPTY_STATE_DEFAULT": "Search by conversation id, email, phone number, messages for better search results.",
"BOT_LABEL": "رد آلي",
"READ_MORE": "Read more",
"WROTE": "wrote:"
}
}

View File

@ -79,7 +79,8 @@
"CONVERSATION_ASSIGNMENT": "إرسال إشعارات البريد الإلكتروني عند إسناد محادثة لي", "CONVERSATION_ASSIGNMENT": "إرسال إشعارات البريد الإلكتروني عند إسناد محادثة لي",
"CONVERSATION_CREATION": "إرسال إشعارات للبريد الإلكتروني عند ورود محادثة جديدة", "CONVERSATION_CREATION": "إرسال إشعارات للبريد الإلكتروني عند ورود محادثة جديدة",
"CONVERSATION_MENTION": "إرسال إشعارات بالبريد الإلكتروني عندما يتم ذكرك في محادثة", "CONVERSATION_MENTION": "إرسال إشعارات بالبريد الإلكتروني عندما يتم ذكرك في محادثة",
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "إرسال إشعارات البريد الإلكتروني عند إنشاء رسالة جديدة في محادثة موكلة" "ASSIGNED_CONVERSATION_NEW_MESSAGE": "إرسال إشعارات البريد الإلكتروني عند إنشاء رسالة جديدة في محادثة موكلة",
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "إرسال إشعارات التنية عند إنشاء رسالة جديدة في محادثة موكلة"
}, },
"API": { "API": {
"UPDATE_SUCCESS": "يتم تحديث إعدادات الإشعارات بنجاح", "UPDATE_SUCCESS": "يتم تحديث إعدادات الإشعارات بنجاح",
@ -92,6 +93,7 @@
"CONVERSATION_CREATION": "إرسال إشعارات المتصفح عند ورود محادثة جديدة", "CONVERSATION_CREATION": "إرسال إشعارات المتصفح عند ورود محادثة جديدة",
"CONVERSATION_MENTION": "إرسال إشعارات بالبريد الإلكتروني عندما يتم ذكرك او الاشارة اليك في محادثة", "CONVERSATION_MENTION": "إرسال إشعارات بالبريد الإلكتروني عندما يتم ذكرك او الاشارة اليك في محادثة",
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "إرسال إشعارات التنية عند إنشاء رسالة جديدة في محادثة موكلة", "ASSIGNED_CONVERSATION_NEW_MESSAGE": "إرسال إشعارات التنية عند إنشاء رسالة جديدة في محادثة موكلة",
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "إرسال إشعارات التنية عند إنشاء رسالة جديدة في محادثة موكلة",
"HAS_ENABLED_PUSH": "لقد قمت بتمكين الإشعارات لهذا المتصفح.", "HAS_ENABLED_PUSH": "لقد قمت بتمكين الإشعارات لهذا المتصفح.",
"REQUEST_PUSH": "تفعيل إشعارات المتصفح" "REQUEST_PUSH": "تفعيل إشعارات المتصفح"
}, },
@ -192,6 +194,7 @@
"CONVERSATIONS": "المحادثات", "CONVERSATIONS": "المحادثات",
"ALL_CONVERSATIONS": "كل المحادثات", "ALL_CONVERSATIONS": "كل المحادثات",
"MENTIONED_CONVERSATIONS": "الإشارات", "MENTIONED_CONVERSATIONS": "الإشارات",
"PARTICIPATING_CONVERSATIONS": "شارك",
"UNATTENDED_CONVERSATIONS": "بدون حضور", "UNATTENDED_CONVERSATIONS": "بدون حضور",
"REPORTS": "التقارير", "REPORTS": "التقارير",
"SETTINGS": "الإعدادات", "SETTINGS": "الإعدادات",
@ -239,6 +242,7 @@
"DRAFT": "مسودة", "DRAFT": "مسودة",
"ARCHIVED": "مؤرشفة", "ARCHIVED": "مؤرشفة",
"CATEGORY": "الفئة", "CATEGORY": "الفئة",
"SETTINGS": "الإعدادات",
"CATEGORY_EMPTY_MESSAGE": "لم يتم العثور على فئات" "CATEGORY_EMPTY_MESSAGE": "لم يتم العثور على فئات"
}, },
"SET_AUTO_OFFLINE": { "SET_AUTO_OFFLINE": {

View File

@ -5,6 +5,9 @@
"TESTIMONIAL_HEADER": "إن كل ما يلزم هو خطوة واحدة للمضي قدما", "TESTIMONIAL_HEADER": "إن كل ما يلزم هو خطوة واحدة للمضي قدما",
"TESTIMONIAL_CONTENT": "You're one step away from engaging your customers, retaining them and finding new ones.", "TESTIMONIAL_CONTENT": "You're one step away from engaging your customers, retaining them and finding new ones.",
"TERMS_ACCEPT": "من خلال التسجيل، فإنك توافق على <a href=\"https://www.chatwoot.com/terms\">شروط الخدمة</a> و <a href=\"https://www.chatwoot.com/privacy-policy\">سياسة الخصوصية</a>", "TERMS_ACCEPT": "من خلال التسجيل، فإنك توافق على <a href=\"https://www.chatwoot.com/terms\">شروط الخدمة</a> و <a href=\"https://www.chatwoot.com/privacy-policy\">سياسة الخصوصية</a>",
"OAUTH": {
"GOOGLE_SIGNUP": "Sign up with Google"
},
"COMPANY_NAME": { "COMPANY_NAME": {
"LABEL": "Company name", "LABEL": "Company name",
"PLACEHOLDER": "Enter your company name. eg: Wayne Enterprises", "PLACEHOLDER": "Enter your company name. eg: Wayne Enterprises",

View File

@ -165,7 +165,8 @@
"CONTEXT_MENU": { "CONTEXT_MENU": {
"COPY": "Copy", "COPY": "Copy",
"DELETE": "Изтрий", "DELETE": "Изтрий",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses" "CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate"
} }
}, },
"EMAIL_TRANSCRIPT": { "EMAIL_TRANSCRIPT": {
@ -253,5 +254,29 @@
"BCC": "Bcc", "BCC": "Bcc",
"CC": "Cc", "CC": "Cc",
"SUBJECT": "Subject" "SUBJECT": "Subject"
},
"CONVERSATION_PARTICIPANTS": {
"SIDEBAR_MENU_TITLE": "Participating",
"SIDEBAR_TITLE": "Conversation participants",
"NO_RECORDS_FOUND": "Няма намерени резултати",
"ADD_PARTICIPANTS": "Select participants",
"REMANING_PARTICIPANTS_TEXT": "+%{count} others",
"REMANING_PARTICIPANT_TEXT": "+%{count} other",
"TOTAL_PARTICIPANTS_TEXT": "%{count} people are participating.",
"TOTAL_PARTICIPANT_TEXT": "%{count} person is participating.",
"NO_PARTICIPANTS_TEXT": "No one is participating!.",
"WATCH_CONVERSATION": "Join conversation",
"YOU_ARE_WATCHING": "You are participating",
"API": {
"ERROR_MESSAGE": "Could not update, try again!",
"SUCCESS_MESSAGE": "Participants updated!"
}
},
"TRANSLATE_MODAL": {
"TITLE": "View translated content",
"DESC": "You can view the translated content in each langauge.",
"ORIGINAL_CONTENT": "Original Content",
"TRANSLATED_CONTENT": "Translated Content",
"NO_TRANSLATIONS_AVAILABLE": "No translations are available for this content"
} }
} }

View File

@ -82,6 +82,7 @@
"conversation_creation": "New conversation", "conversation_creation": "New conversation",
"conversation_assignment": "Conversation Assigned", "conversation_assignment": "Conversation Assigned",
"assigned_conversation_new_message": "New Message", "assigned_conversation_new_message": "New Message",
"participating_conversation_new_message": "New Message",
"conversation_mention": "Mention" "conversation_mention": "Mention"
} }
}, },

View File

@ -28,6 +28,17 @@
"SAVING": "Saving...", "SAVING": "Saving...",
"SAVED": "Saved" "SAVED": "Saved"
}, },
"ARTICLE_EDITOR": {
"IMAGE_UPLOAD": {
"TITLE": "Upload image",
"UPLOADING": "Качване...",
"SUCCESS": "Image uploaded successfully",
"ERROR": "Error while uploading image",
"ERROR_FILE_SIZE": "Image size should be less than {size}MB",
"ERROR_FILE_FORMAT": "Image format should be jpg, jpeg or png",
"ERROR_FILE_DIMENSIONS": "Image dimensions should be less than 2000 x 2000"
}
},
"ARTICLE_SETTINGS": { "ARTICLE_SETTINGS": {
"TITLE": "Article Settings", "TITLE": "Article Settings",
"FORM": { "FORM": {

View File

@ -476,7 +476,8 @@
"WHATSAPP_SECTION_TITLE": "API Key", "WHATSAPP_SECTION_TITLE": "API Key",
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key", "WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here", "WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
"WHATSAPP_SECTION_UPDATE_BUTTON": "Обновяване" "WHATSAPP_SECTION_UPDATE_BUTTON": "Обновяване",
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
}, },
"AUTO_ASSIGNMENT": { "AUTO_ASSIGNMENT": {
"MAX_ASSIGNMENT_LIMIT": "Auto assignment limit", "MAX_ASSIGNMENT_LIMIT": "Auto assignment limit",

View File

@ -23,6 +23,7 @@ import { default as _settings } from './settings.json';
import { default as _signup } from './signup.json'; import { default as _signup } from './signup.json';
import { default as _teamsSettings } from './teamsSettings.json'; import { default as _teamsSettings } from './teamsSettings.json';
import { default as _whatsappTemplates } from './whatsappTemplates.json'; import { default as _whatsappTemplates } from './whatsappTemplates.json';
import { default as _helpCenter } from './helpCenter.json';
export default { export default {
..._advancedFilters, ..._advancedFilters,
@ -50,4 +51,5 @@ export default {
..._teamsSettings, ..._teamsSettings,
..._whatsappTemplates, ..._whatsappTemplates,
..._bulkActions, ..._bulkActions,
..._helpCenter,
}; };

View File

@ -14,6 +14,11 @@
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно", "ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно",
"UNAUTH": "Username / Password Incorrect. Please try again" "UNAUTH": "Username / Password Incorrect. Please try again"
}, },
"OAUTH": {
"GOOGLE_LOGIN": "Login with Google",
"BUSINESS_ACCOUNTS_ONLY": "Please use your company email address to login",
"NO_ACCOUNT_FOUND": "We couldn't find an account for your email address."
},
"FORGOT_PASSWORD": "Forgot your password?", "FORGOT_PASSWORD": "Forgot your password?",
"CREATE_NEW_ACCOUNT": "Create new account", "CREATE_NEW_ACCOUNT": "Create new account",
"SUBMIT": "Login" "SUBMIT": "Login"

Some files were not shown because too many files have changed in this diff Show More