Compare commits
67
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bbb28c432 | ||
|
|
cf91e9eb58 | ||
|
|
b529baa5eb | ||
|
|
c071f66cdd | ||
|
|
92fa9c4fdc | ||
|
|
f6e0453bb2 | ||
|
|
1a07828b1b | ||
|
|
54880b2342 | ||
|
|
e32f3e71e4 | ||
|
|
0874aeee2d | ||
|
|
f1fc658a0d | ||
|
|
e3193dcabc | ||
|
|
f825a22997 | ||
|
|
815322b27a | ||
|
|
474e65f4c8 | ||
|
|
3a35281b3f | ||
|
|
ac6de50b4d | ||
|
|
a34729c153 | ||
|
|
6b2736aa63 | ||
|
|
d1584eea72 | ||
|
|
527042afd1 | ||
|
|
bd1e69e4b4 | ||
|
|
76d4c22c2d | ||
|
|
5cdc7d654a | ||
|
|
821d49943a | ||
|
|
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
|
||||
|
||||
@@ -71,6 +71,10 @@ Rails/ApplicationController:
|
||||
- 'app/controllers/platform_controller.rb'
|
||||
- 'app/controllers/public_controller.rb'
|
||||
- 'app/controllers/survey/responses_controller.rb'
|
||||
Rails/FindEach:
|
||||
Enabled: true
|
||||
Include:
|
||||
- 'app/**/*.rb'
|
||||
Rails/CompactBlank:
|
||||
Enabled: false
|
||||
Rails/EnvironmentVariableAccess:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
# Variables
|
||||
APP_NAME := chatwoot
|
||||
RAILS_ENV ?= development
|
||||
|
||||
# Targets
|
||||
setup:
|
||||
gem install bundler
|
||||
bundle install
|
||||
yarn install
|
||||
|
||||
db_create:
|
||||
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:create
|
||||
|
||||
db_migrate:
|
||||
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:migrate
|
||||
|
||||
db_seed:
|
||||
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:seed
|
||||
|
||||
db:
|
||||
RAILS_ENV=$(RAILS_ENV) bundle exec rails db:chatwoot_prepare
|
||||
|
||||
console:
|
||||
RAILS_ENV=$(RAILS_ENV) bundle exec rails console
|
||||
|
||||
server:
|
||||
RAILS_ENV=$(RAILS_ENV) bundle exec rails server -b 0.0.0.0 -p 3000
|
||||
|
||||
burn:
|
||||
bundle && yarn
|
||||
|
||||
run:
|
||||
overmind start -f Procfile.dev
|
||||
|
||||
docker:
|
||||
docker build -t $(APP_NAME) -f ./docker/Dockerfile .
|
||||
|
||||
.PHONY: setup db_create db_migrate db_seed db console server burn docker run
|
||||
@@ -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
|
||||
|
||||
@@ -64,13 +64,14 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
|
||||
end
|
||||
|
||||
def toggle_priority
|
||||
@conversation.toggle_priority(params[:priority])
|
||||
head :ok
|
||||
end
|
||||
|
||||
def toggle_typing_status
|
||||
case params[:typing_status]
|
||||
when 'on'
|
||||
trigger_typing_event(CONVERSATION_TYPING_ON, params[:is_private])
|
||||
when 'off'
|
||||
trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private])
|
||||
end
|
||||
typing_status_manager = ::Conversations::TypingStatusManager.new(@conversation, current_user, params)
|
||||
typing_status_manager.toggle_typing_status
|
||||
head :ok
|
||||
end
|
||||
|
||||
@@ -111,11 +112,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
@conversation.update_assignee(@agent)
|
||||
end
|
||||
|
||||
def trigger_typing_event(event, is_private)
|
||||
user = current_user.presence || @resource
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user, is_private: is_private)
|
||||
end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Current.account.conversations.find_by!(display_id: params[:id])
|
||||
authorize @conversation.inbox, :show?
|
||||
|
||||
@@ -114,10 +114,13 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
def inbox_attributes
|
||||
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
||||
:lock_to_single_conversation]
|
||||
:lock_to_single_conversation, :portal_id]
|
||||
end
|
||||
|
||||
def permitted_params(channel_attributes = [])
|
||||
# We will remove this line after fixing https://linear.app/chatwoot/issue/CW-1567/null-value-passed-as-null-string-to-backend
|
||||
params.each { |k, v| params[k] = params[k] == 'null' ? nil : v }
|
||||
|
||||
params.permit(
|
||||
*inbox_attributes,
|
||||
channel: [:type, *channel_attributes]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_hook, only: [:update, :destroy]
|
||||
before_action :fetch_hook, except: [:create]
|
||||
before_action :check_authorization
|
||||
|
||||
def create
|
||||
@@ -10,6 +10,10 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
|
||||
@hook.update!(permitted_params.slice(:status, :settings))
|
||||
end
|
||||
|
||||
def process_event
|
||||
render json: { message: @hook.process_event(params[:event]) }
|
||||
end
|
||||
|
||||
def destroy
|
||||
@hook.destroy!
|
||||
head :ok
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -67,14 +72,16 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||
def current_summary_params
|
||||
common_params.merge({
|
||||
since: range[:current][:since],
|
||||
until: range[:current][:until]
|
||||
until: range[:current][:until],
|
||||
timezone_offset: params[:timezone_offset]
|
||||
})
|
||||
end
|
||||
|
||||
def previous_summary_params
|
||||
common_params.merge({
|
||||
since: range[:previous][:since],
|
||||
until: range[:previous][:until]
|
||||
until: range[:previous][:until],
|
||||
timezone_offset: params[:timezone_offset]
|
||||
})
|
||||
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,
|
||||
|
||||
@@ -7,7 +7,7 @@ module DateRangeHelper
|
||||
def range
|
||||
return if params[:since].blank? || params[:until].blank?
|
||||
|
||||
parse_date_time(params[:since])..parse_date_time(params[:until])
|
||||
parse_date_time(params[:since])...parse_date_time(params[:until])
|
||||
end
|
||||
|
||||
def parse_date_time(datetime)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -52,6 +52,12 @@ class ConversationApi extends ApiClient {
|
||||
});
|
||||
}
|
||||
|
||||
togglePriority({ conversationId, priority }) {
|
||||
return axios.post(`${this.url}/${conversationId}/toggle_priority`, {
|
||||
priority,
|
||||
});
|
||||
}
|
||||
|
||||
assignAgent({ conversationId, agentId }) {
|
||||
return axios.post(
|
||||
`${this.url}/${conversationId}/assignments?assignee_id=${agentId}`,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/* global axios */
|
||||
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class OpenAIAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('integrations', { accountScoped: true });
|
||||
}
|
||||
|
||||
processEvent({ name = 'rephrase', content, tone, hookId }) {
|
||||
return axios.post(`${this.url}/hooks/${hookId}/process_event`, {
|
||||
event: {
|
||||
name: name,
|
||||
data: {
|
||||
tone,
|
||||
content,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new OpenAIAPI();
|
||||
@@ -40,6 +40,7 @@ class ReportsAPI extends ApiClient {
|
||||
id,
|
||||
group_by: groupBy,
|
||||
business_hours: businessHours,
|
||||
timezone_offset: getTimeOffset(),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -59,9 +60,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() },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -42,9 +42,13 @@ describe('#Reports API', () => {
|
||||
'/api/v2/reports/summary',
|
||||
{
|
||||
params: {
|
||||
business_hours: undefined,
|
||||
group_by: undefined,
|
||||
id: undefined,
|
||||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
timezone_offset: -0,
|
||||
type: 'account',
|
||||
until: 1621621800,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.settings.back-button {
|
||||
.header-section.back-button {
|
||||
direction: initial;
|
||||
margin-left: var(--space-normal);
|
||||
margin-right: var(--space-smaller);
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
.multiselect--active {
|
||||
>.multiselect__tags {
|
||||
border-color: $color-woot;
|
||||
border-color: var(--w-500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,16 +75,13 @@
|
||||
}
|
||||
|
||||
&.multiselect__option--selected {
|
||||
background: var(--w-400);
|
||||
color: var(--white);
|
||||
background: var(--w-75);
|
||||
|
||||
&.multiselect__option--highlight:hover {
|
||||
background: var(--w-600);
|
||||
color: var(--white);
|
||||
background: var(--w-75);
|
||||
|
||||
&::after {
|
||||
background: transparent;
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
&::after:hover {
|
||||
@@ -196,6 +193,7 @@
|
||||
display: flex;
|
||||
font-size: var(--font-size-small);
|
||||
margin: 0;
|
||||
max-height: 3.8rem;
|
||||
padding: var(--space-smaller) var(--space-micro);
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
@update-conversation-status="toggleConversationStatus"
|
||||
@context-menu-toggle="onContextMenuToggle"
|
||||
@mark-as-unread="markAsUnread"
|
||||
@assign-priority="assignPriority"
|
||||
/>
|
||||
|
||||
<div v-if="chatListLoading" class="text-center">
|
||||
@@ -173,7 +174,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';
|
||||
@@ -191,6 +192,7 @@ import {
|
||||
isOnMentionsView,
|
||||
isOnUnattendedView,
|
||||
} from '../store/modules/conversations/helpers/actionHelpers';
|
||||
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -248,6 +250,9 @@ export default {
|
||||
...filter,
|
||||
attributeName: this.$t(`FILTER.ATTRIBUTES.${filter.attributeI18nKey}`),
|
||||
})),
|
||||
// chatsOnView is to store the chats that are currently visible on the screen,
|
||||
// which mirrors the conversationList.
|
||||
chatsOnView: [],
|
||||
foldersQuery: {},
|
||||
showAddFoldersModal: false,
|
||||
showDeleteFoldersModal: false,
|
||||
@@ -347,18 +352,39 @@ export default {
|
||||
this.currentPageFilterKey
|
||||
);
|
||||
},
|
||||
activeAssigneeTabCount() {
|
||||
const { activeAssigneeTab } = this;
|
||||
const count = this.assigneeTabItems.find(
|
||||
item => item.key === activeAssigneeTab
|
||||
).count;
|
||||
return count;
|
||||
},
|
||||
conversationFilters() {
|
||||
return {
|
||||
inboxId: this.conversationInbox ? this.conversationInbox : undefined,
|
||||
assigneeType: this.activeAssigneeTab,
|
||||
status: this.activeStatus,
|
||||
page: this.currentPage + 1,
|
||||
page: this.conversationListPagination,
|
||||
labels: this.label ? [this.label] : undefined,
|
||||
teamId: this.teamId || undefined,
|
||||
conversationType: this.conversationType || undefined,
|
||||
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
|
||||
};
|
||||
},
|
||||
conversationListPagination() {
|
||||
const conversationsPerPage = 25;
|
||||
const isNoFiltersOrFoldersAndChatListNotEmpty =
|
||||
!this.hasAppliedFiltersOrActiveFolders && this.chatsOnView !== [];
|
||||
const isUnderPerPage =
|
||||
this.chatsOnView.length < conversationsPerPage &&
|
||||
this.activeAssigneeTabCount < conversationsPerPage &&
|
||||
this.activeAssigneeTabCount > this.chatsOnView.length;
|
||||
|
||||
if (isNoFiltersOrFoldersAndChatListNotEmpty && isUnderPerPage) {
|
||||
return 1;
|
||||
}
|
||||
return this.currentPage + 1;
|
||||
},
|
||||
pageTitle() {
|
||||
if (this.hasAppliedFilters) {
|
||||
return this.$t('CHAT_LIST.TAB_HEADING');
|
||||
@@ -400,7 +426,6 @@ export default {
|
||||
} else {
|
||||
conversationList = [...this.chatLists];
|
||||
}
|
||||
|
||||
return conversationList;
|
||||
},
|
||||
activeFolder() {
|
||||
@@ -449,6 +474,9 @@ export default {
|
||||
this.resetAndFetchData();
|
||||
}
|
||||
},
|
||||
chatLists() {
|
||||
this.chatsOnView = this.conversationList;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('setChatFilter', this.activeStatus);
|
||||
@@ -670,6 +698,26 @@ export default {
|
||||
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
|
||||
}
|
||||
},
|
||||
async assignPriority(priority, conversationId = null) {
|
||||
this.$store.dispatch('setCurrentChatPriority', {
|
||||
priority,
|
||||
conversationId,
|
||||
});
|
||||
this.$store
|
||||
.dispatch('assignPriority', { conversationId, priority })
|
||||
.then(() => {
|
||||
this.$track(CONVERSATION_EVENTS.CHANGE_PRIORITY, {
|
||||
newValue: priority,
|
||||
from: 'Context menu',
|
||||
});
|
||||
this.showAlert(
|
||||
this.$t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
|
||||
priority,
|
||||
conversationId,
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
async markAsUnread(conversationId) {
|
||||
try {
|
||||
await this.$store.dispatch('markMessagesUnread', {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<div v-if="isAIIntegrationEnabled" class="position-relative">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('INTEGRATION_SETTINGS.OPEN_AI.TITLE')"
|
||||
icon="wand"
|
||||
color-scheme="secondary"
|
||||
variant="smooth"
|
||||
size="small"
|
||||
@click="toggleDropdown"
|
||||
/>
|
||||
<div
|
||||
v-if="showDropdown"
|
||||
v-on-clickaway="closeDropdown"
|
||||
class="dropdown-pane dropdown-pane--open ai-modal"
|
||||
>
|
||||
<h4 class="sub-block-title margin-top-1">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TITLE') }}
|
||||
</h4>
|
||||
<p>
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.SUBTITLE') }}
|
||||
</p>
|
||||
<label>
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.TONE.TITLE') }}
|
||||
</label>
|
||||
<div class="tone__item">
|
||||
<select v-model="activeTone" class="status--filter small">
|
||||
<option v-for="tone in tones" :key="tone.key" :value="tone.key">
|
||||
{{ tone.value }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-footer flex-container align-right">
|
||||
<woot-button variant="clear" size="small" @click="closeDropdown">
|
||||
{{ $t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.CANCEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
:is-loading="isGenerating"
|
||||
size="small"
|
||||
@click="processText"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import OpenAPI from 'dashboard/api/integrations/openapi';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin, clickaway],
|
||||
props: {
|
||||
conversationId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isGenerating: false,
|
||||
showDropdown: false,
|
||||
activeTone: 'professional',
|
||||
tones: [
|
||||
{
|
||||
key: 'professional',
|
||||
value: this.$t(
|
||||
'INTEGRATION_SETTINGS.OPEN_AI.TONE.OPTIONS.PROFESSIONAL'
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'friendly',
|
||||
value: this.$t('INTEGRATION_SETTINGS.OPEN_AI.TONE.OPTIONS.FRIENDLY'),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ appIntegrations: 'integrations/getAppIntegrations' }),
|
||||
isAIIntegrationEnabled() {
|
||||
return this.appIntegrations.find(
|
||||
integration => integration.id === 'openai' && !!integration.hooks.length
|
||||
);
|
||||
},
|
||||
hookId() {
|
||||
return this.appIntegrations.find(
|
||||
integration => integration.id === 'openai' && !!integration.hooks.length
|
||||
).hooks[0].id;
|
||||
},
|
||||
buttonText() {
|
||||
return this.isGenerating
|
||||
? this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATING')
|
||||
: this.$t('INTEGRATION_SETTINGS.OPEN_AI.BUTTONS.GENERATE');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (!this.appIntegrations.length) {
|
||||
this.$store.dispatch('integrations/get');
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleDropdown() {
|
||||
this.showDropdown = !this.showDropdown;
|
||||
},
|
||||
closeDropdown() {
|
||||
this.showDropdown = false;
|
||||
},
|
||||
async processText() {
|
||||
this.isGenerating = true;
|
||||
try {
|
||||
const result = await OpenAPI.processEvent({
|
||||
hookId: this.hookId,
|
||||
type: 'rephrase',
|
||||
content: this.message,
|
||||
tone: this.activeTone,
|
||||
});
|
||||
const {
|
||||
data: { message: generatedMessage },
|
||||
} = result;
|
||||
this.$emit('replace-text', generatedMessage || this.message);
|
||||
this.closeDropdown();
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR'));
|
||||
} finally {
|
||||
this.isGenerating = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ai-modal {
|
||||
width: 400px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
padding: var(--space-normal);
|
||||
bottom: 34px;
|
||||
position: absolute;
|
||||
span {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--s-600);
|
||||
}
|
||||
|
||||
label {
|
||||
margin-bottom: var(--space-smaller);
|
||||
}
|
||||
|
||||
.status--filter {
|
||||
background-color: var(--color-background-light);
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: var(--font-size-small);
|
||||
height: var(--space-large);
|
||||
padding: 0 var(--space-medium) 0 var(--space-small);
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
gap: var(--space-smaller);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<button class="settings back-button" @click.capture="goBack">
|
||||
<button class="header-section back-button" @click.capture="goBack">
|
||||
<fluent-icon icon="chevron-left" />
|
||||
{{ buttonLabel || $t('GENERAL_SETTINGS.BACK') }}
|
||||
</button>
|
||||
|
||||
@@ -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,37 @@
|
||||
import InboxDropdownItem from './InboxDropdownItem';
|
||||
|
||||
export default {
|
||||
title: 'Components/DropDowns/InboxDropdownItem',
|
||||
component: InboxDropdownItem,
|
||||
argTypes: {
|
||||
name: {
|
||||
defaultValue: 'My new inbox',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
|
||||
inboxIdentifier: {
|
||||
defaultValue: 'nithin@mail.com',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
|
||||
channelType: {
|
||||
defaultValue: 'email',
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const Template = (args, { argTypes }) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: { InboxDropdownItem },
|
||||
template: '<inbox-dropdown-item v-bind="$props" ></inbox-dropdown-item>',
|
||||
});
|
||||
|
||||
export const Banner = Template.bind({});
|
||||
Banner.args = {};
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="option-item--inbox">
|
||||
<span class="badge--icon">
|
||||
<fluent-icon :icon="computedInboxIcon" size="14" />
|
||||
</span>
|
||||
<div class="option__user-data">
|
||||
<h5 class="option__title">
|
||||
{{ name }}
|
||||
</h5>
|
||||
<p class="option__body text-truncate" :title="inboxIdentifier">
|
||||
{{ inboxIdentifier || computedInboxType }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
getInboxClassByType,
|
||||
getReadableInboxByType,
|
||||
} from 'dashboard/helper/inbox';
|
||||
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
inboxIdentifier: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
channelType: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
computedInboxIcon() {
|
||||
if (!this.channelType) return 'chat';
|
||||
const classByType = getInboxClassByType(
|
||||
this.channelType,
|
||||
this.inboxIdentifier
|
||||
);
|
||||
return classByType;
|
||||
},
|
||||
computedInboxType() {
|
||||
if (!this.channelType) return 'chat';
|
||||
const classByType = getReadableInboxByType(
|
||||
this.channelType,
|
||||
this.inboxIdentifier
|
||||
);
|
||||
return classByType;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.option-item--inbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 3.8rem;
|
||||
min-width: 0;
|
||||
padding: 0 var(--space-smaller);
|
||||
}
|
||||
.badge--icon {
|
||||
display: inline-flex;
|
||||
border-radius: var(--border-radius-small);
|
||||
margin-right: var(--space-smaller);
|
||||
background: var(--s-25);
|
||||
padding: var(--space-micro);
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
width: var(--space-medium);
|
||||
height: var(--space-medium);
|
||||
}
|
||||
|
||||
.option__user-data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin-left: var(--space-smaller);
|
||||
margin-right: var(--space-smaller);
|
||||
}
|
||||
.option__body {
|
||||
display: inline-block;
|
||||
color: var(--s-600);
|
||||
font-size: var(--font-size-small);
|
||||
line-height: 1.3;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.option__title {
|
||||
line-height: 1.1;
|
||||
font-size: var(--font-size-mini);
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -91,6 +91,12 @@
|
||||
v-if="(isAWebWidgetInbox || isAPIInbox) && !isOnPrivateNote"
|
||||
:conversation-id="conversationId"
|
||||
/>
|
||||
<AIAssistanceButton
|
||||
v-if="message"
|
||||
:conversation-id="conversationId"
|
||||
:message="message"
|
||||
@replace-text="replaceText"
|
||||
/>
|
||||
<transition name="modal-fade">
|
||||
<div
|
||||
v-show="$refs.upload && $refs.upload.dropActive"
|
||||
@@ -129,12 +135,13 @@ import {
|
||||
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
|
||||
} from 'shared/constants/messages';
|
||||
import VideoCallButton from '../VideoCallButton';
|
||||
import AIAssistanceButton from '../AIAssistanceButton.vue';
|
||||
import { REPLY_EDITOR_MODES } from './constants';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'ReplyBottomPanel',
|
||||
components: { FileUpload, VideoCallButton },
|
||||
components: { FileUpload, VideoCallButton, AIAssistanceButton },
|
||||
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
|
||||
props: {
|
||||
mode: {
|
||||
@@ -217,6 +224,10 @@ export default {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
@@ -303,6 +314,9 @@ export default {
|
||||
send_with_signature: !this.sendWithSignature,
|
||||
});
|
||||
},
|
||||
replaceText(text) {
|
||||
this.$emit('replace-text', text);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -33,13 +33,16 @@
|
||||
<div class="conversation--details columns">
|
||||
<div class="conversation--metadata">
|
||||
<inbox-name v-if="showInboxName" :inbox="inbox" />
|
||||
<span
|
||||
v-if="showAssignee && assignee.name"
|
||||
class="label assignee-label text-truncate"
|
||||
>
|
||||
<fluent-icon icon="person" size="12" />
|
||||
{{ assignee.name }}
|
||||
</span>
|
||||
<div class="conversation-metadata-attributes">
|
||||
<span
|
||||
v-if="showAssignee && assignee.name"
|
||||
class="label assignee-label text-truncate"
|
||||
>
|
||||
<fluent-icon icon="person" size="12" />
|
||||
{{ assignee.name }}
|
||||
</span>
|
||||
<priority-mark :priority="chat.priority" />
|
||||
</div>
|
||||
</div>
|
||||
<h4 class="conversation--user">
|
||||
{{ currentContact.name }}
|
||||
@@ -106,12 +109,14 @@
|
||||
<conversation-context-menu
|
||||
:status="chat.status"
|
||||
:inbox-id="inbox.id"
|
||||
:priority="chat.priority"
|
||||
:has-unread-messages="hasUnread"
|
||||
@update-conversation="onUpdateConversation"
|
||||
@assign-agent="onAssignAgent"
|
||||
@assign-label="onAssignLabel"
|
||||
@assign-team="onAssignTeam"
|
||||
@mark-as-unread="markAsUnread"
|
||||
@assign-priority="assignPriority"
|
||||
/>
|
||||
</woot-context-menu>
|
||||
</div>
|
||||
@@ -131,6 +136,7 @@ import ConversationContextMenu from './contextMenu/Index.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import TimeAgo from 'dashboard/components/ui/TimeAgo';
|
||||
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
||||
import PriorityMark from './PriorityMark.vue';
|
||||
const ATTACHMENT_ICONS = {
|
||||
image: 'image',
|
||||
audio: 'headphones-sound-wave',
|
||||
@@ -147,6 +153,7 @@ export default {
|
||||
Thumbnail,
|
||||
ConversationContextMenu,
|
||||
TimeAgo,
|
||||
PriorityMark,
|
||||
},
|
||||
|
||||
mixins: [
|
||||
@@ -370,6 +377,10 @@ export default {
|
||||
this.$emit('mark-as-unread', this.chat.id);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
async assignPriority(priority) {
|
||||
this.$emit('assign-priority', priority, this.chat.id);
|
||||
this.closeContextMenu();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -429,6 +440,11 @@ export default {
|
||||
padding: var(--space-micro) 0 var(--space-micro) 0;
|
||||
}
|
||||
|
||||
.conversation-metadata-attributes {
|
||||
display: flex;
|
||||
gap: var(--space-small);
|
||||
}
|
||||
|
||||
.assignee-label {
|
||||
display: inline-flex;
|
||||
margin-left: var(--space-small);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<span
|
||||
v-if="priority"
|
||||
v-tooltip="{
|
||||
content: tooltipText,
|
||||
delay: { show: 1500, hide: 0 },
|
||||
hideOnClick: true,
|
||||
}"
|
||||
class="conversation-priority-mark"
|
||||
:class="{ urgent: priority === CONVERSATION_PRIORITY.URGENT }"
|
||||
>
|
||||
<fluent-icon
|
||||
:icon="`priority-${priority.toLowerCase()}`"
|
||||
size="14"
|
||||
view-box="0 0 14 14"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { CONVERSATION_PRIORITY } from '../../../../shared/constants/messages';
|
||||
|
||||
export default {
|
||||
name: 'PriorityMark',
|
||||
props: {
|
||||
priority: {
|
||||
type: String,
|
||||
default: '',
|
||||
validate: value =>
|
||||
[...Object.values(CONVERSATION_PRIORITY), ''].includes(value),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
CONVERSATION_PRIORITY,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tooltipText() {
|
||||
return this.$t(
|
||||
`CONVERSATION.PRIORITY.OPTIONS.${this.priority.toUpperCase()}`
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.conversation-priority-mark {
|
||||
align-items: center;
|
||||
background: var(--s-50);
|
||||
border-radius: var(--border-radius-small);
|
||||
color: var(--s-600);
|
||||
display: inline-flex;
|
||||
width: var(--space-snug);
|
||||
|
||||
&.urgent {
|
||||
background: var(--r-500);
|
||||
color: var(--w-25);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -120,8 +120,10 @@
|
||||
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
|
||||
:toggle-audio-recorder="toggleAudioRecorder"
|
||||
:toggle-emoji-picker="toggleEmojiPicker"
|
||||
:message="message"
|
||||
@selectWhatsappTemplate="openWhatsappTemplateModal"
|
||||
@toggle-editor="toggleRichContentEditor"
|
||||
@replace-text="replaceText"
|
||||
/>
|
||||
<whatsapp-templates
|
||||
:inbox-id="inbox.id"
|
||||
@@ -174,9 +176,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';
|
||||
|
||||
@@ -23,6 +23,14 @@
|
||||
@click="snoozeConversation(option.snoozedUntil)"
|
||||
/>
|
||||
</menu-item-with-submenu>
|
||||
<menu-item-with-submenu :option="priorityConfig">
|
||||
<menu-item
|
||||
v-for="(option, i) in priorityConfig.options"
|
||||
:key="i"
|
||||
:option="option"
|
||||
@click="assignPriority(option.key)"
|
||||
/>
|
||||
</menu-item-with-submenu>
|
||||
<menu-item-with-submenu
|
||||
:option="labelMenuConfig"
|
||||
:sub-menu-available="!!labels.length"
|
||||
@@ -69,7 +77,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';
|
||||
@@ -93,6 +101,10 @@ export default {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
priority: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -140,6 +152,33 @@ export default {
|
||||
},
|
||||
],
|
||||
},
|
||||
priorityConfig: {
|
||||
key: 'priority',
|
||||
label: this.$t('CONVERSATION.PRIORITY.TITLE'),
|
||||
icon: 'warning',
|
||||
options: [
|
||||
{
|
||||
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.NONE'),
|
||||
key: null,
|
||||
},
|
||||
{
|
||||
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.URGENT'),
|
||||
key: 'urgent',
|
||||
},
|
||||
{
|
||||
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.HIGH'),
|
||||
key: 'high',
|
||||
},
|
||||
{
|
||||
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.MEDIUM'),
|
||||
key: 'medium',
|
||||
},
|
||||
{
|
||||
label: this.$t('CONVERSATION.PRIORITY.OPTIONS.LOW'),
|
||||
key: 'low',
|
||||
},
|
||||
].filter(item => item.key !== this.priority),
|
||||
},
|
||||
labelMenuConfig: {
|
||||
key: 'label',
|
||||
icon: 'tag',
|
||||
@@ -193,6 +232,9 @@ export default {
|
||||
this.snoozeTimes[snoozedUntil] || null
|
||||
);
|
||||
},
|
||||
assignPriority(priority) {
|
||||
this.$emit('assign-priority', priority);
|
||||
},
|
||||
show(key) {
|
||||
// If the conversation status is same as the action, then don't display the option
|
||||
// i.e.: Don't show an option to resolve if the conversation is already resolved.
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
@@ -7,6 +7,7 @@ export const CONVERSATION_EVENTS = Object.freeze({
|
||||
USED_MENTIONS: 'Used mentions',
|
||||
SEARCH_CONVERSATION: 'Searched conversations',
|
||||
APPLY_FILTER: 'Applied filters in the conversation list',
|
||||
CHANGE_PRIORITY: 'Assigned priority to a conversation',
|
||||
});
|
||||
|
||||
export const ACCOUNT_EVENTS = Object.freeze({
|
||||
|
||||
@@ -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)}` : '';
|
||||
|
||||
@@ -30,6 +30,36 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
};
|
||||
}
|
||||
|
||||
onReconnect = () => {
|
||||
this.syncActiveConversationMessages();
|
||||
};
|
||||
|
||||
onDisconnected = () => {
|
||||
this.setActiveConversationLastMessageId();
|
||||
};
|
||||
|
||||
setActiveConversationLastMessageId = () => {
|
||||
const {
|
||||
params: { conversation_id },
|
||||
} = this.app.$route;
|
||||
if (conversation_id) {
|
||||
this.app.$store.dispatch('setConversationLastMessageId', {
|
||||
conversationId: Number(conversation_id),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
syncActiveConversationMessages = () => {
|
||||
const {
|
||||
params: { conversation_id },
|
||||
} = this.app.$route;
|
||||
if (conversation_id) {
|
||||
this.app.$store.dispatch('syncActiveConversationMessages', {
|
||||
conversationId: Number(conversation_id),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
isAValidEvent = data => {
|
||||
return this.app.$store.getters.getCurrentAccountId === data.account_id;
|
||||
};
|
||||
|
||||
@@ -16,6 +16,29 @@ const MESSAGE_CONDITION_VALUES = [
|
||||
},
|
||||
];
|
||||
|
||||
export const PRIORITY_CONDITION_VALUES = [
|
||||
{
|
||||
id: 'nil',
|
||||
name: 'None',
|
||||
},
|
||||
{
|
||||
id: 'low',
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 'medium',
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 'high',
|
||||
name: 'High',
|
||||
},
|
||||
{
|
||||
id: 'urgent',
|
||||
name: 'Urgent',
|
||||
},
|
||||
];
|
||||
|
||||
export const getCustomAttributeInputType = key => {
|
||||
const customAttributeMap = {
|
||||
date: 'date',
|
||||
@@ -103,6 +126,7 @@ export const getActionOptions = ({ agents, teams, labels, type }) => {
|
||||
assign_team: teams,
|
||||
send_email_to_team: teams,
|
||||
add_label: generateConditionOptions(labels, 'title'),
|
||||
change_priority: PRIORITY_CONDITION_VALUES,
|
||||
};
|
||||
return actionsMap[type];
|
||||
};
|
||||
@@ -139,6 +163,7 @@ export const getConditionOptions = ({
|
||||
conversation_language: languages,
|
||||
country_code: countries,
|
||||
message_type: MESSAGE_CONDITION_VALUES,
|
||||
priority: PRIORITY_CONDITION_VALUES,
|
||||
};
|
||||
|
||||
return conditionFilterMaps[type];
|
||||
|
||||
@@ -1,5 +1,55 @@
|
||||
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
||||
|
||||
export const getInboxSource = (type, phoneNumber, inbox) => {
|
||||
switch (type) {
|
||||
case INBOX_TYPES.WEB:
|
||||
return inbox.website_url || '';
|
||||
|
||||
case INBOX_TYPES.TWILIO:
|
||||
case INBOX_TYPES.WHATSAPP:
|
||||
return phoneNumber || '';
|
||||
|
||||
case INBOX_TYPES.EMAIL:
|
||||
return inbox.email || '';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
export const getReadableInboxByType = (type, phoneNumber) => {
|
||||
switch (type) {
|
||||
case INBOX_TYPES.WEB:
|
||||
return 'livechat';
|
||||
|
||||
case INBOX_TYPES.FB:
|
||||
return 'facebook';
|
||||
|
||||
case INBOX_TYPES.TWITTER:
|
||||
return 'twitter';
|
||||
|
||||
case INBOX_TYPES.TWILIO:
|
||||
return phoneNumber?.startsWith('whatsapp') ? 'whatsapp' : 'sms';
|
||||
|
||||
case INBOX_TYPES.WHATSAPP:
|
||||
return 'whatsapp';
|
||||
|
||||
case INBOX_TYPES.API:
|
||||
return 'api';
|
||||
|
||||
case INBOX_TYPES.EMAIL:
|
||||
return 'email';
|
||||
|
||||
case INBOX_TYPES.TELEGRAM:
|
||||
return 'telegram';
|
||||
|
||||
case INBOX_TYPES.LINE:
|
||||
return 'line';
|
||||
|
||||
default:
|
||||
return 'chat';
|
||||
}
|
||||
};
|
||||
|
||||
export const getInboxClassByType = (type, phoneNumber) => {
|
||||
switch (type) {
|
||||
case INBOX_TYPES.WEB:
|
||||
|
||||
@@ -57,6 +57,7 @@ export const initializeChatwootEvents = () => {
|
||||
window.$chatwoot.setCustomAttributes({
|
||||
signedUpAt: user.created_at,
|
||||
cloudCustomer: 'true',
|
||||
account_id: user.account_id,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ describe('#resolveActionName', () => {
|
||||
expect(resolveActionName(MACRO_ACTION_TYPES[1].key)).not.toEqual(
|
||||
MACRO_ACTION_TYPES[0].label
|
||||
);
|
||||
expect(resolveActionName('change_priority')).toEqual('Change Priority');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"LABEL": "عنوان البريد الإلكتروني",
|
||||
"PLACEHOLDER": "الرجاء إدخال عنوان البريد الإلكتروني للموظف"
|
||||
},
|
||||
"AGENT_AVAILABILITY": {
|
||||
"LABEL": "التوفر",
|
||||
"PLACEHOLDER": "Please select an availability status",
|
||||
"ERROR": "Availability is required"
|
||||
},
|
||||
"SUBMIT": "تعديل حساب الموظف"
|
||||
},
|
||||
"BUTTON_TEXT": "تعديل",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"AUDIT_LOGS": {
|
||||
"HEADER": "Audit Logs",
|
||||
"HEADER_BTN_TXT": "Add Audit Logs",
|
||||
"LOADING": "Fetching Audit Logs",
|
||||
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام",
|
||||
"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",
|
||||
"Time"
|
||||
]
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
|
||||
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,8 +120,9 @@
|
||||
"PHONE_NUMBER": {
|
||||
"PLACEHOLDER": "أدخل رقم الهاتف الخاص بجهة الاتصال",
|
||||
"LABEL": "رقم الهاتف",
|
||||
"HELP": "يجب ان يحتوى رقم الهاتف على كود دولتك تسبقها علامة +\nمثال: +20101243567",
|
||||
"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": "يجب ان تكون خانة رقم الهاتف إما فارغة او مكتملة مع رمز الدولة",
|
||||
"DIAL_CODE_ERROR": "Please select a dial code from the list",
|
||||
"DUPLICATE": "رقم الهاتف هذا مستخدم لجهة اتصال أخرى."
|
||||
},
|
||||
"LOCATION": {
|
||||
@@ -132,6 +133,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": "أدخل اسم مستخدم فيسبوك",
|
||||
@@ -170,7 +182,8 @@
|
||||
"LABEL": "إلى"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "صندوق الوارد",
|
||||
"LABEL": "Via Inbox",
|
||||
"PLACEHOLDER": "Choose source inbox",
|
||||
"ERROR": "حدد صندوق الوارد"
|
||||
},
|
||||
"SUBJECT": {
|
||||
|
||||
@@ -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": "كود \"الماسنجر\"",
|
||||
@@ -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": "تحديث",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "رمز التحقق من Webhook",
|
||||
"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": {
|
||||
|
||||
@@ -164,6 +164,7 @@
|
||||
"COMPONENTS": {
|
||||
"CODE": {
|
||||
"BUTTON_TEXT": "نسخ",
|
||||
"CODEPEN": "Open in CodePen",
|
||||
"COPY_SUCCESSFUL": "تم نسخ الكود إلى الحافظة بنجاح"
|
||||
},
|
||||
"SHOW_MORE_BLOCK": {
|
||||
@@ -202,6 +203,7 @@
|
||||
"HOME": "الرئيسية",
|
||||
"AGENTS": "موظف الدعم",
|
||||
"AGENT_BOTS": "البوتات",
|
||||
"AUDIT_LOGS": "Audit Logs",
|
||||
"INBOXES": "قنوات التواصل",
|
||||
"NOTIFICATIONS": "الإشعارات",
|
||||
"CANNED_RESPONSES": "الردود السريعة",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"LABEL": "Имейл адрес",
|
||||
"PLACEHOLDER": "Моля, въведете имейл адрес на агента"
|
||||
},
|
||||
"AGENT_AVAILABILITY": {
|
||||
"LABEL": "Availability",
|
||||
"PLACEHOLDER": "Please select an availability status",
|
||||
"ERROR": "Availability is required"
|
||||
},
|
||||
"SUBMIT": "Редактирай агента"
|
||||
},
|
||||
"BUTTON_TEXT": "Редактирай",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"AUDIT_LOGS": {
|
||||
"HEADER": "Audit Logs",
|
||||
"HEADER_BTN_TXT": "Add Audit Logs",
|
||||
"LOADING": "Fetching Audit Logs",
|
||||
"SEARCH_404": "Няма резултати отговарящи на тази заявка",
|
||||
"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 адрес",
|
||||
"Time"
|
||||
]
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
|
||||
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,8 +120,9 @@
|
||||
"PHONE_NUMBER": {
|
||||
"PLACEHOLDER": "Добавете телефона на контакта",
|
||||
"LABEL": "Телефон",
|
||||
"HELP": "Телефона трябва да е във E.164 формат, например: +35955555555 [+][код на държавата][телефонен номер]",
|
||||
"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": "Телефона трябва да е празен или във E.164 формат",
|
||||
"DIAL_CODE_ERROR": "Please select a dial code from the list",
|
||||
"DUPLICATE": "Телефона се използва от друг контакт."
|
||||
},
|
||||
"LOCATION": {
|
||||
@@ -132,6 +133,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 потребителско име",
|
||||
@@ -170,7 +182,8 @@
|
||||
"LABEL": "До"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Входяща кутия",
|
||||
"LABEL": "Via Inbox",
|
||||
"PLACEHOLDER": "Choose source inbox",
|
||||
"ERROR": "Изберете входяща кутия"
|
||||
},
|
||||
"SUBJECT": {
|
||||
|
||||
@@ -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": "Обновяване",
|
||||
"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": {
|
||||
|
||||
@@ -164,6 +164,7 @@
|
||||
"COMPONENTS": {
|
||||
"CODE": {
|
||||
"BUTTON_TEXT": "Copy",
|
||||
"CODEPEN": "Open in CodePen",
|
||||
"COPY_SUCCESSFUL": "Code copied to clipboard successfully"
|
||||
},
|
||||
"SHOW_MORE_BLOCK": {
|
||||
@@ -202,6 +203,7 @@
|
||||
"HOME": "Home",
|
||||
"AGENTS": "Агенти",
|
||||
"AGENT_BOTS": "Bots",
|
||||
"AUDIT_LOGS": "Audit Logs",
|
||||
"INBOXES": "Inboxes",
|
||||
"NOTIFICATIONS": "Notifications",
|
||||
"CANNED_RESPONSES": "Готови отговори",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"AUDIT_LOGS": {
|
||||
"HEADER": "Audit Logs",
|
||||
"HEADER_BTN_TXT": "Add Audit Logs",
|
||||
"LOADING": "Fetching Audit Logs",
|
||||
"SEARCH_404": "No hi ha cap resposta que coincideixi amb aquesta consulta",
|
||||
"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",
|
||||
"Adreça IP",
|
||||
"Time"
|
||||
]
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
|
||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,8 +120,9 @@
|
||||
"PHONE_NUMBER": {
|
||||
"PLACEHOLDER": "Introdueix el número de telèfon del contacte",
|
||||
"LABEL": "Número de telèfon",
|
||||
"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": {
|
||||
@@ -132,6 +133,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",
|
||||
@@ -170,7 +182,8 @@
|
||||
"LABEL": "To"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Inbox",
|
||||
"LABEL": "Via Inbox",
|
||||
"PLACEHOLDER": "Choose source inbox",
|
||||
"ERROR": "Select an inbox"
|
||||
},
|
||||
"SUBJECT": {
|
||||
|
||||
@@ -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",
|
||||
@@ -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": "Actualitza",
|
||||
"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": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -202,6 +203,7 @@
|
||||
"HOME": "Inici",
|
||||
"AGENTS": "Agents",
|
||||
"AGENT_BOTS": "Bots",
|
||||
"AUDIT_LOGS": "Audit Logs",
|
||||
"INBOXES": "Safates d'entrada",
|
||||
"NOTIFICATIONS": "Notificacions",
|
||||
"CANNED_RESPONSES": "Respostes predeterminades",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"AUDIT_LOGS": {
|
||||
"HEADER": "Audit Logs",
|
||||
"HEADER_BTN_TXT": "Add Audit Logs",
|
||||
"LOADING": "Fetching Audit Logs",
|
||||
"SEARCH_404": "Neexistují žádné položky odpovídající tomuto dotazu",
|
||||
"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 adresa",
|
||||
"Time"
|
||||
]
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
|
||||
"ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,8 +120,9 @@
|
||||
"PHONE_NUMBER": {
|
||||
"PLACEHOLDER": "Zadejte telefonní číslo kontaktu",
|
||||
"LABEL": "Telefonní číslo",
|
||||
"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": {
|
||||
@@ -132,6 +133,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",
|
||||
@@ -170,7 +182,8 @@
|
||||
"LABEL": "To"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Inbox",
|
||||
"LABEL": "Via Inbox",
|
||||
"PLACEHOLDER": "Choose source inbox",
|
||||
"ERROR": "Select an inbox"
|
||||
},
|
||||
"SUBJECT": {
|
||||
|
||||
@@ -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",
|
||||
@@ -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": "Aktualizovat",
|
||||
"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": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -202,6 +203,7 @@
|
||||
"HOME": "Domů",
|
||||
"AGENTS": "Agenti",
|
||||
"AGENT_BOTS": "Bots",
|
||||
"AUDIT_LOGS": "Audit Logs",
|
||||
"INBOXES": "Schránky",
|
||||
"NOTIFICATIONS": "Oznámení",
|
||||
"CANNED_RESPONSES": "Konzervované odpovědi",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"AUDIT_LOGS": {
|
||||
"HEADER": "Audit Logs",
|
||||
"HEADER_BTN_TXT": "Add Audit Logs",
|
||||
"LOADING": "Fetching Audit Logs",
|
||||
"SEARCH_404": "Der er ingen elementer, der matcher denne forespørgsel",
|
||||
"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 Adresse",
|
||||
"Time"
|
||||
]
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
|
||||
"ERROR_MESSAGE": "Kunne ikke oprette forbindelse til Woot Server, Prøv igen senere"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,8 +120,9 @@
|
||||
"PHONE_NUMBER": {
|
||||
"PLACEHOLDER": "Indtast telefonnummeret på kontaktpersonen",
|
||||
"LABEL": "Telefonnummer",
|
||||
"HELP": "Telefonnummer skal være af E.164 format fx: +1415555555 [+] [landekode] [områdekode] [lokalt telefonnummer]",
|
||||
"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": "Telefonnummer skal være enten tomt eller i E.164-format",
|
||||
"DIAL_CODE_ERROR": "Please select a dial code from the list",
|
||||
"DUPLICATE": "Dette telefonnummer er i brug for en anden kontakt."
|
||||
},
|
||||
"LOCATION": {
|
||||
@@ -132,6 +133,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",
|
||||
@@ -170,7 +182,8 @@
|
||||
"LABEL": "Til"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Indbakke",
|
||||
"LABEL": "Via Inbox",
|
||||
"PLACEHOLDER": "Choose source inbox",
|
||||
"ERROR": "Vælg en indbakke"
|
||||
},
|
||||
"SUBJECT": {
|
||||
|
||||
@@ -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",
|
||||
@@ -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": "Opdater",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook Verificér 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": {
|
||||
|
||||
@@ -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": {
|
||||
@@ -202,6 +203,7 @@
|
||||
"HOME": "Hjem",
|
||||
"AGENTS": "Agenter",
|
||||
"AGENT_BOTS": "Bots",
|
||||
"AUDIT_LOGS": "Audit Logs",
|
||||
"INBOXES": "Indbakker",
|
||||
"NOTIFICATIONS": "Notifikationer",
|
||||
"CANNED_RESPONSES": "Standardsvar Svar",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"AUDIT_LOGS": {
|
||||
"HEADER": "Audit Logs",
|
||||
"HEADER_BTN_TXT": "Add Audit Logs",
|
||||
"LOADING": "Fetching Audit Logs",
|
||||
"SEARCH_404": "Es gibt keine Elemente, die dieser Abfrage entsprechen",
|
||||
"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-Adresse",
|
||||
"Time"
|
||||
]
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
|
||||
"ERROR_MESSAGE": "Es konnte keine Verbindung zum Woot Server hergestellt werden. Bitte versuchen Sie es später erneut"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,8 +120,9 @@
|
||||
"PHONE_NUMBER": {
|
||||
"PLACEHOLDER": "Geben Sie die Telefonnummer des Kontakts ein",
|
||||
"LABEL": "Telefonnummer",
|
||||
"HELP": "Telefonnummer sollte im E.164-Format sein, z. B.: +1415555555 [+][Landesvorwahl][Landesvorwahl][Ortsvorwahl]",
|
||||
"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": "Telefonnummer muss leer sein oder im E.164-Format",
|
||||
"DIAL_CODE_ERROR": "Please select a dial code from the list",
|
||||
"DUPLICATE": "Diese Telefonnummer wird für einen anderen Kontakt verwendet."
|
||||
},
|
||||
"LOCATION": {
|
||||
@@ -132,6 +133,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",
|
||||
@@ -170,7 +182,8 @@
|
||||
"LABEL": "An"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Posteingang",
|
||||
"LABEL": "Via Inbox",
|
||||
"PLACEHOLDER": "Choose source inbox",
|
||||
"ERROR": "Posteingang auswählen"
|
||||
},
|
||||
"SUBJECT": {
|
||||
|
||||
@@ -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",
|
||||
@@ -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": "Aktualisieren",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Webhook-Verifizierungstoken",
|
||||
"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": {
|
||||
|
||||
@@ -164,6 +164,7 @@
|
||||
"COMPONENTS": {
|
||||
"CODE": {
|
||||
"BUTTON_TEXT": "Kopieren",
|
||||
"CODEPEN": "Open in CodePen",
|
||||
"COPY_SUCCESSFUL": "Code erfolgreich in die Zwischenablage kopiert"
|
||||
},
|
||||
"SHOW_MORE_BLOCK": {
|
||||
@@ -202,6 +203,7 @@
|
||||
"HOME": "Hauptseite",
|
||||
"AGENTS": "Agenten",
|
||||
"AGENT_BOTS": "Bots",
|
||||
"AUDIT_LOGS": "Audit Logs",
|
||||
"INBOXES": "Posteingänge",
|
||||
"NOTIFICATIONS": "Push-Benachrichtigungen",
|
||||
"CANNED_RESPONSES": "Vorgefertigte Antworten",
|
||||
|
||||
@@ -74,6 +74,11 @@
|
||||
"LABEL": "Διεύθυνση Email",
|
||||
"PLACEHOLDER": "Παρακαλώ εισάγετε το email του πράκτορα"
|
||||
},
|
||||
"AGENT_AVAILABILITY": {
|
||||
"LABEL": "Διαθεσιμότητα",
|
||||
"PLACEHOLDER": "Please select an availability status",
|
||||
"ERROR": "Availability is required"
|
||||
},
|
||||
"SUBMIT": "Καταχώρηση"
|
||||
},
|
||||
"BUTTON_TEXT": "Επεξεργασία",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"AUDIT_LOGS": {
|
||||
"HEADER": "Audit Logs",
|
||||
"HEADER_BTN_TXT": "Add Audit Logs",
|
||||
"LOADING": "Fetching Audit Logs",
|
||||
"SEARCH_404": "Δεν υπάρχουν αντικείμενα να ταιριάζουν με αυτό το ερώτημα",
|
||||
"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",
|
||||
"Time"
|
||||
]
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
|
||||
"ERROR_MESSAGE": "Αδυναμία σύνδεσης με τον Woot Server, Παρακαλώ προσπαθήστε αργότερα"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,8 +120,9 @@
|
||||
"PHONE_NUMBER": {
|
||||
"PLACEHOLDER": "Εισάγετε τον αριθμό τηλεφώνου της επαφής",
|
||||
"LABEL": "Αριθμός Τηλεφώνου",
|
||||
"HELP": "Ο αριθμός τηλεφώνου πρέπει να έχει μορφή E.164 π.χ.: +14155555 [+][κωδικός χώρας][κωδικός περιοχής][αριθμός τοπικού τηλεφώνου]",
|
||||
"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": "Ο αριθμός τηλεφώνου πρέπει να είναι είτε άδειος είτε με μορφή E.164",
|
||||
"DIAL_CODE_ERROR": "Please select a dial code from the list",
|
||||
"DUPLICATE": "Αυτός ο αριθμός τηλεφώνου χρησιμοποιείται σε άλλη επαφή."
|
||||
},
|
||||
"LOCATION": {
|
||||
@@ -132,6 +133,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",
|
||||
@@ -170,7 +182,8 @@
|
||||
"LABEL": "Προς"
|
||||
},
|
||||
"INBOX": {
|
||||
"LABEL": "Εισερχόμενα",
|
||||
"LABEL": "Via Inbox",
|
||||
"PLACEHOLDER": "Choose source inbox",
|
||||
"ERROR": "Επιλέξτε ένα κιβώτιο εισερχόμενων"
|
||||
},
|
||||
"SUBJECT": {
|
||||
|
||||
@@ -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)",
|
||||
@@ -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": "Ενημέρωση",
|
||||
"WHATSAPP_WEBHOOK_TITLE": "Token Επαλήθευσης Webhook",
|
||||
"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": {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user