Compare commits

...
Author SHA1 Message Date
Muhsin Keloth c9551c6f68 Add modal 2023-04-19 15:11:56 +05:30
Shivam MishraandGitHub 9c0259da6b feat: add priority field to conversation (#6921) (#6927)
* feat: add priority

* feat: add indexes
2023-04-19 13:23:14 +05:30
Pranav Raj SandGitHub ea2c442328 chore: Add account_id as custom_attribute for cloud users (#6926) 2023-04-18 22:35:11 -07:00
Volodymyr MakukhandGitHub b93b8cdab6 Fix flaky reporting_event_listener_spec (#6923) 2023-04-19 10:42:45 +05:30
Pranav Raj SandGitHub e6505fc7a4 chore: Cache the dashboard app on the first load (#6774) 2023-04-18 19:44:57 -07:00
Pranav Raj SandGitHub c5c36af529 fix: Remove fallback phone_number search in WhatsApp event processing (#6904) 2023-04-18 18:30:01 -07:00
026e03c307 fix: Handle spaces in CC/BCC email lists (#6788)
When the CC field is generated in the UI, the email values are joined together
with ", " but when they are parsed, we currently split by just ",".

This causes an error on the backend and on the frontend.

It seems reasonable to update the code to allow whitespace in the input and to
split by `\s*,\s` and also to trim leading and trailing whitespace from the CC
list.

---------

Co-authored-by: Sojan <sojan@pepalo.com>
2023-04-18 19:18:23 +05:30
Sojan 37b7098673 Merge branch 'release/2.16.0' into develop 2023-04-18 01:04:27 +05:30
Sojan cb2f86b983 Bump version to 2.16.0 2023-04-18 01:02:19 +05:30
905e77048f chore: Contact import improvements (CW-1362) (#6747)
Fixes: https://linear.app/chatwoot/issue/CW-1362/csv-imports-are-not-working-properly
Fixes: #3462

---------

Co-authored-by: Sojan <sojan@pepalo.com>
2023-04-18 00:40:55 +05:30
Vishnu NarayananandGitHub 4505c5dda3 chore: add build id to settings page (#6873)
- Adds a build Id to the settings page
2023-04-18 00:35:35 +05:30
Sivin VargheseGitHubMuhsin Keloth <muhsinkeramam@gmail.com>, Nithin David
c9ce9e5b8f feat: Improved country code in contact form view. (#6801)
* feat: Improved country code in contact.

* chore: Minor fixes

* chore: Minor fixes

* chore: Adds arrow key navigation and cursor pointer

* chore: Minor fix

* chore: Code clean up

* chore: Handle outside click

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>, Nithin David
2023-04-17 20:32:09 +05:30
9e2f991484 feat: audit logs UI (#6803)
* feat: init auditlogs ui

* chore: add api

* fix: action

* chore: add action,username,time

* feat: add pagination support

* chore: format time

* chore: refactor

* chore: refactor auditlogs api response

* chore: update icon

* chore: rubocop fixes

* Fixes the way meta is handled in store

* Fixes meta not appearing issue

---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-04-17 19:11:05 +05:30
Sivin VargheseandGitHub 80dcd17f6e fix: Border color for CSAT component in widget (#6915) 2023-04-17 17:50:30 +05:30
Shivam MishraandGitHub a38ecf3dde feat: show webhook verify token for WhatsApp (#6916)
- Display WhatsApp webhook verify token in configuration settings
2023-04-17 17:14:08 +05:30
9090eabb8a chore: Update translations (#6895)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-04-17 14:45:49 +05:30
ca2506a941 feat: allow sorting of articles (#6833)
* feat: sort by position

* chore: whitespace change

* feat: add border bottom color to list item

* feat: allow dragging articles

* feat: add migration to reorder all articles

* feat: add onsort method

* feat: finish UI sorting

* feat: show 50 per page in articles list

* feat: add article sorting methods

* feat: patch up reorder action with the API

* refactor: better naming

* chore: add comments

* feat: attach position to article before create

* feat: move article to end if moved between categories

* chore: add comments

* chore: update version

* fix: don't change position if previous category was nil

* fix: condition to trigger update on category change

* refactor: store new_position

* refactor: use grid instead of table

* feat: add snug spacing

* feat: add grab-icon

* feat: add grab icon to list

* refactor: show draggable only for category page

* feat: add update_positions as a class method

---------

Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
2023-04-17 14:43:10 +05:30
Shivam MishraandGitHub 1886d4ce08 fix: response body in twitter callback (#6907)
* fix: response body

* fix: tests
2023-04-14 16:48:28 +05:30
Shivam MishraandGitHub 4d49b81f1c fix: use response.status instead of success (#6906)
* fix: use response.status instead of success

* refactor: use sentry to capture exception

* refactor: explicitly convert to i
2023-04-14 15:22:08 +05:30
Tejaswini ChileandGitHub 2b736f4698 fix: Update from_email in the name to fix the syntax error (#6900) 2023-04-13 10:42:38 -07:00
Tejaswini ChileandGitHub 9ca21df9fd feat: Route emails based on x-original-to in email channel (#6901)
Fixes: #6608
ref: https://linear.app/chatwoot/issue/CW-30/emails-not-routed-based-on-x-original-to
2023-04-13 14:52:12 +05:30
Tejaswini ChileandGitHub 44837aa657 Fix: save twitter profile for inbox (#6667)
Fixes: #737
2023-04-12 14:16:24 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Sojan Jose
d04344c094 chore(deps): bump commonmarker from 0.23.7 to 0.23.9 (#6892)
Bumps [commonmarker](https://github.com/gjtorikian/commonmarker) from 0.23.7 to 0.23.9.
- [Release notes](https://github.com/gjtorikian/commonmarker/releases)
- [Changelog](https://github.com/gjtorikian/commonmarker/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gjtorikian/commonmarker/compare/v0.23.7...v0.23.9)

---
updated-dependencies:
- dependency-name: commonmarker
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-04-12 14:08:14 +05:30
d45512df72 feat: Account deletion with deleteObjectJob (#6885)
Fixes: https://linear.app/chatwoot/issue/CW-1365/allow-super-admin-to-delete-an-account

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-04-12 13:54:01 +05:30
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2731c2a5be chore(deps): bump nokogiri from 1.14.2 to 1.14.3 (#6893)
Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.14.2 to 1.14.3.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.14.2...v1.14.3)

---
updated-dependencies:
- dependency-name: nokogiri
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-12 13:29:57 +05:30
Tejaswini ChileandGitHub 610463c980 fix: flatten template array for whatsapp templates (#6880)
- Refactor the WhatsApp template sync job
- Fix the issue when fetching the next set of templates
2023-04-11 17:48:17 +05:30
09ce85b30d Chore: moves localstorage helper as a shared utility (#6838)
* Chore: moves localstorage helper as a shared utility and refactors constants

* Refactors constants file

* Fixes merge conflicts

* Delete constants.js

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-04-11 15:50:46 +05:30
Shivam MishraandGitHub 17ff1f11a7 feat: better download for conversation traffic heatmap (#6755)
* feat: genearte report in a grid

* refactor: update API usage

* refactor: separate generate method

* refactor: abstract transform_data

* feat: annotate with comments

* feat: add explicit timezone

* feat: download data only in user timezone

* fix: dates included in heatmap
2023-04-11 09:40:54 +05:30
Pranav Raj SandGitHub f4e121cc44 fix: Update the profile API URL in the documentation (#6875) 2023-04-10 18:23:20 -07:00
Pranav Raj SandGitHub cf934450ab chore: Remove context menu on links (#6874) 2023-04-10 14:13:08 -07:00
Vishnu NarayananandGitHub ad75a79135 feat: add pagination support for audit logs API (#6843)
Add pagination support for audit logs API
2023-04-10 21:07:01 +05:30
Tejaswini ChileandGitHub d49989ace1 chore: Specs for Whatsapp template pagination (#6870)
Spec for https://github.com/chatwoot/chatwoot/pull/6835
2023-04-10 20:44:43 +05:30
Tejaswini ChileandGitHub e69e0bc984 Add status reopen activity message for api channel (#6839) 2023-04-10 19:12:20 +05:30
e877b01e00 fix: Invalidate cache if any of the related objects change (#6860)
* fix: Invalidate cache if any of the related objects change

* Add specs:

* Update context

---------

Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
2023-04-10 15:47:13 +05:30
da11feb39b feat: one-click codepen for widget configuration (#6865)
* feat: allow codepen in code

* feat: enable codepen in config

* feat: update codepen title

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-04-10 15:44:06 +05:30
Tejaswini ChileandGitHub 138afd9af6 Feat: Fetch whatsapp templates till next cursor (#6835) 2023-04-10 14:44:27 +05:30
e753365493 feat: Add the ability to change the agent availability status (#6855)
* Add the option change agent availability

* Remove callout

* Move `AVAILABILITY_STATUS_KEYS` to constants

* Update app/javascript/dashboard/i18n/locale/en/agentMgmt.json

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>

---------

Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-04-10 13:37:12 +05:30
Pranav Raj SandGitHub 91dc7733b0 feat: Use inbox image as avatar for the bot (#6859) 2023-04-07 13:25:18 -07:00
Pranav Raj SandGitHub 463c09184c fix: Disable processing events if account is suspended (#6849) 2023-04-07 12:01:03 -07:00
040e9a732f chore: Update translations (#6854)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-04-07 16:05:33 +05:30
71c5a1e1d4 feat: add lograge to improve logging (#5423)
- Add lograge gem to improve rails logging using `LOGRAGE_ENABLED` env variable
- When enabled Single line log for requests in JSON formatting
- Switch sidekiq also to use JSON formatting

Fixes: chatwoot/product#437
---------

Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-04-07 13:44:30 +05:30
Muhsin KelothandGitHub a521762dd6 feat: Support after param in messages end point (#6848)
Adds support to `after` param while fetching messages
Fixes: https://linear.app/chatwoot/issue/CW-1475/support-after-param-in-messages-end-point
2023-04-07 13:42:54 +05:30
406e8405eb fix: Specs failing for teams/actions.js (#6845)
* fix: specs failing

* fix: specs for labels and inboxes

* Update reports.js

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-04-06 20:48:49 +05:30
427 changed files with 5864 additions and 1757 deletions
-2
View File
@@ -1,8 +1,6 @@
.bundle
.env
.env.*
.git
.gitignore
docker-compose.*
docker/Dockerfile
docker/dockerfiles
+2 -3
View File
@@ -110,6 +110,8 @@ AWS_REGION=
RAILS_LOG_TO_STDOUT=true
LOG_LEVEL=info
LOG_SIZE=500
# Configure this environment variable if you want to use lograge instead of rails logger
#LOGRAGE_ENABLED=true
### This environment variables are only required if you are setting up social media channels
@@ -159,9 +161,6 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
# for mobile apps
# FCM_SERVER_KEY=
## Bot Customizations
USE_INBOX_AVATAR_FOR_BOT=true
### APM and Error Monitoring configurations
## Elastic APM
## https://www.elastic.co/guide/en/apm/agent/ruby/current/getting-started-rails.html
+3 -2
View File
@@ -61,8 +61,9 @@ test/cypress/videos/*
/config/master.key
/config/*.enc
.vscode/settings.json
#ignore files under .vscode directory
.vscode
# yalc for local testing
.yalc
yalc.lock
yalc.lock
+18 -12
View File
@@ -87,7 +87,7 @@ gem 'line-bot-api'
gem 'twilio-ruby', '~> 5.66'
# twitty will handle subscription of twitter account events
# gem 'twitty', git: 'https://github.com/chatwoot/twitty'
gem 'twitty'
gem 'twitty', '~> 0.1.5'
# facebook client
gem 'koala'
# slack client
@@ -149,7 +149,23 @@ gem 'net-imap', require: false
gem 'net-pop', require: false
gem 'net-smtp', require: false
group :production, :staging do
# Include logrange conditionally in intializer using env variable
gem 'lograge', '~> 0.12.0', require: false
# worked with microsoft refresh token
gem 'omniauth-oauth2'
gem 'audited', '~> 5.2'
# need for google auth
gem 'omniauth'
gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
### Gems required only in specific deployment environments ###
##############################################################
group :production do
# we dont want request timing out in development while using byebug
gem 'rack-timeout'
end
@@ -206,13 +222,3 @@ group :development, :test do
gem 'spring'
gem 'spring-watcher-listen'
end
# worked with microsoft refresh token
gem 'omniauth-oauth2'
gem 'audited', '~> 5.2'
# need for google auth
gem 'omniauth'
gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
+15 -7
View File
@@ -137,7 +137,7 @@ GEM
byebug (11.1.3)
climate_control (1.1.1)
coderay (1.1.3)
commonmarker (0.23.7)
commonmarker (0.23.9)
concurrent-ruby (1.2.2)
connection_pool (2.2.5)
crack (0.4.5)
@@ -416,6 +416,11 @@ GEM
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
lograge (0.12.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.19.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
@@ -459,14 +464,14 @@ GEM
sidekiq
newrelic_rpm (8.15.0)
nio4r (2.5.8)
nokogiri (1.14.2)
nokogiri (1.14.3)
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
nokogiri (1.14.2-arm64-darwin)
nokogiri (1.14.3-arm64-darwin)
racc (~> 1.4)
nokogiri (1.14.2-x86_64-darwin)
nokogiri (1.14.3-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.14.2-x86_64-linux)
nokogiri (1.14.3-x86_64-linux)
racc (~> 1.4)
oauth (0.5.10)
oauth2 (2.0.9)
@@ -568,6 +573,8 @@ GEM
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
@@ -710,7 +717,7 @@ GEM
faraday (>= 0.9, < 3.0)
jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0)
twitty (0.1.4)
twitty (0.1.5)
oauth
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
@@ -824,6 +831,7 @@ DEPENDENCIES
line-bot-api
liquid
listen
lograge (~> 0.12.0)
maxminddb
mock_redis
net-imap
@@ -875,7 +883,7 @@ DEPENDENCIES
test-prof
time_diff
twilio-ruby (~> 5.66)
twitty
twitty (~> 0.1.5)
tzinfo-data
uglifier
valid_email2
+1 -1
View File
@@ -1,3 +1,3 @@
release: POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rails db:chatwoot_prepare
release: POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rails db:chatwoot_prepare && echo $SOURCE_VERSION > .git_sha
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
+2 -2
View File
@@ -49,10 +49,10 @@ class Messages::MessageBuilder
return unless @conversation.inbox&.inbox_type == 'Email'
cc_emails = []
cc_emails = @params[:cc_emails].split(',') if @params[:cc_emails].present?
cc_emails = @params[:cc_emails].gsub(/\s+/, '').split(',') if @params[:cc_emails].present?
bcc_emails = []
bcc_emails = @params[:bcc_emails].split(',') if @params[:bcc_emails].present?
bcc_emails = @params[:bcc_emails].gsub(/\s+/, '').split(',') if @params[:bcc_emails].present?
all_email_addresses = cc_emails + bcc_emails
validate_email_addresses(all_email_addresses)
@@ -1,14 +1,19 @@
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
before_action :portal
before_action :check_authorization
before_action :fetch_article, except: [:index, :create, :attach_file]
before_action :fetch_article, except: [:index, :create, :attach_file, :reorder]
before_action :set_current_page, only: [:index]
def index
@portal_articles = @portal.articles
@all_articles = @portal_articles.search(list_params)
@articles_count = @all_articles.count
@articles = @all_articles.order_by_updated_at.page(@current_page)
@articles = if list_params[:category_slug].present?
@all_articles.order_by_position.page(@current_page).per(50)
else
@all_articles.order_by_updated_at.page(@current_page)
end
end
def create
@@ -43,6 +48,11 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
render json: { file_url: url_for(file_blob) }
end
def reorder
Article.update_positions(params[:positions_hash])
head :ok
end
private
def fetch_article
@@ -1,5 +1,7 @@
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
include Api::V2::Accounts::ReportsHelper
include Api::V2::Accounts::HeatmapHelper
before_action :check_authorization
def index
@@ -34,6 +36,9 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
def conversation_traffic
@report_data = generate_conversations_heatmap_report
timezone_offset = (params[:timezone_offset] || 0).to_f
@timezone = ActiveSupport::TimeZone[timezone_offset]
generate_csv('conversation_traffic_reports', 'api/v2/accounts/reports/conversation_traffic')
end
+2 -1
View File
@@ -56,7 +56,8 @@ class DashboardController < ActionController::Base
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
FACEBOOK_API_VERSION: 'v14.0',
IS_ENTERPRISE: ChatwootApp.enterprise?,
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', '')
AZURE_APP_ID: ENV.fetch('AZURE_APP_ID', ''),
GIT_SHA: GIT_HASH
}
end
end
@@ -47,4 +47,11 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
Internal::SeedAccountJob.perform_later(requested_resource)
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account seeding triggered')
end
def destroy
account = Account.find(params[:id])
DeleteObjectJob.perform_later(account) if account.present?
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Account deletion is in progress.')
end
end
@@ -14,7 +14,7 @@ class Twitter::CallbacksController < Twitter::BaseController
redirect_to app_twitter_inbox_agents_url(account_id: account.id, inbox_id: inbox.id)
end
rescue StandardError => e
Rails.logger.error e
ChatwootExceptionTracker.new(e).capture_exception
redirect_to twitter_app_redirect_url
end
@@ -49,10 +49,22 @@ class Twitter::CallbacksController < Twitter::BaseController
twitter_access_token_secret: parsed_body['oauth_token_secret'],
profile_id: parsed_body['user_id']
)
account.inboxes.create!(
inbox = account.inboxes.create!(
name: parsed_body['screen_name'],
channel: twitter_profile
)
save_profile_image(inbox)
inbox
end
def save_profile_image(inbox)
response = twitter_client.user_show(screen_name: inbox.name)
return unless response.status.to_i == 200
parsed_user_profile = response.body
::Avatar::AvatarFromUrlJob.perform_later(inbox, parsed_user_profile['profile_image_url_https'])
end
def permitted_params
+7 -1
View File
@@ -7,12 +7,18 @@ class EmailChannelFinder
def perform
channel = nil
recipient_mails = @email_object.to.to_a + @email_object.cc.to_a
recipient_mails.each do |email|
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
end
def recipient_mails
recipient_addresses = @email_object.to.to_a + @email_object.cc.to_a + @email_object.bcc.to_a + [@email_object['X-Original-To'].try(:value)]
recipient_addresses.flatten.compact
end
end
+21 -3
View File
@@ -22,11 +22,29 @@ class MessageFinder
def current_messages
if @params[:after].present? && @params[:before].present?
messages.reorder('created_at asc').where('id >= ? AND id < ?', @params[:after].to_i, @params[:before].to_i).limit(1000)
messages_between(@params[:after].to_i, @params[:before].to_i)
elsif @params[:before].present?
messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse
messages_before(@params[:before].to_i)
elsif @params[:after].present?
messages_after(@params[:after].to_i)
else
messages.reorder('created_at desc').limit(20).reverse
messages_latest
end
end
def messages_after(after_id)
messages.reorder('created_at asc').where('id > ?', after_id).limit(100)
end
def messages_before(before_id)
messages.reorder('created_at desc').where('id < ?', before_id).limit(20).reverse
end
def messages_between(after_id, before_id)
messages.reorder('created_at asc').where('id >= ? AND id < ?', after_id, before_id).limit(1000)
end
def messages_latest
messages.reorder('created_at desc').limit(20).reverse
end
end
@@ -0,0 +1,103 @@
module Api::V2::Accounts::HeatmapHelper
def generate_conversations_heatmap_report
timezone_data = generate_heatmap_data_for_timezone(params[:timezone_offset])
group_traffic_data(timezone_data)
end
private
def group_traffic_data(data)
# start with an empty array
result_arr = []
# pick all the unique dates from the data in ascending order
dates = data.pluck(:date).uniq.sort
# add the dates as the first row, leave an empty cell for the hour column
# e.g. [nil, '2023-01-01', '2023-1-02', '2023-01-03']
result_arr << ([nil] + dates)
# group the data by hour, we do not need to sort it, because the data is already sorted
# given it starts from the beginning of the day
# here each hour is a key, and the value is an array of all the items for that hour at each date
# e.g. hour = 1
# value = [{date: 2023-01-01, value: 1}, {date: 2023-01-02, value: 1}, {date: 2023-01-03, value: 1}, ...]
data.group_by { |d| d[:hour] }.each do |hour, items|
# create a new row for each hour
row = [hour]
# group the items by date, so we can easily access the value for each date
# grouped values will be a hasg with the date as the key, and the value as the value
# e.g. { '2023-01-01' => [{date: 2023-01-01, value: 1}], '2023-01-02' => [{date: 2023-01-02, value: 1}], ... }
grouped_values = items.group_by { |d| d[:date] }
# now for each unique date we have, we can access the value for that date and append it to the array
dates.each do |date|
row << (grouped_values[date][0][:value] if grouped_values[date].is_a?(Array))
end
# row will look like [22, 0, 0, 1, 4, 6, 7, 4]
# add the row to the result array
result_arr << row
end
# return the resultant array
# the result looks like this
# [
# [nil, '2023-01-01', '2023-1-02', '2023-01-03'],
# [0, 0, 0, 0],
# [1, 0, 0, 0],
# [2, 0, 0, 0],
# [3, 0, 0, 0],
# [4, 0, 0, 0],
# ]
result_arr
end
def generate_heatmap_data_for_timezone(offset)
timezone = ActiveSupport::TimeZone[offset]&.name
timezone_today = DateTime.now.in_time_zone(timezone).beginning_of_day
timezone_data_raw = generate_heatmap_data(timezone_today, offset)
transform_data(timezone_data_raw, false)
end
def generate_heatmap_data(date, offset)
report_params = {
type: :account,
group_by: 'hour',
metric: 'conversations_count',
business_hours: false
}
V2::ReportBuilder.new(Current.account, report_params.merge({
since: since_timestamp(date),
until: until_timestamp(date),
timezone_offset: offset
})).build
end
def transform_data(data, zone_transform)
# rubocop:disable Rails/TimeZone
data.map do |d|
date = zone_transform ? Time.zone.at(d[:timestamp]) : Time.at(d[:timestamp])
{
date: date.to_date.to_s,
hour: date.hour,
value: d[:value]
}
end
# rubocop:enable Rails/TimeZone
end
def since_timestamp(date)
(date - 6.days).to_i.to_s
end
def until_timestamp(date)
date.to_i.to_s
end
end
@@ -27,27 +27,6 @@ module Api::V2::Accounts::ReportsHelper
end
end
def generate_conversations_heatmap_report
report_params = {
type: :account,
group_by: 'hour',
since: params[:since],
until: params[:until],
metric: 'conversations_count',
business_hours: false
}
data = V2::ReportBuilder.new(Current.account, report_params).build
# data format is { timestamp: 1231242342, value: 3}
# we need to convert it to { date: "2020-01-01", hour: 12, value: 3}
#
# the generated report is **always** in UTC timezone
data.map do |d|
date = Time.zone.at(d[:timestamp]).to_s
[date, d[:value]]
end
end
def generate_report(report_params)
V2::ReportBuilder.new(
Current.account,
+16
View File
@@ -0,0 +1,16 @@
/* global axios */
import ApiClient from './ApiClient';
class AuditLogs extends ApiClient {
constructor() {
super('audit_logs', { accountScoped: true });
}
get({ page }) {
const url = page ? `${this.url}?page=${page}` : this.url;
return axios.get(url);
}
}
export default new AuditLogs();
@@ -60,6 +60,13 @@ class ArticlesAPI extends PortalsAPI {
}
);
}
reorderArticles({ portalSlug, reorderedGroup, categorySlug }) {
return axios.post(`${this.url}/${portalSlug}/articles/reorder`, {
positions_hash: reorderedGroup,
category_slug: categorySlug,
});
}
}
export default new ArticlesAPI();
+2 -2
View File
@@ -59,9 +59,9 @@ class ReportsAPI extends ApiClient {
});
}
getConversationTrafficCSV({ from: since, to: until }) {
getConversationTrafficCSV() {
return axios.get(`${this.url}/conversation_traffic`, {
params: { since, until },
params: { timezone_offset: getTimeOffset() },
});
}
+1 -1
View File
@@ -1,5 +1,5 @@
/* global axios */
import wootConstants from 'dashboard/constants';
import wootConstants from 'dashboard/constants/globals';
export const getTestimonialContent = () => {
return axios.get(wootConstants.TESTIMONIAL_URL);
@@ -173,7 +173,7 @@ import ConversationCard from './widgets/conversation/ConversationCard';
import timeMixin from '../mixins/time';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import conversationMixin from '../mixins/conversations';
import wootConstants from '../constants';
import wootConstants from 'dashboard/constants/globals';
import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
+43 -6
View File
@@ -1,8 +1,23 @@
<template>
<div class="code--container">
<button class="button small button--copy-code" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</button>
<div class="code--action-area">
<form
v-if="enableCodePen"
class="code--codeopen-form"
action="https://codepen.io/pen/define"
method="POST"
target="_blank"
>
<input type="hidden" name="data" :value="codepenScriptValue" />
<button type="submit" class="button secondary tiny">
{{ $t('COMPONENTS.CODE.CODEPEN') }}
</button>
</form>
<button class="button secondary tiny" @click="onCopy">
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
</button>
</div>
<highlightjs v-if="script" :language="lang" :code="script" />
</div>
</template>
@@ -21,6 +36,24 @@ export default {
type: String,
default: 'javascript',
},
enableCodePen: {
type: Boolean,
default: false,
},
codepenTitle: {
type: String,
default: 'Chatwoot Codepen',
},
},
computed: {
codepenScriptValue() {
const lang = this.lang === 'javascript' ? 'js' : this.lang;
return JSON.stringify({
title: this.codepenTitle,
private: true,
[lang]: this.script,
});
},
},
methods: {
async onCopy(e) {
@@ -37,10 +70,14 @@ export default {
position: relative;
text-align: left;
.button--copy-code {
margin-top: 0;
.code--action-area {
top: var(--space-small);
position: absolute;
right: 0;
right: var(--space-small);
}
.code--codeopen-form {
display: inline-block;
}
}
</style>
@@ -12,7 +12,8 @@
</template>
<script>
import Banner from 'dashboard/components/ui/Banner.vue';
import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../helper/localStorage';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import { mapGetters } from 'vuex';
import adminMixin from 'dashboard/mixins/isAdmin';
import { hasAnUpdateAvailable } from './versionCheckHelper';
@@ -126,7 +126,7 @@ import WootDropdownSubMenu from 'shared/components/ui/dropdown/DropdownSubMenu.v
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
import wootConstants from '../../constants';
import wootConstants from 'dashboard/constants/globals';
import {
CMD_REOPEN_CONVERSATION,
CMD_RESOLVE_CONVERSATION,
@@ -13,6 +13,7 @@ import DropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import FeatureToggle from './widgets/FeatureToggle';
import HorizontalBar from './widgets/chart/HorizontalBarChart';
import Input from './widgets/forms/Input.vue';
import PhoneInput from './widgets/forms/PhoneInput.vue';
import Label from './ui/Label';
import LoadingState from './widgets/LoadingState';
import Modal from './Modal';
@@ -40,6 +41,7 @@ const WootUIKit = {
FeatureToggle,
HorizontalBar,
Input,
PhoneInput,
Label,
LoadingState,
Modal,
@@ -52,8 +52,9 @@ import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
import WootDropdownDivider from 'shared/components/ui/dropdown/DropdownDivider';
import AvailabilityStatusBadge from '../widgets/conversation/AvailabilityStatusBadge';
import wootConstants from 'dashboard/constants/globals';
const AVAILABILITY_STATUS_KEYS = ['online', 'busy', 'offline'];
const { AVAILABILITY_STATUS_KEYS } = wootConstants;
export default {
components: {
@@ -8,6 +8,7 @@ const settings = accountId => ({
'agent_list',
'attributes_list',
'automation_list',
'auditlogs_list',
'billing_settings_index',
'canned_list',
'general_settings_index',
@@ -150,6 +151,14 @@ const settings = accountId => ({
toStateName: 'billing_settings_index',
showOnlyOnCloud: true,
},
{
icon: 'key',
label: 'AUDIT_LOGS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/settings/audit-log/list`),
toStateName: 'auditlogs_list',
beta: true,
},
],
});
@@ -41,7 +41,7 @@ import PrimaryNavItem from './PrimaryNavItem';
import OptionsMenu from './OptionsMenu';
import AgentDetails from './AgentDetails';
import NotificationBell from './NotificationBell';
import wootConstants from 'dashboard/constants';
import wootConstants from 'dashboard/constants/globals';
import { frontendURL } from 'dashboard/helper/URLHelper';
export default {
@@ -9,7 +9,7 @@
</woot-tabs>
</template>
<script>
import wootConstants from '../../constants';
import wootConstants from 'dashboard/constants/globals';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import { hasPressedAltAndNKey } from 'shared/helpers/KeyboardHelpers';
@@ -1,5 +1,5 @@
<template>
<div class="dashboard-app--container">
<div v-if="hasOpenedAtleastOnce" class="dashboard-app--container">
<div
v-for="(configItem, index) in config"
:key="index"
@@ -35,9 +35,14 @@ export default {
type: Object,
default: () => ({}),
},
isVisible: {
type: Boolean,
default: false,
},
},
data() {
return {
hasOpenedAtleastOnce: false,
iframeLoading: true,
};
},
@@ -57,6 +62,13 @@ export default {
return { id, name, email };
},
},
watch: {
isVisible() {
if (this.isVisible) {
this.hasOpenedAtleastOnce = true;
}
},
},
mounted() {
window.onmessage = e => {
@@ -0,0 +1,148 @@
<template>
<div style="position: relative;">
<woot-button
v-tooltip.top-end="'Compose With AI'"
icon="wand"
color-scheme="secondary"
variant="smooth"
size="small"
:title="'AI'"
@click="toggleDropdown"
/>
<div
v-if="showActionsDropdown"
v-on-clickaway="closeDropdown"
class="dropdown-pane dropdown-pane--open basic-filter"
>
<h3 class="page-sub-title">
Compose with AI
</h3>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Ducimus odio
dolorem laborum mollitia nesciunt corrupti saepe recusandae esse nisi
earum, dignissimos fugiat nobis voluptatibus rerum quo illum. Atque, rem
voluptatem!
</p>
<h4 class="text-block-title">
Tone
</h4>
<div class="filter__item">
<select v-model="activeValue" class="status--filter">
<option v-for="item in items" :key="item.status" :value="item.status">
{{ item.value }}
</option>
</select>
</div>
<div class="medium-12 columns">
<div class="modal-footer justify-content-end w-full buttons">
<woot-button class="button clear" size="tiny">
Cancel
</woot-button>
<woot-button size="tiny">
Generate
</woot-button>
</div>
</div>
</div>
</div>
</template>
<script>
import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
export default {
mixins: [clickaway],
data() {
return {
showActionsDropdown: false,
chatStatusItems: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS'),
chatSortItems: this.$t('CHAT_LIST.CHAT_SORT_FILTER_ITEMS'),
items: [
{
value: 'Professional',
status: 'Professional',
},
],
activeValue: 'Professional',
};
},
computed: {
...mapGetters({
chatStatusFilter: 'getChatStatusFilter',
chatSortFilter: 'getChatSortFilter',
}),
chatStatus() {
return this.chatStatusFilter || wootConstants.STATUS_TYPE.OPEN;
},
sortFilter() {
return this.chatSortFilter || wootConstants.SORT_BY_TYPE.LATEST;
},
},
methods: {
onTabChange(value) {
this.$emit('changeFilter', value);
this.closeDropdown();
},
toggleDropdown() {
this.showActionsDropdown = !this.showActionsDropdown;
},
closeDropdown() {
this.showActionsDropdown = false;
},
onChangeFilter(type, value) {
this.$emit('changeFilter', type, value);
},
},
};
</script>
<style lang="scss" scoped>
.basic-filter {
width: 400px;
margin-top: var(--space-smaller);
right: 0;
left: 0;
padding: var(--space-normal) var(--space-small);
bottom: 34px;
position: absolute;
span {
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
}
.filter__item {
justify-content: space-between;
display: flex;
align-items: center;
&:last-child {
margin-top: var(--space-normal);
}
span {
font-size: var(--font-size-mini);
}
}
}
.icon {
margin-right: var(--space-smaller);
}
.dropdown-icon {
margin-left: var(--space-smaller);
}
.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);
}
.buttons {
display: flex;
justify-content: flex-end;
margin-top: var(--space-normal);
}
</style>
@@ -91,6 +91,7 @@
v-if="(isAWebWidgetInbox || isAPIInbox) && !isOnPrivateNote"
:conversation-id="conversationId"
/>
<AIEditor />
<transition name="modal-fade">
<div
v-show="$refs.upload && $refs.upload.dropActive"
@@ -124,6 +125,7 @@ import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import inboxMixin from 'shared/mixins/inboxMixin';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import AIEditor from './AIEditor.vue';
import {
ALLOWED_FILE_TYPES,
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
@@ -134,7 +136,7 @@ import { mapGetters } from 'vuex';
export default {
name: 'ReplyBottomPanel',
components: { FileUpload, VideoCallButton },
components: { FileUpload, VideoCallButton, AIEditor },
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
props: {
mode: {
@@ -11,7 +11,7 @@
</template>
<script>
import wootConstants from '../../../constants';
import wootConstants from 'dashboard/constants/globals';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import { hasPressedAltAndBKey } from 'shared/helpers/KeyboardHelpers';
@@ -23,7 +23,7 @@
:show-badge="false"
/>
</woot-tabs>
<div v-if="!activeIndex" class="messages-and-sidebar">
<div v-show="!activeIndex" class="messages-and-sidebar">
<messages-view
v-if="currentChat.id"
:inbox-id="inboxId"
@@ -41,9 +41,11 @@
</div>
</div>
<dashboard-app-frame
v-else
:key="currentChat.id + '-' + activeIndex"
:config="dashboardApps[activeIndex - 1].content"
v-for="(dashboardApp, index) in dashboardApps"
v-show="activeIndex - 1 === index"
:key="currentChat.id + '-' + dashboardApp.id"
:is-visible="activeIndex - 1 === index"
:config="dashboardApps[index].content"
:current-chat="currentChat"
/>
</div>
@@ -112,6 +114,7 @@ export default {
},
'currentChat.id'() {
this.fetchLabels();
this.activeIndex = 0;
},
},
mounted() {
@@ -63,7 +63,7 @@ import inboxMixin from 'shared/mixins/inboxMixin';
import InboxName from '../InboxName';
import MoreActions from './MoreActions';
import Thumbnail from '../Thumbnail';
import wootConstants from '../../../constants';
import wootConstants from 'dashboard/constants/globals';
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
export default {
components: {
@@ -444,9 +444,9 @@ export default {
this.hasImageError = true;
},
openContextMenu(e) {
const shouldSkipContextMenu = e.target?.classList.contains(
'skip-context-menu'
);
const shouldSkipContextMenu =
e.target?.classList.contains('skip-context-menu') ||
e.target?.tagName.toLowerCase() === 'a';
if (shouldSkipContextMenu || getSelection().toString()) {
return;
}
@@ -174,9 +174,10 @@ import inboxMixin from 'shared/mixins/inboxMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { DirectUpload } from 'activestorage';
import { frontendURL } from '../../../helper/URLHelper';
import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../../helper/localStorage';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import { trimContent, debounce } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants';
import wootConstants from 'dashboard/constants/globals';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import rtlMixin from 'shared/mixins/rtlMixin';
@@ -69,7 +69,7 @@
<script>
import MenuItem from './menuItem.vue';
import MenuItemWithSubmenu from './menuItemWithSubmenu.vue';
import wootConstants from 'dashboard/constants.js';
import wootConstants from 'dashboard/constants/globals';
import snoozeTimesMixin from 'dashboard/mixins/conversation/snoozeTimesMixin';
import { mapGetters } from 'vuex';
import AgentLoadingPlaceholder from './agentLoadingPlaceholder.vue';
@@ -2,6 +2,6 @@ import emailValidator from 'vuelidate/lib/validators/email';
export const validEmailsByComma = value => {
if (!value.length) return true;
const emails = value.split(',');
const emails = value.replace(/\s+/g, '').split(',');
return emails.every(email => emailValidator(email));
};
@@ -10,4 +10,7 @@ describe('#validEmailsByComma', () => {
it('returns false when one of the email passed is invalid', () => {
expect(validEmailsByComma('ni@njan.com,pova.da')).toEqual(false);
});
it('strips spaces between emails before validating', () => {
expect(validEmailsByComma('1@test.com , 2@test.com')).toEqual(true);
});
});
@@ -0,0 +1,361 @@
<template>
<div class="phone-input--wrap">
<div class="phone-input" :class="{ 'has-error': error }">
<div class="country-emoji--wrap" @click="toggleCountryDropdown">
<h5 v-if="activeCountry.emoji">{{ activeCountry.emoji }}</h5>
<fluent-icon v-else icon="globe" class="fluent-icon" size="16" />
<fluent-icon icon="chevron-down" class="fluent-icon" size="12" />
</div>
<span v-if="activeDialCode" class="country-dial--code">
{{ activeDialCode }}
</span>
<input
:value="phoneNumber"
type="tel"
class="phone-input--field"
:placeholder="placeholder"
:readonly="readonly"
:style="styles"
@input="onChange"
@blur="onBlur"
/>
</div>
<div v-if="showDropdown" ref="dropdown" class="country-dropdown">
<div class="dropdown-search--wrap">
<input
ref="searchbar"
v-model="searchCountry"
type="text"
placeholder="Search"
class="dropdown-search"
/>
</div>
<div
v-for="(country, index) in filteredCountriesBySearch"
ref="dropdownItem"
:key="index"
class="country-dropdown--item"
:class="{
active: country.id === activeCountryCode,
focus: index === selectedIndex,
}"
@click="onSelectCountry(country)"
>
<span class="country-emoji">{{ country.emoji }}</span>
<span class="country-name">
{{ country.name }}
</span>
<span class="country-dial-code">{{ country.dial_code }}</span>
</div>
<div v-if="filteredCountriesBySearch.length === 0">
<span class="no-results">No results found</span>
</div>
</div>
</div>
</template>
<script>
import countries from 'shared/constants/countries.js';
import parsePhoneNumber from 'libphonenumber-js';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import {
hasPressedArrowUpKey,
hasPressedArrowDownKey,
isEnter,
} from 'shared/helpers/KeyboardHelpers';
export default {
mixins: [eventListenerMixins],
props: {
value: {
type: [String, Number],
default: '',
},
placeholder: {
type: String,
default: '',
},
readonly: {
type: Boolean,
default: false,
},
styles: {
type: Object,
default: () => {},
},
error: {
type: Boolean,
default: false,
},
},
data() {
return {
countries: [
{
name: 'Select Country',
dial_code: '',
emoji: '',
id: '',
},
...countries,
],
selectedIndex: -1,
showDropdown: false,
searchCountry: '',
activeCountryCode: '',
activeDialCode: '',
phoneNumber: this.value,
};
},
computed: {
filteredCountriesBySearch() {
return this.countries.filter(country => {
const { name, dial_code, id } = country;
const search = this.searchCountry.toLowerCase();
return (
name.toLowerCase().includes(search) ||
dial_code.toLowerCase().includes(search) ||
id.toLowerCase().includes(search)
);
});
},
activeCountry() {
if (this.activeCountryCode) {
return this.countries.find(
country => country.id === this.activeCountryCode
);
}
return '';
},
},
watch: {
value() {
const number = parsePhoneNumber(this.value);
if (number) {
this.activeCountryCode = number.country;
this.activeDialCode = `+${number.countryCallingCode}`;
this.phoneNumber = this.value.replace(
`+${number.countryCallingCode}`,
''
);
}
},
},
mounted() {
window.addEventListener('mouseup', this.onOutsideClick);
this.setActiveCountry();
},
beforeDestroy() {
window.removeEventListener('mouseup', this.onOutsideClick);
},
methods: {
onOutsideClick(e) {
if (
this.showDropdown &&
e.target !== this.$refs.dropdown &&
!this.$refs.dropdown.contains(e.target)
) {
this.closeDropdown();
}
},
onChange(e) {
this.phoneNumber = e.target.value;
this.$emit('input', e.target.value, this.activeDialCode);
},
onBlur(e) {
this.$emit('blur', e.target.value);
},
dropdownItem() {
return Array.from(
this.$refs.dropdown.querySelectorAll(
'div.country-dropdown div.country-dropdown--item'
)
);
},
focusedItem() {
return Array.from(
this.$refs.dropdown.querySelectorAll('div.country-dropdown div.focus')
);
},
focusedItemIndex() {
return Array.from(this.dropdownItem()).indexOf(this.focusedItem()[0]);
},
onKeyDownHandler(e) {
const { showDropdown, filteredCountriesBySearch, onSelectCountry } = this;
const { selectedIndex } = this;
if (showDropdown) {
if (hasPressedArrowDownKey(e)) {
e.preventDefault();
this.selectedIndex = Math.min(
selectedIndex + 1,
filteredCountriesBySearch.length - 1
);
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28;
} else if (hasPressedArrowUpKey(e)) {
e.preventDefault();
this.selectedIndex = Math.max(selectedIndex - 1, 0);
this.$refs.dropdown.scrollTop = this.focusedItemIndex() * 28 - 56;
} else if (isEnter(e)) {
e.preventDefault();
onSelectCountry(filteredCountriesBySearch[selectedIndex]);
}
}
},
onSelectCountry(country) {
this.activeCountryCode = country.id;
this.searchCountry = '';
this.activeDialCode = country.dial_code;
this.$emit('setCode', country.dial_code);
this.closeDropdown();
},
setActiveCountry() {
const { phoneNumber } = this;
if (!phoneNumber) return;
const number = parsePhoneNumber(phoneNumber);
if (number) {
this.activeCountryCode = number.country;
this.activeDialCode = number.countryCallingCode;
}
},
toggleCountryDropdown() {
this.showDropdown = !this.showDropdown;
this.selectedIndex = -1;
if (this.showDropdown) {
this.$nextTick(() => {
this.$refs.searchbar.focus();
});
}
},
closeDropdown() {
this.selectedIndex = -1;
this.showDropdown = false;
},
},
};
</script>
<style scoped lang="scss">
.phone-input--wrap {
position: relative;
.phone-input {
display: flex;
align-items: center;
justify-content: flex-start;
margin-bottom: var(--space-normal);
border: 1px solid var(--s-200);
border-radius: var(--border-radius-normal);
&.has-error {
border: 1px solid var(--r-400);
}
}
.country-emoji--wrap {
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: var(--space-small);
background: var(--s-25);
height: 4rem;
width: 5.2rem;
border-radius: var(--border-radius-normal) 0 0 var(--border-radius-normal);
padding: var(--space-small) var(--space-smaller) var(--space-small)
var(--space-small);
h5 {
margin-bottom: 0;
}
}
.country-dial--code {
display: flex;
color: var(--s-300);
font-size: var(--space-normal);
font-weight: normal;
line-height: 1.5;
padding: var(--space-small) 0 var(--space-small) var(--space-small);
}
.phone-input--field {
margin-bottom: 0;
border: 0;
}
.country-dropdown {
z-index: var(--z-index-low);
position: absolute;
height: var(--space-giga);
width: 20rem;
overflow-y: auto;
top: 4rem;
border-radius: var(--border-radius-default);
padding: 0 0 var(--space-smaller) 0;
background-color: var(--white);
box-shadow: var(--shadow-context-menu);
border-radius: var(--border-radius-normal);
.dropdown-search--wrap {
top: 0;
position: sticky;
background-color: var(--white);
padding: var(--space-smaller);
.dropdown-search {
height: var(--space-large);
margin-bottom: 0;
font-size: var(--font-size-small);
border: 1px solid var(--s-200) !important;
}
}
.country-dropdown--item {
display: flex;
align-items: center;
height: 2.8rem;
padding: 0 var(--space-smaller);
cursor: pointer;
&.active {
background-color: var(--s-50);
}
&.focus {
background-color: var(--s-25);
}
&:hover {
background-color: var(--s-50);
}
.country-emoji {
font-size: var(--font-size-default);
margin-right: var(--space-smaller);
}
.country-name {
max-width: 12rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.country-dial-code {
margin-left: var(--space-smaller);
color: var(--s-300);
font-size: var(--font-size-mini);
}
}
.no-results {
display: flex;
align-items: center;
justify-content: center;
color: var(--s-500);
margin-top: var(--space-normal);
}
}
}
</style>
@@ -24,5 +24,6 @@ export default {
DOCS_URL: '//www.chatwoot.com/docs/product/',
TESTIMONIAL_URL: 'https://testimonials.cdn.chatwoot.com/content.json',
SMALL_SCREEN_BREAKPOINT: 1024,
AVAILABILITY_STATUS_KEYS: ['online', 'busy', 'offline'],
};
export const DEFAULT_REDIRECT_URL = '/app/';
@@ -0,0 +1,5 @@
export const LOCAL_STORAGE_KEYS = {
DISMISSED_UPDATES: 'dismissedUpdates',
WIDGET_BUILDER: 'widgetBubble_',
DRAFT_MESSAGES: 'draftMessages',
};
+1 -1
View File
@@ -1,4 +1,4 @@
import { DEFAULT_REDIRECT_URL } from '../constants';
import { DEFAULT_REDIRECT_URL } from 'dashboard/constants/globals';
export const frontendURL = (path, params) => {
const stringifiedParams = params ? `?${new URLSearchParams(params)}` : '';
@@ -57,6 +57,7 @@ export const initializeChatwootEvents = () => {
window.$chatwoot.setCustomAttributes({
signedUpAt: user.created_at,
cloudCustomer: 'true',
account_id: user.account_id,
});
}
@@ -74,6 +74,11 @@
"LABEL": "عنوان البريد الإلكتروني",
"PLACEHOLDER": "الرجاء إدخال عنوان البريد الإلكتروني للموظف"
},
"AGENT_AVAILABILITY": {
"LABEL": "التوفر",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "تعديل حساب الموظف"
},
"BUTTON_TEXT": "تعديل",
@@ -132,6 +132,17 @@
"PLACEHOLDER": "أدخل اسم الشركة",
"LABEL": "اسم الشركة"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "اسم الدولة",
"SELECT_PLACEHOLDER": "اختر",
"REMOVE": "حذف",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "أدخل اسم مستخدم فيسبوك",
@@ -439,7 +439,8 @@
"LABEL": "الخصائص",
"DISPLAY_FILE_PICKER": "عرض أداة انتقاء الملفات في الـ widget",
"DISPLAY_EMOJI_PICKER": "عرض منتقي الرموز التعبيرية على الـ widget",
"ALLOW_END_CONVERSATION": "السماح للمستخدمين بإنهاء المحادثة من عنصر واجهة المستخدم"
"ALLOW_END_CONVERSATION": "السماح للمستخدمين بإنهاء المحادثة من عنصر واجهة المستخدم",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "كود \"الماسنجر\"",
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "نسخ",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "تم نسخ الكود إلى الحافظة بنجاح"
},
"SHOW_MORE_BLOCK": {
@@ -74,6 +74,11 @@
"LABEL": "Имейл адрес",
"PLACEHOLDER": "Моля, въведете имейл адрес на агента"
},
"AGENT_AVAILABILITY": {
"LABEL": "Availability",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Редактирай агента"
},
"BUTTON_TEXT": "Редактирай",
@@ -132,6 +132,17 @@
"PLACEHOLDER": "Въведете име на фирма",
"LABEL": "Име на фирма"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Име на държавата",
"SELECT_PLACEHOLDER": "Select",
"REMOVE": "Remove",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Въведете Facebook потребителско име",
@@ -439,7 +439,8 @@
"LABEL": "Features",
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget",
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget"
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger Script",
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Copy",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "Code copied to clipboard successfully"
},
"SHOW_MORE_BLOCK": {
@@ -74,6 +74,11 @@
"LABEL": "Adreça de correu electrònic",
"PLACEHOLDER": "Introduïu l'adreça de correu electrònic de l'agent"
},
"AGENT_AVAILABILITY": {
"LABEL": "Disponibilitat",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Editar l'agent"
},
"BUTTON_TEXT": "Edita",
@@ -132,6 +132,17 @@
"PLACEHOLDER": "Introdueix el nom de la companyia",
"LABEL": "Nom de la companyia"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Nom del país",
"SELECT_PLACEHOLDER": "Select",
"REMOVE": "Suprimeix",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Introduïu el nom d'usuari de Facebook",
@@ -439,7 +439,8 @@
"LABEL": "Característiques",
"DISPLAY_FILE_PICKER": "Mostra el selector de fitxers al widget",
"DISPLAY_EMOJI_PICKER": "Mostra el selector d'emoji al widget",
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget"
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Script del missatger",
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Copia",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "El codi s'ha copiat al porta-retalls amb èxit"
},
"SHOW_MORE_BLOCK": {
@@ -74,6 +74,11 @@
"LABEL": "E-mailová adresa",
"PLACEHOLDER": "Zadejte prosím e-mailovou adresu agenta"
},
"AGENT_AVAILABILITY": {
"LABEL": "Dostupnost",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Upravit agenta"
},
"BUTTON_TEXT": "Upravit",
@@ -132,6 +132,17 @@
"PLACEHOLDER": "Zadejte název společnosti",
"LABEL": "Název společnosti"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Country Name",
"SELECT_PLACEHOLDER": "Select",
"REMOVE": "Odebrat",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Zadejte uživatelské jméno na Facebooku",
@@ -439,7 +439,8 @@
"LABEL": "Funkce",
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget",
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget"
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger skript",
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Kopírovat",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "Kód byl úspěšně zkopírován do schránky"
},
"SHOW_MORE_BLOCK": {
@@ -74,6 +74,11 @@
"LABEL": "E-Mail Adresse",
"PLACEHOLDER": "Indtast venligst en e-mail adresse på agenten"
},
"AGENT_AVAILABILITY": {
"LABEL": "Tilgængelighed",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Rediger Agent"
},
"BUTTON_TEXT": "Rediger",
@@ -132,6 +132,17 @@
"PLACEHOLDER": "Indtast virksomhedens navn",
"LABEL": "Virksomhedens Navn"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Land Navn",
"SELECT_PLACEHOLDER": "Vælg",
"REMOVE": "Fjern",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Indtast Facebook brugernavn",
@@ -439,7 +439,8 @@
"LABEL": "Funktioner",
"DISPLAY_FILE_PICKER": "Vis filvælger på widget'en",
"DISPLAY_EMOJI_PICKER": "Vis emoji-vælger på widget'en",
"ALLOW_END_CONVERSATION": "Tillad brugere at afslutte samtale fra widget"
"ALLOW_END_CONVERSATION": "Tillad brugere at afslutte samtale fra widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger- Script",
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Kopiér",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "Kode kopieret til udklipsholder med succes"
},
"SHOW_MORE_BLOCK": {
@@ -74,6 +74,11 @@
"LABEL": "E-Mail-Addresse",
"PLACEHOLDER": "Bitte geben Sie eine E-Mail-Adresse des Agenten ein"
},
"AGENT_AVAILABILITY": {
"LABEL": "Verfügbarkeit",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Agent bearbeiten"
},
"BUTTON_TEXT": "Bearbeiten",
@@ -132,6 +132,17 @@
"PLACEHOLDER": "Firmenname eingeben",
"LABEL": "Firmenname"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Ländername",
"SELECT_PLACEHOLDER": "Auswählen",
"REMOVE": "Entfernen",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Facebook-Benutzername eingeben",
@@ -439,7 +439,8 @@
"LABEL": "Funktionen",
"DISPLAY_FILE_PICKER": "Dateiauswahl im Widget anzeigen",
"DISPLAY_EMOJI_PICKER": "Emoji-Auswahl im Widget anzeigen",
"ALLOW_END_CONVERSATION": "Benutzern erlauben, die Konversation vom Widget zu beenden"
"ALLOW_END_CONVERSATION": "Benutzern erlauben, die Konversation vom Widget zu beenden",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger-Skript",
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Kopieren",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "Code erfolgreich in die Zwischenablage kopiert"
},
"SHOW_MORE_BLOCK": {
@@ -74,6 +74,11 @@
"LABEL": "Διεύθυνση Email",
"PLACEHOLDER": "Παρακαλώ εισάγετε το email του πράκτορα"
},
"AGENT_AVAILABILITY": {
"LABEL": "Διαθεσιμότητα",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Καταχώρηση"
},
"BUTTON_TEXT": "Επεξεργασία",
@@ -132,6 +132,17 @@
"PLACEHOLDER": "Εισάγετε το όνομα της εταιρείας",
"LABEL": "Όνομα Εταιρείας"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Όνομα Χώρας",
"SELECT_PLACEHOLDER": "Επιλογή",
"REMOVE": "Διαγραφή",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Εισάγετε το όνομα χρήστη στο Facebook",
@@ -439,7 +439,8 @@
"LABEL": "Χαρακτηριστικά",
"DISPLAY_FILE_PICKER": "Να εμφανίζεται η επιλογή αρχείου στο widget",
"DISPLAY_EMOJI_PICKER": "Να εμφανίζεται ο επιλογές emoji στο widget",
"ALLOW_END_CONVERSATION": "Επιτρέψτε στους χρήστες να τερματίσουν τη συνομιλία από το widget"
"ALLOW_END_CONVERSATION": "Επιτρέψτε στους χρήστες να τερματίσουν τη συνομιλία από το widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Κώδικας (Script)",
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Αντιγραφή",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "Ο κώδικας αντιγράφτηκε με επιτυχία στο πρόχειρο"
},
"SHOW_MORE_BLOCK": {
@@ -33,6 +33,7 @@
"PLACEHOLDER": "Please select a role",
"ERROR": "Role is required"
},
"EMAIL": {
"LABEL": "Email Address",
"PLACEHOLDER": "Please enter an email address of the agent"
@@ -74,6 +75,11 @@
"LABEL": "Email Address",
"PLACEHOLDER": "Please enter an email address of the agent"
},
"AGENT_AVAILABILITY": {
"LABEL": "Availability",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Edit Agent"
},
"BUTTON_TEXT": "Edit",
@@ -0,0 +1,24 @@
{
"AUDIT_LOGS": {
"HEADER": "Audit Logs",
"HEADER_BTN_TXT": "Add Audit Logs",
"LOADING": "Fetching Audit Logs",
"SEARCH_404": "There are no items matching this query",
"SIDEBAR_TXT": "<p><b>Audit Logs</b> </p><p> Audit Logs are trails for events and actions in a Chatwoot System. </p>",
"LIST": {
"404": "There are no Audit Logs available in this account.",
"TITLE": "Manage Audit Logs",
"DESC": "Audit Logs are trails for events and actions in a Chatwoot System.",
"TABLE_HEADER": [
"User",
"Action",
"IP Address",
"Time"
]
},
"API": {
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
}
}
}
@@ -120,8 +120,9 @@
"PHONE_NUMBER": {
"PLACEHOLDER": "Enter the phone number of the contact",
"LABEL": "Phone Number",
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]",
"HELP": "Phone number should be of E.164 format eg: +1415555555 [+][country code][area code][local phone number]. You can select the dial code from the dropdown.",
"ERROR": "Phone number should be either empty or of E.164 format",
"DIAL_CODE_ERROR": "Please select a dial code from the list",
"DUPLICATE": "This phone number is in use for another contact."
},
"LOCATION": {
@@ -439,7 +439,8 @@
"LABEL": "Features",
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget",
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget"
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger Script",
@@ -477,6 +478,8 @@
"WHATSAPP_SECTION_UPDATE_TITLE": "Update API Key",
"WHATSAPP_SECTION_UPDATE_PLACEHOLDER": "Enter the new API Key here",
"WHATSAPP_SECTION_UPDATE_BUTTON": "Update",
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verify Token",
"WHATSAPP_WEBHOOK_SUBHEADER": "This token is used to verify the authenticity of the webhook endpoint.",
"UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings"
},
"AUTO_ASSIGNMENT": {
@@ -2,6 +2,7 @@ import advancedFilters from './advancedFilters.json';
import agentBots from './agentBots.json';
import agentMgmt from './agentMgmt.json';
import attributesMgmt from './attributesMgmt.json';
import auditLogs from './auditLogs.json';
import automation from './automation.json';
import bulkActions from './bulkActions.json';
import campaign from './campaign.json';
@@ -34,6 +35,7 @@ export default {
...agentBots,
...agentMgmt,
...attributesMgmt,
...auditLogs,
...automation,
...bulkActions,
...campaign,
@@ -160,6 +160,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Copy",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "Copied to clipboard"
},
"SHOW_MORE_BLOCK": {
@@ -198,6 +199,7 @@
"HOME": "Home",
"AGENTS": "Agents",
"AGENT_BOTS": "Bots",
"AUDIT_LOGS": "Audit Logs",
"INBOXES": "Inboxes",
"NOTIFICATIONS": "Notifications",
"CANNED_RESPONSES": "Canned Responses",
@@ -23,7 +23,7 @@
"is_greater_than": "Es mayor que",
"is_less_than": "Es menor que",
"days_before": "Es X días antes",
"starts_with": "Starts with"
"starts_with": "Empieza con"
},
"ATTRIBUTE_LABELS": {
"TRUE": "Verdadero",
@@ -74,6 +74,11 @@
"LABEL": "Dirección de email",
"PLACEHOLDER": "Por favor, introduzca una dirección de correo electrónico del agente"
},
"AGENT_AVAILABILITY": {
"LABEL": "Disponibilidad",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Editar agente"
},
"BUTTON_TEXT": "Editar",
@@ -132,6 +132,17 @@
"PLACEHOLDER": "Escriba el nombre de la empresa",
"LABEL": "Empresa"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Nombre del país",
"SELECT_PLACEHOLDER": "Seleccionar",
"REMOVE": "Eliminar",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Escriba el usuario de Facebook",
@@ -439,7 +439,8 @@
"LABEL": "Características",
"DISPLAY_FILE_PICKER": "Mostrar el selector de archivos en el widget",
"DISPLAY_EMOJI_PICKER": "Mostrar el selector de emoji en el widget",
"ALLOW_END_CONVERSATION": "Permitir a los usuarios finalizar conversaciones desde el widget"
"ALLOW_END_CONVERSATION": "Permitir a los usuarios finalizar conversaciones desde el widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Script de Messenger",
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Copiar",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "Código copiado al portapapeles con éxito"
},
"SHOW_MORE_BLOCK": {
@@ -23,7 +23,7 @@
"is_greater_than": "بزرگتر از",
"is_less_than": "کوچکتر از",
"days_before": "x روز قبل است",
"starts_with": "Starts with"
"starts_with": "شروع می‌‌شود با"
},
"ATTRIBUTE_LABELS": {
"TRUE": "درست",
@@ -74,6 +74,11 @@
"LABEL": "ایمیل",
"PLACEHOLDER": "لطفا ایمیل اپراتور را وارد کنید"
},
"AGENT_AVAILABILITY": {
"LABEL": "در دسترس",
"PLACEHOLDER": "لطفا یک وضعیت در دسترس بودن را انتخاب کنید",
"ERROR": "در دسترس بودن الزامی است"
},
"SUBMIT": "تغییر ایجنت"
},
"BUTTON_TEXT": "ویرایش",
@@ -132,6 +132,17 @@
"PLACEHOLDER": "نام شرکت را وارد کنید",
"LABEL": "نام شرکت"
},
"COUNTRY": {
"PLACEHOLDER": "نام کشور را وارد کنید",
"LABEL": "نام کشور",
"SELECT_PLACEHOLDER": "انتخاب کنید",
"REMOVE": "حذف",
"SELECT_COUNTRY": "کشور را انتخاب کنید"
},
"CITY": {
"PLACEHOLDER": "نام شهر را وارد کنید",
"LABEL": "نام شهر"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "نام‌کاربری فیس‌بوک را وارد کنید",
@@ -167,8 +167,8 @@
"DELETE": "حذف",
"CREATE_A_CANNED_RESPONSE": "اضافه کردن به پاسخ‌های آماده",
"TRANSLATE": "ترجمه",
"COPY_PERMALINK": "Copy link to the message",
"LINK_COPIED": "Message URL copied to the clipboard",
"COPY_PERMALINK": "کپی پیوند پیام",
"LINK_COPIED": "نشانی اینترنتی پیام در کلیپ‌بورد کپی شد",
"DELETE_CONFIRMATION": {
"TITLE": "آیا از حذف این پیام مطمئن هستید؟",
"MESSAGE": "شما نمی توانید این عمل را لغو کنید",
@@ -439,7 +439,8 @@
"LABEL": "امکانات",
"DISPLAY_FILE_PICKER": "نمایش گزینه ضمیه فایل در ویجت",
"DISPLAY_EMOJI_PICKER": "نمایش گزینه انتخاب ایموجی در ویجت",
"ALLOW_END_CONVERSATION": "به کاربران اجازه دهید مکالمه را از ویجت پایان دهند"
"ALLOW_END_CONVERSATION": "به کاربران اجازه دهید مکالمه را از ویجت پایان دهند",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "اسکریپت ویجت",
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "کپی",
"CODEPEN": "در CodePen باز کنید",
"COPY_SUCCESSFUL": "کد به حافظه کپی شد"
},
"SHOW_MORE_BLOCK": {
@@ -74,6 +74,11 @@
"LABEL": "Sähköpostiosoite",
"PLACEHOLDER": "Ole hyvä ja kirjoita edustajan sähköpostiosoite"
},
"AGENT_AVAILABILITY": {
"LABEL": "Saatavuus",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Muokkaa edustajaa"
},
"BUTTON_TEXT": "Muokkaa",
@@ -132,6 +132,17 @@
"PLACEHOLDER": "Anna yrityksen nimi",
"LABEL": "Yrityksen nimi"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Country Name",
"SELECT_PLACEHOLDER": "Select",
"REMOVE": "Poista",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Syötä Facebookin käyttäjätunnus",
@@ -439,7 +439,8 @@
"LABEL": "Ominaisuudet",
"DISPLAY_FILE_PICKER": "Näytä liitevalitsin widgetissä",
"DISPLAY_EMOJI_PICKER": "Näytä emojivalitsin widgetissä",
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget"
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger-skripti",
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Kopioi",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "Koodi kopioitu leikepöydälle onnistuneesti"
},
"SHOW_MORE_BLOCK": {
@@ -74,6 +74,11 @@
"LABEL": "Adresse de courriel",
"PLACEHOLDER": "Veuillez entrer l'adresse de courriel de l'agent"
},
"AGENT_AVAILABILITY": {
"LABEL": "Disponibilité",
"PLACEHOLDER": "Please select an availability status",
"ERROR": "Availability is required"
},
"SUBMIT": "Modifier l'agent"
},
"BUTTON_TEXT": "Modifier",
@@ -132,6 +132,17 @@
"PLACEHOLDER": "Entrez le nom de la société",
"LABEL": "Nom de la société"
},
"COUNTRY": {
"PLACEHOLDER": "Enter the country name",
"LABEL": "Nom du pays",
"SELECT_PLACEHOLDER": "Select",
"REMOVE": "Supprimer",
"SELECT_COUNTRY": "Select Country"
},
"CITY": {
"PLACEHOLDER": "Enter the city name",
"LABEL": "City Name"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Entrez le nom d'utilisateur Facebook",
@@ -439,7 +439,8 @@
"LABEL": "Fonctionnalités",
"DISPLAY_FILE_PICKER": "Afficher le sélecteur de fichiers sur le widget",
"DISPLAY_EMOJI_PICKER": "Afficher le sélecteur d'émoticônes sur le widget",
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget"
"ALLOW_END_CONVERSATION": "Allow users to end conversation from the widget",
"USE_INBOX_AVATAR_FOR_BOT": "Use inbox name and avatar for the bot"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Script du Widget Web",
@@ -164,6 +164,7 @@
"COMPONENTS": {
"CODE": {
"BUTTON_TEXT": "Copier",
"CODEPEN": "Open in CodePen",
"COPY_SUCCESSFUL": "Code copié dans le presse-papier avec succès"
},
"SHOW_MORE_BLOCK": {
@@ -23,7 +23,7 @@
"is_greater_than": "גדול מ",
"is_less_than": "קטן מ",
"days_before": "זה x ימים לפני",
"starts_with": "Starts with"
"starts_with": "מתחיל עם"
},
"ATTRIBUTE_LABELS": {
"TRUE": "נכון",
@@ -74,6 +74,11 @@
"LABEL": "כתובת מייל",
"PLACEHOLDER": "אנא הכנס כתובת מייל של הסוכן"
},
"AGENT_AVAILABILITY": {
"LABEL": "זמינות",
"PLACEHOLDER": "אנא בחר סטטוס זמינות",
"ERROR": "נדרשת זמינות"
},
"SUBMIT": "ערוך סוכן"
},
"BUTTON_TEXT": "ערוך",

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