Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9551c6f68 | ||
|
|
9c0259da6b | ||
|
|
ea2c442328 | ||
|
|
b93b8cdab6 | ||
|
|
e6505fc7a4 | ||
|
|
c5c36af529 | ||
|
|
026e03c307 | ||
|
|
37b7098673 | ||
|
|
cb2f86b983 | ||
|
|
905e77048f | ||
|
|
4505c5dda3 | ||
|
|
c9ce9e5b8f | ||
|
|
9e2f991484 | ||
|
|
80dcd17f6e | ||
|
|
a38ecf3dde | ||
|
|
9090eabb8a | ||
|
|
ca2506a941 | ||
|
|
1886d4ce08 | ||
|
|
4d49b81f1c | ||
|
|
2b736f4698 | ||
|
|
9ca21df9fd | ||
|
|
44837aa657 | ||
|
|
d04344c094 | ||
|
|
d45512df72 | ||
|
|
2731c2a5be | ||
|
|
610463c980 | ||
|
|
09ce85b30d | ||
|
|
17ff1f11a7 | ||
|
|
f4e121cc44 | ||
|
|
cf934450ab | ||
|
|
ad75a79135 | ||
|
|
d49989ace1 | ||
|
|
e69e0bc984 | ||
|
|
e877b01e00 | ||
|
|
da11feb39b | ||
|
|
138afd9af6 | ||
|
|
e753365493 | ||
|
|
91dc7733b0 | ||
|
|
463c09184c | ||
|
|
040e9a732f | ||
|
|
71c5a1e1d4 | ||
|
|
a521762dd6 | ||
|
|
406e8405eb |
@@ -1,8 +1,6 @@
|
||||
.bundle
|
||||
.env
|
||||
.env.*
|
||||
.git
|
||||
.gitignore
|
||||
docker-compose.*
|
||||
docker/Dockerfile
|
||||
docker/dockerfiles
|
||||
|
||||
+2
-3
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
+3
@@ -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,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
Reference in New Issue
Block a user