Merge branch 'release/2.15.0'

This commit is contained in:
Sojan
2023-03-15 19:58:19 +05:30
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'."
exit 1
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
java -jar ~/tmp/openapi-generator-cli-5.3.0.jar validate -i swagger/swagger.json
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-6.3.0.jar validate -i swagger/swagger.json
# Database setup
- run: yarn install --check-files

View File

@@ -30,7 +30,7 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
npm
# 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 \
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc

View File

@@ -25,7 +25,7 @@
// 1025,8025 mailhog
"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": {
"3000": {
"label": "Rails Server"

View File

@@ -52,6 +52,8 @@ POSTGRES_HOST=postgres
POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD=
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
# The email from which all outgoing emails are sent
@@ -169,6 +171,9 @@ USE_INBOX_AVATAR_FOR_BOT=true
## Sentry
# SENTRY_DSN=
## LogRocket
# LOG_ROCKET_PROJECT_ID=xxxxx/some-project
## Scout
## https://scoutapm.com/docs/ruby/configuration
# 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
# DD_TRACE_AGENT_URL=
## IP look up configuration
## 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
# MaxMindDB API key to download GeoLite2 City database
# IP_LOOKUP_API_KEY=
## Rack Attack configuration

View File

@@ -44,3 +44,9 @@ jobs:
# sudo systemctl restart chatwoot.target
# 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:
test:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
services:
postgres:
image: postgres:10.8
@@ -49,6 +49,10 @@ jobs:
with:
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- uses: actions/setup-node@v3
with:
node-version: 16
- name: yarn
run: yarn install
@@ -70,3 +74,10 @@ jobs:
- name: Run backend tests
run: |
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
# 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
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 --##
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
gem 'bootsnap', require: false
@@ -96,13 +96,16 @@ gem 'slack-ruby-client'
gem 'google-cloud-dialogflow'
##-- apm and error monitoring ---#
gem 'ddtrace'
gem 'elastic-apm'
gem 'newrelic_rpm'
gem 'scout_apm'
gem 'sentry-rails', '~> 5.3', '>= 5.3.1'
gem 'sentry-ruby', '~> 5.3'
gem 'sentry-sidekiq', '~> 5.3', '>= 5.3.1'
# loaded only when environment variables are set.
# ref application.rb
gem 'ddtrace', require: false
gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', require: false
gem 'scout_apm', require: false
gem 'sentry-rails', require: false
gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', require: false
##-- background job processing --##
gem 'sidekiq', '~> 6.4.2'
@@ -203,6 +206,8 @@ end
# worked with microsoft refresh token
gem 'omniauth-oauth2'
gem 'audited', '~> 5.2'
# need for google auth
gem 'omniauth'
gem 'omniauth-google-oauth2'

View File

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

View File

@@ -1,3 +1,3 @@
release: bundle exec rails db:chatwoot_prepare
web: bin/rails server -p $PORT -e $RAILS_ENV
worker: bundle exec sidekiq -C config/sidekiq.yml
release: POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rails db:chatwoot_prepare
web: bundle exec rails ip_lookup:setup && bin/rails server -p $PORT -e $RAILS_ENV
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.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 'selectize';
@import 'datetime_picker';
@import 'library/clearfix';
@import 'library/data-label';

View File

@@ -43,3 +43,20 @@
.cell-label--number {
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 = {}
# 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
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
result = {}
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception

View File

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

View File

@@ -55,9 +55,9 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
def article_params
params.require(:article).permit(
:title, :slug, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title,
:description,
{ tags: [] }]
:title, :slug, :position, :content, :description, :position, :category_id, :author_id, :associated_article_id, :status, meta: [:title,
:description,
{ tags: [] }]
)
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
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
authorize @contact_inbox.inbox, :show?
rescue ActiveRecord::RecordNotUnique
render json: { error: 'source_id should be unique' }, status: :unprocessable_entity
end
def build_contact_inbox

View File

@@ -1,5 +1,5 @@
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_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)
ContactIdentifyAction.new(
contact: @contact,
params: { email: contact_email }
params: { email: contact_email, name: contact_name }
).perform
else
@message.update!(message_update_params[:message])

View File

@@ -45,13 +45,18 @@ class Microsoft::CallbacksController < ApplicationController
channel_email.inbox
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
ActiveRecord::Base.transaction do
channel_email = Channel::Email.create!(email: users_data['email'], account: account)
account.inboxes.create!(
account: account,
channel: channel_email,
name: users_data['name']
name: users_data['name'] || fallback_name
)
channel_email
end

View File

@@ -12,6 +12,8 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
end
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)
rescue StandardError => e
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
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
def permitted_params
@@ -64,4 +66,8 @@ class Public::Api::V1::Inboxes::MessagesController < Public::Api::V1::InboxesCon
message_type: :incoming
}
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

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 :portal
before_action :set_category, except: [:index]
@@ -8,6 +8,7 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
def index
@articles = @portal.articles
@articles = @articles.search(list_params) if list_params.present?
@articles.order(position: :asc)
end
def show; end
@@ -15,21 +16,30 @@ class Public::Api::V1::Portals::ArticlesController < PublicController
private
def set_article
@article = @category.articles.find(params[:id])
@article = @category.articles.find(permitted_params[:id])
@article.increment_view_count
@parsed_content = render_article_content(@article.content)
end
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
def portal
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
@portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false)
end
def list_params
params.permit(:query)
params.permit(:query, :locale)
end
def permitted_params
params.permit(:slug, :category_slug, :locale, :id)
end
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 :portal
before_action :set_category, only: [:show]
layout 'portal'
def index
@categories = @portal.categories
@categories = @portal.categories.order(position: :asc)
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 :portal
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_web_widget
before_action :ensure_account_is_active
before_action :ensure_location_is_supported
before_action :set_token
before_action :set_contact
before_action :build_contact
@@ -18,6 +19,9 @@ class WidgetsController < ActionController::Base
def set_web_widget
@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
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?
end
def ensure_location_is_supported; end
def additional_attributes
if @web_widget.inbox.account.feature_enabled?('ip_lookup')
{ created_at_ip: request.remote_ip }
@@ -67,3 +73,5 @@ class WidgetsController < ActionController::Base
response.headers.delete('X-Frame-Options')
end
end
WidgetsController.prepend_mod_with('WidgetsController')

View File

@@ -10,7 +10,7 @@ class AccessTokenDashboard < Administrate::BaseDashboard
ATTRIBUTE_TYPES = {
owner: Field::Polymorphic,
id: Field::Number,
token: Field::String,
token: SecretField,
created_at: Field::DateTime,
updated_at: Field::DateTime
}.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
include EmailHelper
def initialize(email_object)
@email_object = email_object
end
@@ -7,7 +9,8 @@ class EmailChannelFinder
channel = nil
recipient_mails = @email_object.to.to_a + @email_object.cc.to_a
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?
end
channel

View File

@@ -3,4 +3,11 @@ module EmailHelper
domain = email.split('@').last
domain.split('.').first
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

View File

@@ -17,6 +17,15 @@ module ReportingEventHelper
from_in_inbox_timezone.working_time_until(to_in_inbox_timezone)
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
def configure_working_hours(working_hours)

View File

@@ -1,5 +1,10 @@
<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" />
<transition name="fade" mode="out-in">
<router-view />
@@ -22,6 +27,7 @@ import NetworkNotification from './components/NetworkNotification';
import UpdateBanner from './components/app/UpdateBanner.vue';
import vueActionCable from './helper/actionCable';
import WootSnackbarBox from './components/SnackbarContainer';
import rtlMixin from 'shared/mixins/rtlMixin';
import {
registerSubscription,
verifyServiceWorkerExistence,
@@ -38,6 +44,8 @@ export default {
WootSnackbarBox,
},
mixins: [rtlMixin],
data() {
return {
showAddAccountModal: false,
@@ -96,6 +104,7 @@ export default {
} = this.getAccount(this.currentAccountId);
const { pubsub_token: pubsubToken } = this.currentUser || {};
this.setLocale(locale);
this.updateRTLDirectionView(locale);
this.latestChatwootVersion = latestChatwootVersion;
vueActionCable.init(pubsubToken);
},

View File

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

View File

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

View File

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

View File

@@ -16,12 +16,12 @@ body {
}
.banner + .app-wrapper {
.button--fixed-right-top {
.button--fixed-top {
top: 5.6 * $space-one;
}
.off-canvas-content {
.button--fixed-right-top {
.button--fixed-top {
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: var(--space-small);
}
@@ -14,6 +10,10 @@
margin-left: var(--space-minus-slab);
}
.margin-right-minus-slab {
margin-right: var(--space-minus-slab);
}
.fs-small {
font-size: var(--font-size-small);
}
@@ -75,3 +75,9 @@
align-items: center;
display: flex;
}
.button--fixed-top {
position: fixed;
right: var(--space-small);
top: var(--space-small);
}

View File

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

View File

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

View File

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

View File

@@ -50,22 +50,22 @@ $default-button-height: 4.0rem;
&.secondary {
border-color: var(--s-200);
color: var(--s-700)
color: var(--s-700);
}
&.success {
border-color: var(--s-200);
color: var(--g-700)
color: var(--g-700);
}
&.alert {
border-color: var(--s-200);
color: var(--r-700)
color: var(--r-700);
}
&.warning {
border-color: var(--s-200);
color: var(--y-700)
color: var(--y-700);
}
&:hover {
@@ -116,19 +116,19 @@ $default-button-height: 4.0rem;
color: var(--w-700);
&.secondary {
color: var(--s-700)
color: var(--s-700);
}
&.success {
color: var(--g-700)
color: var(--g-700);
}
&.alert {
color: var(--r-700)
color: var(--r-700);
}
&.warning {
color: var(--y-700)
color: var(--y-700);
}
&: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;
flex-direction: column;
justify-content: flex-start;
margin-left: $space-slab;
margin-left: var(--space-small);
min-width: 0;
}
}

View File

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

View File

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

View File

@@ -3,23 +3,23 @@
cursor: pointer;
margin: 0;
padding: $space-normal $space-small $space-normal $space-two;
padding: var(--space-normal);
&.active {
@include custom-border-top(3px, $color-woot);
@include custom-border-top(3px, var(--color-woot));
@include background-white;
.heading,
.metric {
color: $color-woot;
color: var(--color-woot);
}
}
.heading {
align-items: center;
color: $color-heading;
color: var(--color-heading);
display: flex;
font-size: $font-size-small;
font-weight: $font-weight-bold;
font-size: var(--font-size-small);
font-weight: var(--font-weight-bold);
margin: 0;
}
@@ -29,19 +29,19 @@
}
.metric-wrap {
align-items: baseline;
align-items: center;
display: flex;
}
.metric {
font-size: $font-size-big;
font-weight: $font-weight-feather;
margin-top: $space-smaller;
font-size: var(--font-size-big);
font-weight: var(--font-weight-feather);
margin-top: var(--space-smaller);
}
.metric-trend {
font-size: $font-size-small;
margin-left: $space-small;
font-size: var(--font-size-small);
margin: 0 var(--space-small);
}
.metric-up {
@@ -53,7 +53,7 @@
}
.desc {
font-size: $font-size-small;
font-size: var(--font-size-small);
margin: 0;
text-transform: capitalize;
}
@@ -62,8 +62,8 @@
.report-bar {
@include background-white;
@include border-light;
margin: -1px $zero;
padding: $space-small $space-medium;
margin: var(--space-minus-micro) 0;
padding: var(--space-small) var(--space-medium);
.chart-container {
@include flex;
@@ -76,12 +76,12 @@
.empty-state {
color: $color-gray;
font-size: $font-size-default;
margin: $space-jumbo;
font-size: var(--font-size-default);
margin: var(--space-jumbo);
}
.business-hours {
margin: $space-normal;
margin: var(--space-normal);
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 {
border-radius: 100%;
height: var(--space-two);
@@ -26,17 +18,21 @@
display: flex;
}
.reports-option__title {
margin: 0 var(--space-small);
}
.business-hours {
align-items: center;
display: flex;
justify-content: end;
margin-bottom: var(--space-normal);
justify-content: flex-start;
margin-left: auto;
padding-right: var(--space-normal);
}
.business-hours-text {
font-size: var(--font-size-small);
margin: 0 var(--space-small);
}
.switch {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -331,11 +331,11 @@ export default {
.beta {
padding-right: 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;
font-size: var(--font-size-micro);
font-weight: var(--font-weight-medium);
line-height: 18px;
line-height: 14px;
border: 1px solid transparent;
border-radius: 2em;
color: var(--g-800);
@@ -348,7 +348,7 @@ export default {
color: var(--s-600);
font-size: var(--font-size-micro);
font-weight: var(--font-weight-bold);
margin-left: var(--space-smaller);
margin: 0 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>
<button
v-if="showClose"
class="label-close--button "
class="label-close--button"
:style="{ color: textColor }"
@click="onClick"
>
@@ -108,6 +108,7 @@ export default {
display: inline-flex;
align-items: center;
font-weight: var(--font-weight-medium);
gap: var(--space-smaller);
margin-right: var(--space-smaller);
margin-bottom: var(--space-smaller);
padding: var(--space-smaller);
@@ -126,9 +127,6 @@ export default {
.label--icon {
cursor: pointer;
}
.label-color-dot {
margin-right: var(--space-smaller);
}
&.small .label--icon,
&.small .close--icon {
@@ -209,7 +207,6 @@ export default {
.label-close--button {
color: var(--s-800);
margin-bottom: var(--space-minus-micro);
margin-left: var(--space-smaller);
border-radius: var(--border-radius-small);
cursor: pointer;
@@ -232,7 +229,6 @@ export default {
width: var(--space-slab);
height: var(--space-slab);
border-radius: var(--border-radius-small);
margin-right: var(--space-smaller);
box-shadow: var(--shadow-small);
}
.label.small .label-color-dot {

View File

@@ -1,5 +1,5 @@
<template>
<button class="back-button" @click.capture="goBack">
<button class="settings back-button" @click.capture="goBack">
<fluent-icon icon="chevron-left" />
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
</button>
@@ -29,4 +29,3 @@ export default {
},
};
</script>
<style scoped></style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -243,24 +243,20 @@ export default {
return this.contentAttributes.translations || {};
},
displayQuotedButton() {
if (!this.isIncoming) {
return false;
}
if (this.emailMessageContent.includes('<blockquote')) {
return true;
}
if (!this.isIncoming) {
return false;
}
return false;
},
translationsAvailable() {
return !!Object.keys(this.translations).length;
},
message() {
if (this.contentType === 'input_csat') {
return this.$t('CONVERSATION.CSAT_REPLY_MESSAGE');
}
// If the message is an email, emailMessageContent would be present
// In that case, we would use letter package to render the email
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 (
this.formatMessage(
this.data.content,
@@ -659,4 +660,71 @@ li.right {
.context-menu {
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>

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { getLanguageName } from '../languages';
import { getLanguageName, getLanguageDirection } from '../languages';
describe('#getLanguageName', () => {
it('Returns correct language name', () => {
@@ -8,3 +8,10 @@ describe('#getLanguageName', () => {
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
class="message-text__wrap"
:class="{
'show--quoted': showQuotedContent,
'hide--quoted': !showQuotedContent,
'show--quoted': isQuotedContentPresent,
'hide--quoted': !isQuotedContentPresent,
}"
>
<div v-if="!isEmail" v-dompurify-html="message" class="text-content" />
<letter v-else class="text-content" :html="message" />
<button
v-if="displayQuotedButton"
v-if="showQuoteToggle"
class="quoted-text--button"
@click="toggleQuotedContent"
>
@@ -49,6 +49,20 @@ export default {
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: {
toggleQuotedContent() {
this.showQuotedContent = !this.showQuotedContent;

View File

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

View File

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

View File

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

View File

@@ -184,9 +184,18 @@ export default {
</script>
<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 {
border-bottom: 1px solid var(--s-100);
padding: var(--space-normal) var(--space-one);
padding: var(--space-normal);
position: relative;
}
@@ -195,7 +204,7 @@ export default {
span {
font-size: var(--font-size-mini);
margin-left: var(--space-smaller);
margin: 0 var(--space-smaller);
}
input[type='checkbox'] {

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,7 +35,7 @@
"REMOVE_SELECTION": "إزالة التحديد",
"DOWNLOAD": "تنزيل",
"UNKNOWN_FILE_TYPE": "ملف غير معروف",
"SAVE_CONTACT": "Save",
"SAVE_CONTACT": "حفظ",
"UPLOADING_ATTACHMENTS": "جاري تحميل المرفقات...",
"SUCCESS_DELETE_MESSAGE": "تم حذف الرسالة بنجاح",
"FAIL_DELETE_MESSSAGE": "تعذر حذف الرسالة! حاول مرة أخرى",
@@ -43,8 +43,8 @@
"RATING_TITLE": "التقييم",
"FEEDBACK_TITLE": "الملاحظات",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
"SHOW_LABELS": "إظهار السمات",
"HIDE_LABELS": "إخفاء السمات"
},
"HEADER": {
"RESOLVE_ACTION": "إغلاق المحادثة",
@@ -69,7 +69,7 @@
"CARD_CONTEXT_MENU": {
"PENDING": "تحديد كمعلق",
"RESOLVED": "تحديد كمحلولة",
"MARK_AS_UNREAD": "Mark as unread",
"MARK_AS_UNREAD": "وضع علامة كغير مقروء",
"REOPEN": "إعادة فتح المحادثة",
"SNOOZE": {
"TITLE": "غفوة",
@@ -135,8 +135,8 @@
}
},
"UNDEFINED_VARIABLES": {
"TITLE": "Undefined variables",
"MESSAGE": "You have {undefinedVariablesCount} undefined variables in your message: {undefinedVariables}. Would you like to send the message anyway?",
"TITLE": "متغيرات غير معرفة",
"MESSAGE": "لديك {undefinedVariablesCount} متغيرات غير محددة في رسالتك: {undefinedVariables}. هل ترغب في إرسال الرسالة على أي حال؟",
"CONFIRM": {
"YES": "إرسال",
"CANCEL": "إلغاء"
@@ -165,7 +165,8 @@
"CONTEXT_MENU": {
"COPY": "نسخ",
"DELETE": "حذف",
"CREATE_A_CANNED_RESPONSE": "إضافة إلى الردود السريعة"
"CREATE_A_CANNED_RESPONSE": "إضافة إلى الردود السريعة",
"TRANSLATE": "ترجم"
}
},
"EMAIL_TRANSCRIPT": {
@@ -253,5 +254,29 @@
"BCC": "Bcc",
"CC": "Cc",
"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_assignment": "تم تعيين المحادثة",
"assigned_conversation_new_message": "رسالة جديدة",
"participating_conversation_new_message": "رسالة جديدة",
"conversation_mention": "إشارة"
}
},

View File

@@ -28,6 +28,17 @@
"SAVING": "جاري الحفظ...",
"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": {
"TITLE": "إعدادات المقالة",
"FORM": {

View File

@@ -476,7 +476,8 @@
"WHATSAPP_SECTION_TITLE": "مفتاح API",
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"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": {
"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 _teamsSettings } from './teamsSettings.json';
import { default as _whatsappTemplates } from './whatsappTemplates.json';
import { default as _helpCenter } from './helpCenter.json';
export default {
..._advancedFilters,
@@ -50,4 +51,5 @@ export default {
..._teamsSettings,
..._whatsappTemplates,
..._bulkActions,
..._helpCenter,
};

View File

@@ -14,6 +14,11 @@
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً",
"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": "نسيت كلمة المرور؟",
"CREATE_NEW_ACCOUNT": "إنشاء حساب جديد",
"SUBMIT": "تسجيل الدخول"

View File

@@ -426,6 +426,12 @@
"UNATTENDED": "بدون حضور",
"UNASSIGNED": "غير مسند"
},
"CONVERSATION_HEATMAP": {
"HEADER": "Conversation Traffic",
"NO_CONVERSATIONS": "No conversations",
"CONVERSATION": "%{count} conversation",
"CONVERSATIONS": "%{count} conversations"
},
"AGENT_CONVERSATIONS": {
"HEADER": "المحادثات من قبل الوكلاء",
"LOADING_MESSAGE": "جاري تحميل مقاييس الوكيل...",
@@ -443,5 +449,14 @@
"BUSY": "مشغول",
"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_CREATION": "إرسال إشعارات للبريد الإلكتروني عند ورود محادثة جديدة",
"CONVERSATION_MENTION": "إرسال إشعارات بالبريد الإلكتروني عندما يتم ذكرك في محادثة",
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "إرسال إشعارات البريد الإلكتروني عند إنشاء رسالة جديدة في محادثة موكلة"
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "إرسال إشعارات البريد الإلكتروني عند إنشاء رسالة جديدة في محادثة موكلة",
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "إرسال إشعارات التنية عند إنشاء رسالة جديدة في محادثة موكلة"
},
"API": {
"UPDATE_SUCCESS": "يتم تحديث إعدادات الإشعارات بنجاح",
@@ -92,6 +93,7 @@
"CONVERSATION_CREATION": "إرسال إشعارات المتصفح عند ورود محادثة جديدة",
"CONVERSATION_MENTION": "إرسال إشعارات بالبريد الإلكتروني عندما يتم ذكرك او الاشارة اليك في محادثة",
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "إرسال إشعارات التنية عند إنشاء رسالة جديدة في محادثة موكلة",
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "إرسال إشعارات التنية عند إنشاء رسالة جديدة في محادثة موكلة",
"HAS_ENABLED_PUSH": "لقد قمت بتمكين الإشعارات لهذا المتصفح.",
"REQUEST_PUSH": "تفعيل إشعارات المتصفح"
},
@@ -192,6 +194,7 @@
"CONVERSATIONS": "المحادثات",
"ALL_CONVERSATIONS": "كل المحادثات",
"MENTIONED_CONVERSATIONS": "الإشارات",
"PARTICIPATING_CONVERSATIONS": "شارك",
"UNATTENDED_CONVERSATIONS": "بدون حضور",
"REPORTS": "التقارير",
"SETTINGS": "الإعدادات",
@@ -239,6 +242,7 @@
"DRAFT": "مسودة",
"ARCHIVED": "مؤرشفة",
"CATEGORY": "الفئة",
"SETTINGS": "الإعدادات",
"CATEGORY_EMPTY_MESSAGE": "لم يتم العثور على فئات"
},
"SET_AUTO_OFFLINE": {

View File

@@ -5,6 +5,9 @@
"TESTIMONIAL_HEADER": "إن كل ما يلزم هو خطوة واحدة للمضي قدما",
"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>",
"OAUTH": {
"GOOGLE_SIGNUP": "Sign up with Google"
},
"COMPANY_NAME": {
"LABEL": "Company name",
"PLACEHOLDER": "Enter your company name. eg: Wayne Enterprises",

View File

@@ -165,7 +165,8 @@
"CONTEXT_MENU": {
"COPY": "Copy",
"DELETE": "Изтрий",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses"
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate"
}
},
"EMAIL_TRANSCRIPT": {
@@ -253,5 +254,29 @@
"BCC": "Bcc",
"CC": "Cc",
"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_assignment": "Conversation Assigned",
"assigned_conversation_new_message": "New Message",
"participating_conversation_new_message": "New Message",
"conversation_mention": "Mention"
}
},

View File

@@ -28,6 +28,17 @@
"SAVING": "Saving...",
"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": {
"TITLE": "Article Settings",
"FORM": {

View File

@@ -476,7 +476,8 @@
"WHATSAPP_SECTION_TITLE": "API Key",
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"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": {
"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 _teamsSettings } from './teamsSettings.json';
import { default as _whatsappTemplates } from './whatsappTemplates.json';
import { default as _helpCenter } from './helpCenter.json';
export default {
..._advancedFilters,
@@ -50,4 +51,5 @@ export default {
..._teamsSettings,
..._whatsappTemplates,
..._bulkActions,
..._helpCenter,
};

View File

@@ -14,6 +14,11 @@
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно",
"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?",
"CREATE_NEW_ACCOUNT": "Create new account",
"SUBMIT": "Login"

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