Merge branch 'release/1.7.0'
This commit is contained in:
commit
2063e248a9
304 changed files with 5600 additions and 1367 deletions
|
@ -40,7 +40,8 @@ SMTP_AUTHENTICATION=
|
|||
SMTP_ENABLE_STARTTLS_AUTO=
|
||||
|
||||
# Mail Incoming
|
||||
|
||||
# This is the domain set for the reply emails when conversation continuity is enabled
|
||||
MAILER_INBOUND_EMAIL_DOMAIN=
|
||||
# Set this to appropriate ingress channel with regards to incoming emails
|
||||
# Possible values are :
|
||||
# :relay for Exim, Postfix, Qmail
|
||||
|
|
|
@ -46,5 +46,6 @@ module.exports = {
|
|||
},
|
||||
globals: {
|
||||
__WEBPACK_ENV__: true,
|
||||
bus: true,
|
||||
},
|
||||
};
|
||||
|
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -55,3 +55,7 @@ node_modules
|
|||
package-lock.json
|
||||
|
||||
*.dump
|
||||
|
||||
|
||||
# cypress
|
||||
test/cypress/videos/*
|
|
@ -282,3 +282,4 @@ exclude:
|
|||
- 'app/javascript/widget/assets/scss/_reset.scss'
|
||||
- 'app/javascript/widget/assets/scss/sdk.css'
|
||||
- 'app/assets/stylesheets/administrate/reset/_normalize.scss'
|
||||
- 'app/javascript/shared/assets/stylesheets/*.scss'
|
||||
|
|
20
Gemfile
20
Gemfile
|
@ -8,7 +8,7 @@ gem 'rails'
|
|||
# Reduces boot times through caching; required in config/boot.rb
|
||||
gem 'bootsnap', require: false
|
||||
|
||||
##-- rails helper gems --##
|
||||
##-- rails application helper gems --##
|
||||
gem 'acts-as-taggable-on'
|
||||
gem 'attr_extras'
|
||||
gem 'browser'
|
||||
|
@ -23,6 +23,12 @@ gem 'tzinfo-data'
|
|||
gem 'valid_email2'
|
||||
# compress javascript config.assets.js_compressor
|
||||
gem 'uglifier'
|
||||
##-- used for single column multiple binary flags in notification settings/feature flagging --##
|
||||
gem 'flag_shih_tzu'
|
||||
# Random name generator for user names
|
||||
gem 'haikunator'
|
||||
# Template parsing safetly
|
||||
gem 'liquid'
|
||||
|
||||
##-- for active storage --##
|
||||
gem 'aws-sdk-s3', require: false
|
||||
|
@ -67,8 +73,6 @@ gem 'twitty'
|
|||
gem 'koala'
|
||||
# slack client
|
||||
gem 'slack-ruby-client'
|
||||
# Random name generator
|
||||
gem 'haikunator'
|
||||
|
||||
##--- gems for debugging and error reporting ---##
|
||||
# static analysis
|
||||
|
@ -79,9 +83,6 @@ gem 'sentry-raven'
|
|||
##-- background job processing --##
|
||||
gem 'sidekiq'
|
||||
|
||||
##-- used for single column multiple binary flags in notification settings/feature flagging --##
|
||||
gem 'flag_shih_tzu'
|
||||
|
||||
##-- Push notification service --##
|
||||
gem 'fcm'
|
||||
gem 'webpush'
|
||||
|
@ -96,6 +97,13 @@ group :development do
|
|||
gem 'json_refs', git: 'https://github.com/tzmfreedom/json_refs', ref: 'e32deb0'
|
||||
end
|
||||
|
||||
group :test do
|
||||
# Cypress in rails.
|
||||
gem 'cypress-on-rails', '~> 1.0'
|
||||
# fast cleaning of database
|
||||
gem 'database_cleaner'
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
# locking until https://github.com/codeclimate/test-reporter/issues/418 is resolved
|
||||
gem 'action-cable-testing'
|
||||
|
|
|
@ -146,6 +146,9 @@ GEM
|
|||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.6)
|
||||
cypress-on-rails (1.7.0)
|
||||
rack
|
||||
database_cleaner (1.8.5)
|
||||
datetime_picker_rails (0.0.7)
|
||||
momentjs-rails (>= 2.8.1)
|
||||
declarative (0.0.10)
|
||||
|
@ -272,6 +275,7 @@ GEM
|
|||
addressable (~> 2.7)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
liquid (4.0.3)
|
||||
listen (3.2.1)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
|
@ -560,6 +564,8 @@ DEPENDENCIES
|
|||
bullet
|
||||
bundle-audit
|
||||
byebug
|
||||
cypress-on-rails (~> 1.0)
|
||||
database_cleaner
|
||||
devise
|
||||
devise_token_auth
|
||||
dotenv-rails
|
||||
|
@ -579,6 +585,7 @@ DEPENDENCIES
|
|||
kaminari
|
||||
koala
|
||||
letter_opener
|
||||
liquid
|
||||
listen
|
||||
mini_magick
|
||||
mock_redis!
|
||||
|
|
3
Procfile.test
Normal file
3
Procfile.test
Normal file
|
@ -0,0 +1,3 @@
|
|||
backend: RAILS_ENV=test bin/rails s -p 5050
|
||||
frontend: bin/webpack-dev-server
|
||||
worker: RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml
|
10
README.md
10
README.md
|
@ -27,9 +27,9 @@ ___
|
|||
|
||||
## Background
|
||||
|
||||
Chatwoot is a customer support tool for instant messaging channels which can help businesses to provide exceptional customer support. The development of Chatwoot started in 2016 and it failed to succeed as a business and eventually shut the shop in 2017. During 2019 #Hacktoberfest, the maintainers decided to make it opensource instead of letting the code rust in a private repo. With a pleasant surprise, Chatwoot became a trending project on Hacker News and best of all, got lots of love from the community.
|
||||
Chatwoot is a customer support tool for instant messaging channels which can help businesses provide exceptional customer support. The development of Chatwoot started in 2016. It failed to succeed as a business and eventually shut up shop in 2017. During 2019 #Hacktoberfest, the maintainers decided to make it opensource, instead of letting the code rust in a private repo. With a pleasant surprise, Chatwoot became a trending project on Hacker News and best of all, got lots of love from the community.
|
||||
|
||||
Now, a failed project is back on track and the prospects are looking great. The team is back to working on the project and we are building it in the open. Thanks to the ideas and contributions from the community.
|
||||
Now, a failed project is back on track and the prospects are looking great. The team is back to working on the project and this time, we are building it in the open. Thanks to the ideas and contributions from the community.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
@ -39,12 +39,12 @@ You can find the quick setup docs [here](https://www.chatwoot.com/docs/quick-set
|
|||
|
||||
## Branching model
|
||||
|
||||
We use [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. The base branch is `develop`.
|
||||
We use the [git-flow](https://nvie.com/posts/a-successful-git-branching-model/) branching model. The base branch is `develop`.
|
||||
If you are looking for a stable version, please use the `master` or tags labelled as `v1.x.x`.
|
||||
|
||||
## Heroku one-click deploy
|
||||
|
||||
Deploying chatwoot to heroku, it's a breeze. It's as simple as clicking this button.
|
||||
Deploying chatwoot to heroku is a breeze. It's as simple as clicking this button:
|
||||
|
||||
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master)
|
||||
|
||||
|
@ -54,7 +54,7 @@ Follow this [link](https://www.chatwoot.com/docs/environment-variables) to under
|
|||
|
||||
Follow our [docker development guide](https://www.chatwoot.com/docs/installation-guide-docker) to develop and debug the application using `docker-compose`.
|
||||
|
||||
Follow our [environment variables](https://www.chatwoot.com/docs/environment-variables/) guide to setup environment for Docker.
|
||||
Follow our [environment variables](https://www.chatwoot.com/docs/environment-variables/) guide to setup the environment for Docker.
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.logo-brand {
|
||||
margin-bottom: $space-normal;
|
||||
padding: $space-normal $space-smaller;
|
||||
padding: $space-normal $space-smaller $space-small;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
@ -70,3 +70,9 @@
|
|||
left: $space-normal;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
color: $color-gray;
|
||||
font-size: $font-size-small;
|
||||
padding-top: $space-smaller;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ html {
|
|||
line-height: 1.15; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Sections
|
||||
|
|
|
@ -2,15 +2,18 @@
|
|||
|
||||
class AccountBuilder
|
||||
include CustomExceptions::Account
|
||||
pattr_initialize [:account_name!, :email!, :confirmed!]
|
||||
pattr_initialize [:account_name!, :email!, :confirmed!, :user]
|
||||
|
||||
def perform
|
||||
validate_email
|
||||
validate_user
|
||||
if @user.nil?
|
||||
validate_email
|
||||
validate_user
|
||||
end
|
||||
ActiveRecord::Base.transaction do
|
||||
@account = create_account
|
||||
@user = create_and_link_user
|
||||
end
|
||||
[@user, @account]
|
||||
rescue StandardError => e
|
||||
@account&.destroy
|
||||
puts e.inspect
|
||||
|
@ -42,13 +45,7 @@ class AccountBuilder
|
|||
end
|
||||
|
||||
def create_and_link_user
|
||||
password = Time.now.to_i
|
||||
@user = User.new(email: @email,
|
||||
password: password,
|
||||
password_confirmation: password,
|
||||
name: email_to_name(@email))
|
||||
@user.confirm if @confirmed
|
||||
if @user.save!
|
||||
if @user.present? || create_user
|
||||
link_user_to_account(@user, @account)
|
||||
@user
|
||||
else
|
||||
|
@ -68,4 +65,14 @@ class AccountBuilder
|
|||
name = email[/[^@]+/]
|
||||
name.split('.').map(&:capitalize).join(' ')
|
||||
end
|
||||
|
||||
def create_user
|
||||
password = Time.now.to_i
|
||||
@user = User.new(email: @email,
|
||||
password: password,
|
||||
password_confirmation: password,
|
||||
name: email_to_name(@email))
|
||||
@user.confirm if @confirmed
|
||||
@user.save!
|
||||
end
|
||||
end
|
||||
|
|
139
app/builders/messages/facebook/message_builder.rb
Normal file
139
app/builders/messages/facebook/message_builder.rb
Normal file
|
@ -0,0 +1,139 @@
|
|||
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
|
||||
# Assumptions
|
||||
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
|
||||
# based on this we are showing "not sent from chatwoot" message in frontend
|
||||
# Hence there is no need to set user_id in message for outgoing echo messages.
|
||||
|
||||
class Messages::Facebook::MessageBuilder
|
||||
attr_reader :response
|
||||
|
||||
def initialize(response, inbox, outgoing_echo = false)
|
||||
@response = response
|
||||
@inbox = inbox
|
||||
@sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id)
|
||||
@message_type = (outgoing_echo ? :outgoing : :incoming)
|
||||
end
|
||||
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
build_contact
|
||||
build_message
|
||||
end
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def contact
|
||||
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
|
||||
end
|
||||
|
||||
def build_contact
|
||||
return if contact.present?
|
||||
|
||||
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
||||
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url]
|
||||
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||
end
|
||||
|
||||
def build_message
|
||||
@message = conversation.messages.create!(message_params)
|
||||
(response.attachments || []).each do |attachment|
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||
end
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
file_resource = LocalResource.new(file_url)
|
||||
attachment.file.attach(io: file_resource.file, filename: file_resource.tmp_filename, content_type: file_resource.encoding)
|
||||
end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
||||
end
|
||||
|
||||
def build_conversation
|
||||
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
|
||||
Conversation.create!(conversation_params.merge(
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
))
|
||||
end
|
||||
|
||||
def attachment_params(attachment)
|
||||
file_type = attachment['type'].to_sym
|
||||
params = { file_type: file_type, account_id: @message.account_id }
|
||||
|
||||
if [:image, :file, :audio, :video].include? file_type
|
||||
params.merge!(file_type_params(attachment))
|
||||
elsif file_type == :location
|
||||
params.merge!(location_params(attachment))
|
||||
elsif file_type == :fallback
|
||||
params.merge!(fallback_params(attachment))
|
||||
end
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
def file_type_params(attachment)
|
||||
{
|
||||
external_url: attachment['payload']['url'],
|
||||
remote_file_url: attachment['payload']['url']
|
||||
}
|
||||
end
|
||||
|
||||
def location_params(attachment)
|
||||
lat = attachment['payload']['coordinates']['lat']
|
||||
long = attachment['payload']['coordinates']['long']
|
||||
{
|
||||
external_url: attachment['url'],
|
||||
coordinates_lat: lat,
|
||||
coordinates_long: long,
|
||||
fallback_title: attachment['title']
|
||||
}
|
||||
end
|
||||
|
||||
def fallback_params(attachment)
|
||||
{
|
||||
fallback_title: attachment['title'],
|
||||
external_url: attachment['url']
|
||||
}
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: contact.id
|
||||
}
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: @message_type,
|
||||
content: response.content,
|
||||
source_id: response.identifier,
|
||||
sender: contact
|
||||
}
|
||||
end
|
||||
|
||||
def contact_params
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
result = k.get_object(@sender_id) || {}
|
||||
rescue StandardError => e
|
||||
result = {}
|
||||
Raven.capture_exception(e)
|
||||
end
|
||||
{
|
||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||
account_id: @inbox.account_id,
|
||||
remote_avatar_url: result['profile_pic'] || ''
|
||||
}
|
||||
end
|
||||
end
|
|
@ -1,2 +0,0 @@
|
|||
class Messages::IncomingMessageBuilder < Messages::MessageBuilder
|
||||
end
|
|
@ -1,139 +1,59 @@
|
|||
# This class creates both outgoing messages from chatwoot and echo outgoing messages based on the flag `outgoing_echo`
|
||||
# Assumptions
|
||||
# 1. Incase of an outgoing message which is echo, source_id will NOT be nil,
|
||||
# based on this we are showing "not sent from chatwoot" message in frontend
|
||||
# Hence there is no need to set user_id in message for outgoing echo messages.
|
||||
|
||||
class Messages::MessageBuilder
|
||||
attr_reader :response
|
||||
include ::FileTypeHelper
|
||||
attr_reader :message
|
||||
|
||||
def initialize(response, inbox, outgoing_echo = false)
|
||||
@response = response
|
||||
@inbox = inbox
|
||||
@sender_id = (outgoing_echo ? @response.recipient_id : @response.sender_id)
|
||||
@message_type = (outgoing_echo ? :outgoing : :incoming)
|
||||
def initialize(user, conversation, params)
|
||||
@content = params[:content]
|
||||
@private = params[:private] || false
|
||||
@conversation = conversation
|
||||
@user = user
|
||||
@message_type = params[:message_type] || 'outgoing'
|
||||
@content_type = params[:content_type]
|
||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||
@attachments = params[:attachments]
|
||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
||||
end
|
||||
|
||||
def perform
|
||||
ActiveRecord::Base.transaction do
|
||||
build_contact
|
||||
build_message
|
||||
@message = @conversation.messages.build(message_params)
|
||||
if @attachments.present?
|
||||
@attachments.each do |uploaded_attachment|
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_type(uploaded_attachment&.content_type)
|
||||
)
|
||||
attachment.file.attach(uploaded_attachment)
|
||||
end
|
||||
end
|
||||
rescue StandardError => e
|
||||
Raven.capture_exception(e)
|
||||
true
|
||||
@message.save
|
||||
@message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def contact
|
||||
@contact ||= @inbox.contact_inboxes.find_by(source_id: @sender_id)&.contact
|
||||
end
|
||||
|
||||
def build_contact
|
||||
return if contact.present?
|
||||
|
||||
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
|
||||
ContactAvatarJob.perform_later(@contact, contact_params[:remote_avatar_url]) if contact_params[:remote_avatar_url]
|
||||
@contact_inbox = ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
|
||||
end
|
||||
|
||||
def build_message
|
||||
@message = conversation.messages.create!(message_params)
|
||||
(response.attachments || []).each do |attachment|
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||
end
|
||||
end
|
||||
|
||||
def attach_file(attachment, file_url)
|
||||
file_resource = LocalResource.new(file_url)
|
||||
attachment.file.attach(io: file_resource.file, filename: file_resource.tmp_filename, content_type: file_resource.encoding)
|
||||
end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Conversation.find_by(conversation_params) || build_conversation
|
||||
end
|
||||
|
||||
def build_conversation
|
||||
@contact_inbox ||= contact.contact_inboxes.find_by!(source_id: @sender_id)
|
||||
Conversation.create!(conversation_params.merge(
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
))
|
||||
end
|
||||
|
||||
def attachment_params(attachment)
|
||||
file_type = attachment['type'].to_sym
|
||||
params = { file_type: file_type, account_id: @message.account_id }
|
||||
|
||||
if [:image, :file, :audio, :video].include? file_type
|
||||
params.merge!(file_type_params(attachment))
|
||||
elsif file_type == :location
|
||||
params.merge!(location_params(attachment))
|
||||
elsif file_type == :fallback
|
||||
params.merge!(fallback_params(attachment))
|
||||
def message_type
|
||||
if @conversation.inbox.channel_type != 'Channel::Api' && @message_type == 'incoming'
|
||||
raise StandardError, 'Incoming messages are only allowed in Api inboxes'
|
||||
end
|
||||
|
||||
params
|
||||
@message_type
|
||||
end
|
||||
|
||||
def file_type_params(attachment)
|
||||
{
|
||||
external_url: attachment['payload']['url'],
|
||||
remote_file_url: attachment['payload']['url']
|
||||
}
|
||||
end
|
||||
|
||||
def location_params(attachment)
|
||||
lat = attachment['payload']['coordinates']['lat']
|
||||
long = attachment['payload']['coordinates']['long']
|
||||
{
|
||||
external_url: attachment['url'],
|
||||
coordinates_lat: lat,
|
||||
coordinates_long: long,
|
||||
fallback_title: attachment['title']
|
||||
}
|
||||
end
|
||||
|
||||
def fallback_params(attachment)
|
||||
{
|
||||
fallback_title: attachment['title'],
|
||||
external_url: attachment['url']
|
||||
}
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: @inbox.account_id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: contact.id
|
||||
}
|
||||
def sender
|
||||
message_type == 'outgoing' ? @user : @conversation.contact
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: @message_type,
|
||||
content: response.content,
|
||||
source_id: response.identifier,
|
||||
sender: contact
|
||||
}
|
||||
end
|
||||
|
||||
def contact_params
|
||||
begin
|
||||
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
|
||||
result = k.get_object(@sender_id) || {}
|
||||
rescue Exception => e
|
||||
result = {}
|
||||
Raven.capture_exception(e)
|
||||
end
|
||||
{
|
||||
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
|
||||
account_id: @inbox.account_id,
|
||||
remote_avatar_url: result['profile_pic'] || ''
|
||||
account_id: @conversation.account_id,
|
||||
inbox_id: @conversation.inbox_id,
|
||||
message_type: message_type,
|
||||
content: @content,
|
||||
private: @private,
|
||||
sender: sender,
|
||||
content_type: @content_type,
|
||||
items: @items,
|
||||
in_reply_to: @in_reply_to
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
class Messages::Outgoing::EchoBuilder < ::Messages::MessageBuilder
|
||||
end
|
|
@ -1,46 +0,0 @@
|
|||
class Messages::Outgoing::NormalBuilder
|
||||
include ::FileTypeHelper
|
||||
attr_reader :message
|
||||
|
||||
def initialize(user, conversation, params)
|
||||
@content = params[:content]
|
||||
@private = params[:private] || false
|
||||
@conversation = conversation
|
||||
@user = user
|
||||
@fb_id = params[:fb_id]
|
||||
@content_type = params[:content_type]
|
||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||
@attachments = params[:attachments]
|
||||
end
|
||||
|
||||
def perform
|
||||
@message = @conversation.messages.build(message_params)
|
||||
if @attachments.present?
|
||||
@attachments.each do |uploaded_attachment|
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_type(uploaded_attachment&.content_type)
|
||||
)
|
||||
attachment.file.attach(uploaded_attachment)
|
||||
end
|
||||
end
|
||||
@message.save
|
||||
@message
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: @conversation.account_id,
|
||||
inbox_id: @conversation.inbox_id,
|
||||
message_type: :outgoing,
|
||||
content: @content,
|
||||
private: @private,
|
||||
sender: @user,
|
||||
source_id: @fb_id,
|
||||
content_type: @content_type,
|
||||
items: @items
|
||||
}
|
||||
end
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::BaseController
|
||||
before_action :ensure_contact
|
||||
before_action :ensure_inbox, only: [:create]
|
||||
before_action :validate_channel_type
|
||||
|
||||
def create
|
||||
source_id = params[:source_id] || SecureRandom.uuid
|
||||
@contact_inbox = ContactInbox.create(contact: @contact, inbox: @inbox, source_id: source_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_channel_type
|
||||
return if @inbox.channel_type == 'Channel::Api'
|
||||
|
||||
render json: { error: 'Contact Inbox creation is only allowed in API inboxes' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def ensure_inbox
|
||||
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
end
|
||||
|
||||
def ensure_contact
|
||||
@contact = Current.account.contacts.find(params[:contact_id])
|
||||
end
|
||||
end
|
|
@ -11,21 +11,37 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
|||
def show; end
|
||||
|
||||
def create
|
||||
@contact = Current.account.contacts.new(contact_create_params)
|
||||
@contact.save!
|
||||
render json: @contact
|
||||
ActiveRecord::Base.transaction do
|
||||
@contact = Current.account.contacts.new(contact_create_params)
|
||||
@contact.save!
|
||||
@contact_inbox = build_contact_inbox
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@contact.update!(contact_params)
|
||||
end
|
||||
|
||||
def search
|
||||
render json: { error: 'Specify search string with parameter q' }, status: :unprocessable_entity if params[:q].blank? && return
|
||||
|
||||
@contacts = Current.account.contacts.where('name LIKE :search OR email LIKE :search', search: "%#{params[:q]}%")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_authorization
|
||||
authorize(Contact)
|
||||
end
|
||||
|
||||
def build_contact_inbox
|
||||
return if params[:inbox_id].blank?
|
||||
|
||||
inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
source_id = params[:source_id] || SecureRandom.uuid
|
||||
ContactInbox.create(contact: @contact, inbox: inbox, source_id: source_id)
|
||||
end
|
||||
|
||||
def contact_params
|
||||
params.require(:contact).permit(:name, :email, :phone_number)
|
||||
end
|
||||
|
|
|
@ -5,8 +5,10 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
|||
|
||||
def create
|
||||
user = current_user || @resource
|
||||
mb = Messages::Outgoing::NormalBuilder.new(user, @conversation, params)
|
||||
mb = Messages::MessageBuilder.new(user, @conversation, params)
|
||||
@message = mb.perform
|
||||
rescue StandardError => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -44,9 +44,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
end
|
||||
|
||||
def update_last_seen
|
||||
@conversation.agent_last_seen_at = parsed_last_seen_at
|
||||
@conversation.agent_last_seen_at = DateTime.now.utc
|
||||
@conversation.save!
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -56,10 +55,6 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
|||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
|
||||
end
|
||||
|
||||
def parsed_last_seen_at
|
||||
DateTime.strptime(params[:agent_last_seen_at].to_s, '%s')
|
||||
end
|
||||
|
||||
def conversation
|
||||
@conversation ||= Current.account.conversations.find_by(display_id: params[:id])
|
||||
end
|
||||
|
|
|
@ -4,12 +4,12 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
before_action :check_authorization
|
||||
|
||||
def index
|
||||
@inboxes = policy_scope(Current.account.inboxes)
|
||||
@inboxes = policy_scope(Current.account.inboxes.order_by_id.includes(:channel, :avatar_attachment))
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
channel = web_widgets.create!(permitted_params[:channel].except(:type)) if permitted_params[:channel][:type] == 'web_widget'
|
||||
channel = create_channel
|
||||
@inbox = Current.account.inboxes.build(
|
||||
name: permitted_params[:name],
|
||||
greeting_message: permitted_params[:greeting_message],
|
||||
|
@ -23,7 +23,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
|
||||
def update
|
||||
@inbox.update(inbox_update_params.except(:channel))
|
||||
@inbox.channel.update!(inbox_update_params[:channel]) if @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present?
|
||||
return unless @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present?
|
||||
|
||||
@inbox.channel.update!(inbox_update_params[:channel])
|
||||
update_channel_feature_flags
|
||||
end
|
||||
|
||||
def set_agent_bot
|
||||
|
@ -52,21 +55,43 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
|
||||
end
|
||||
|
||||
def web_widgets
|
||||
Current.account.web_widgets
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
authorize(Inbox)
|
||||
end
|
||||
|
||||
def create_channel
|
||||
case permitted_params[:channel][:type]
|
||||
when 'web_widget'
|
||||
Current.account.web_widgets.create!(permitted_params[:channel].except(:type))
|
||||
when 'api'
|
||||
Current.account.api_channels.create!(permitted_params[:channel].except(:type))
|
||||
when 'email'
|
||||
Current.account.email_channels.create!(permitted_params[:channel].except(:type))
|
||||
end
|
||||
end
|
||||
|
||||
def update_channel_feature_flags
|
||||
return unless inbox_update_params[:channel].key? :selected_feature_flags
|
||||
|
||||
@inbox.channel.selected_feature_flags = inbox_update_params[:channel][:selected_feature_flags]
|
||||
@inbox.channel.save!
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :avatar, :name, :greeting_message, :greeting_enabled, channel:
|
||||
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline])
|
||||
[:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :webhook_url, :email])
|
||||
end
|
||||
|
||||
def inbox_update_params
|
||||
params.permit(:enable_auto_assignment, :name, :avatar, :greeting_message, :greeting_enabled,
|
||||
channel: [:website_url, :widget_color, :welcome_title, :welcome_tagline])
|
||||
channel: [
|
||||
:website_url,
|
||||
:widget_color,
|
||||
:welcome_title,
|
||||
:welcome_tagline,
|
||||
:webhook_url,
|
||||
:email,
|
||||
selected_feature_flags: []
|
||||
])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,14 +14,15 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
with: :render_error_response
|
||||
|
||||
def create
|
||||
@user = AccountBuilder.new(
|
||||
@user, @account = AccountBuilder.new(
|
||||
account_name: account_params[:account_name],
|
||||
email: account_params[:email],
|
||||
confirmed: confirmed?
|
||||
confirmed: confirmed?,
|
||||
user: current_user
|
||||
).perform
|
||||
if @user
|
||||
send_auth_headers(@user)
|
||||
render partial: 'devise/auth.json', locals: { resource: @user }
|
||||
render 'api/v1/accounts/create.json', locals: { resource: @user }
|
||||
else
|
||||
render_error_response(CustomExceptions::Account::SignupFailed.new({}))
|
||||
end
|
||||
|
@ -32,7 +33,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def update
|
||||
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :domain_emails_enabled))
|
||||
@account.update!(account_params.slice(:name, :locale, :domain, :support_email))
|
||||
end
|
||||
|
||||
def update_active_at
|
||||
|
@ -57,7 +58,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def account_params
|
||||
params.permit(:account_name, :email, :name, :locale, :domain, :support_email, :domain_emails_enabled)
|
||||
params.permit(:account_name, :email, :name, :locale, :domain, :support_email)
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
|
|
|
@ -16,6 +16,14 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||
end
|
||||
|
||||
def profile_params
|
||||
params.require(:profile).permit(:email, :name, :password, :password_confirmation, :avatar, :availability)
|
||||
params.require(:profile).permit(
|
||||
:email,
|
||||
:name,
|
||||
:display_name,
|
||||
:password,
|
||||
:password_confirmation,
|
||||
:avatar,
|
||||
:availability
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,4 +30,14 @@ class Api::V1::Widget::BaseController < ApplicationController
|
|||
)
|
||||
@contact = @contact_inbox.contact
|
||||
end
|
||||
|
||||
def browser_params
|
||||
{
|
||||
browser_name: browser.name,
|
||||
browser_version: browser.full_version,
|
||||
device_name: browser.device.name,
|
||||
platform_name: browser.platform.name,
|
||||
platform_version: browser.platform.version
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,7 +11,8 @@ class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController
|
|||
def event_info
|
||||
{
|
||||
widget_language: params[:locale],
|
||||
browser_language: browser.accept_language.first&.code
|
||||
browser_language: browser.accept_language.first&.code,
|
||||
browser: browser_params
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -66,16 +66,6 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
|||
}
|
||||
end
|
||||
|
||||
def browser_params
|
||||
{
|
||||
browser_name: browser.name,
|
||||
browser_version: browser.full_version,
|
||||
device_name: browser.device.name,
|
||||
platform_name: browser.platform.name,
|
||||
platform_version: browser.platform.version
|
||||
}
|
||||
end
|
||||
|
||||
def timestamp_params
|
||||
{
|
||||
timestamp: permitted_params[:message][:timestamp]
|
||||
|
|
|
@ -15,7 +15,10 @@ class DashboardController < ActionController::Base
|
|||
'WIDGET_BRAND_URL',
|
||||
'TERMS_URL',
|
||||
'PRIVACY_URL',
|
||||
'DISPLAY_MANIFEST'
|
||||
'DISPLAY_MANIFEST',
|
||||
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD'
|
||||
).merge(
|
||||
APP_VERSION: Chatwoot.config[:version]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,7 +11,7 @@ class WidgetsController < ActionController::Base
|
|||
private
|
||||
|
||||
def set_global_config
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'INSTALLATION_NAME', 'WIDGET_BRAND_URL')
|
||||
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'WIDGET_BRAND_URL')
|
||||
end
|
||||
|
||||
def set_web_widget
|
||||
|
|
|
@ -24,7 +24,7 @@ class UserDashboard < Administrate::BaseDashboard
|
|||
confirmation_sent_at: Field::DateTime,
|
||||
unconfirmed_email: Field::String,
|
||||
name: Field::String,
|
||||
nickname: Field::String,
|
||||
display_name: Field::String,
|
||||
email: Field::String,
|
||||
tokens: Field::String.with_options(searchable: false),
|
||||
created_at: Field::DateTime,
|
||||
|
@ -53,7 +53,7 @@ class UserDashboard < Administrate::BaseDashboard
|
|||
avatar_url
|
||||
unconfirmed_email
|
||||
name
|
||||
nickname
|
||||
display_name
|
||||
email
|
||||
created_at
|
||||
updated_at
|
||||
|
@ -65,7 +65,7 @@ class UserDashboard < Administrate::BaseDashboard
|
|||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
name
|
||||
nickname
|
||||
display_name
|
||||
email
|
||||
password
|
||||
].freeze
|
||||
|
|
2
app/drops/account_drop.rb
Normal file
2
app/drops/account_drop.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
class AccountDrop < BaseDrop
|
||||
end
|
13
app/drops/base_drop.rb
Normal file
13
app/drops/base_drop.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
class BaseDrop < Liquid::Drop
|
||||
def initialize(obj)
|
||||
@obj = obj
|
||||
end
|
||||
|
||||
def id
|
||||
@obj.try(:id)
|
||||
end
|
||||
|
||||
def name
|
||||
@obj.try(:name)
|
||||
end
|
||||
end
|
5
app/drops/conversation_drop.rb
Normal file
5
app/drops/conversation_drop.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
class ConversationDrop < BaseDrop
|
||||
def display_id
|
||||
@obj.try(:display_id)
|
||||
end
|
||||
end
|
2
app/drops/inbox_drop.rb
Normal file
2
app/drops/inbox_drop.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
class InboxDrop < BaseDrop
|
||||
end
|
2
app/drops/user_drop.rb
Normal file
2
app/drops/user_drop.rb
Normal file
|
@ -0,0 +1,2 @@
|
|||
class UserDrop < BaseDrop
|
||||
end
|
|
@ -1,9 +1,14 @@
|
|||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class AccountAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('', { accountScoped: true });
|
||||
}
|
||||
|
||||
createAccount(data) {
|
||||
return axios.post(`${this.apiVersion}/accounts`, data);
|
||||
}
|
||||
}
|
||||
|
||||
export default new AccountAPI();
|
||||
|
|
|
@ -118,7 +118,12 @@ export default {
|
|||
return axios.post(urlData.url, { email });
|
||||
},
|
||||
|
||||
profileUpdate({ password, password_confirmation, ...profileAttributes }) {
|
||||
profileUpdate({
|
||||
password,
|
||||
password_confirmation,
|
||||
displayName,
|
||||
...profileAttributes
|
||||
}) {
|
||||
const formData = new FormData();
|
||||
Object.keys(profileAttributes).forEach(key => {
|
||||
const value = profileAttributes[key];
|
||||
|
@ -126,6 +131,7 @@ export default {
|
|||
formData.append(`profile[${key}]`, value);
|
||||
}
|
||||
});
|
||||
formData.append('profile[display_name]', displayName || '');
|
||||
if (password && password_confirmation) {
|
||||
formData.append('profile[password]', password);
|
||||
formData.append('profile[password_confirmation]', password_confirmation);
|
||||
|
|
|
@ -29,10 +29,8 @@ class ConversationApi extends ApiClient {
|
|||
);
|
||||
}
|
||||
|
||||
markMessageRead({ id, lastSeen }) {
|
||||
return axios.post(`${this.url}/${id}/update_last_seen`, {
|
||||
agent_last_seen_at: lastSeen,
|
||||
});
|
||||
markMessageRead({ id }) {
|
||||
return axios.post(`${this.url}/${id}/update_last_seen`);
|
||||
}
|
||||
|
||||
toggleTyping({ conversationId, status }) {
|
||||
|
|
|
@ -7,10 +7,11 @@ class MessageApi extends ApiClient {
|
|||
super('conversations', { accountScoped: true });
|
||||
}
|
||||
|
||||
create({ conversationId, message, private: isPrivate }) {
|
||||
create({ conversationId, message, private: isPrivate, contentAttributes }) {
|
||||
return axios.post(`${this.url}/${conversationId}/messages`, {
|
||||
content: message,
|
||||
private: isPrivate,
|
||||
content_attributes: contentAttributes,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -20,9 +21,10 @@ class MessageApi extends ApiClient {
|
|||
});
|
||||
}
|
||||
|
||||
sendAttachment([conversationId, { file }]) {
|
||||
sendAttachment([conversationId, { file, isPrivate = false }]) {
|
||||
const formData = new FormData();
|
||||
formData.append('attachments[]', file, file.name);
|
||||
formData.append('private', isPrivate);
|
||||
return axios({
|
||||
method: 'post',
|
||||
url: `${this.url}/${conversationId}/messages`,
|
||||
|
|
BIN
app/javascript/dashboard/assets/images/channels/api.png
Normal file
BIN
app/javascript/dashboard/assets/images/channels/api.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
BIN
app/javascript/dashboard/assets/images/channels/email.png
Normal file
BIN
app/javascript/dashboard/assets/images/channels/email.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
|
@ -1,11 +1,11 @@
|
|||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.app-wrapper {
|
||||
|
@ -26,36 +26,40 @@ body {
|
|||
|
||||
.view-box {
|
||||
@include full-height;
|
||||
height: 100vh;
|
||||
@include margin(0);
|
||||
@include space-between-column;
|
||||
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.view-panel {
|
||||
flex-direction: column;
|
||||
@include margin($zero);
|
||||
@include padding($space-normal);
|
||||
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.content-box {
|
||||
overflow: auto;
|
||||
@include padding($space-normal);
|
||||
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
@include flex;
|
||||
|
||||
align-items: center;
|
||||
color: $color-woot;
|
||||
cursor: pointer;
|
||||
font-size: $font-size-default;
|
||||
font-weight: $font-weight-normal;
|
||||
margin-right: $space-normal;
|
||||
cursor: pointer;
|
||||
|
||||
&:before {
|
||||
vertical-align: text-bottom;
|
||||
margin-right: $space-smaller;
|
||||
&::before {
|
||||
font-size: $font-size-large;
|
||||
margin-right: $space-small;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,12 +70,14 @@ body {
|
|||
.no-items-error-message {
|
||||
@include flex;
|
||||
@include full-height;
|
||||
justify-content: center;
|
||||
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
max-width: $space-mega;
|
||||
@include padding($space-one);
|
||||
|
||||
max-width: $space-mega;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,8 +46,8 @@ $color-gray: #6e6f73;
|
|||
$color-light-gray: #999a9b;
|
||||
$color-border: #e0e6ed;
|
||||
$color-border-light: #f0f4f5;
|
||||
$color-background: #f4f6fb;
|
||||
$color-border-dark: #cad0d4;
|
||||
$color-background: #f4f6fb;
|
||||
$color-background-light: #f9fafc;
|
||||
$color-white: #fff;
|
||||
$color-body: #3c4858;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
@import 'shared/assets/fonts/inter';
|
||||
|
||||
@import 'shared/assets/stylesheets/colors';
|
||||
@import 'shared/assets/stylesheets/spacing';
|
||||
@import 'shared/assets/stylesheets/font-size';
|
||||
@import 'variables';
|
||||
|
||||
@import '~spinkit/scss/spinners/7-three-bounce';
|
||||
|
|
|
@ -202,7 +202,7 @@
|
|||
}
|
||||
|
||||
.settings--content {
|
||||
@include margin($space-small $space-larger);
|
||||
@include margin($space-small $space-large);
|
||||
|
||||
.title {
|
||||
font-weight: $font-weight-medium;
|
||||
|
|
|
@ -8,21 +8,9 @@
|
|||
font-weight: $font-weight-normal;
|
||||
position: relative;
|
||||
|
||||
.icon {
|
||||
bottom: $space-smaller;
|
||||
position: absolute;
|
||||
right: $space-small;
|
||||
}
|
||||
|
||||
.message-text__wrap {
|
||||
position: relative;
|
||||
|
||||
.time {
|
||||
color: $color-primary-light;
|
||||
display: block;
|
||||
font-size: $font-size-micro;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $color-white;
|
||||
|
@ -37,24 +25,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.audio {
|
||||
.time {
|
||||
margin-top: -$space-two;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
.time {
|
||||
bottom: $space-smaller;
|
||||
color: $color-white;
|
||||
position: absolute;
|
||||
right: $space-small;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -74,30 +48,6 @@
|
|||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.map {
|
||||
@include flex;
|
||||
flex-direction: column;
|
||||
text-align: right;
|
||||
|
||||
img {
|
||||
@include padding($space-small);
|
||||
max-height: 30rem;
|
||||
max-width: 20rem;
|
||||
}
|
||||
|
||||
.time {
|
||||
@include padding($space-small);
|
||||
margin-left: -$space-smaller;
|
||||
margin-top: -$space-two;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.locname {
|
||||
font-weight: $font-weight-medium;
|
||||
padding: $space-smaller;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversations-sidebar {
|
||||
|
@ -257,14 +207,6 @@
|
|||
color: $color-body;
|
||||
margin-right: auto;
|
||||
|
||||
.time {
|
||||
color: $color-light-gray;
|
||||
}
|
||||
|
||||
.image .time {
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $color-primary-dark;
|
||||
}
|
||||
|
@ -321,10 +263,6 @@
|
|||
right: $space-one;
|
||||
top: $space-smaller + $space-micro;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: $color-light-gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -389,11 +327,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
color: $medium-gray;
|
||||
font-size: $font-size-micro;
|
||||
margin-left: $space-slab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,15 +7,25 @@
|
|||
<p v-if="headerContent" class="small-12 column">
|
||||
{{ headerContent }}
|
||||
</p>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
headerTitle: String,
|
||||
headerContent: String,
|
||||
headerImage: String,
|
||||
headerTitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
headerContent: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
headerImage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<button
|
||||
:type="type"
|
||||
data-testid="submit_button"
|
||||
:disabled="disabled"
|
||||
:class="computedClass"
|
||||
@click="onClick"
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
class="dropdown-pane top"
|
||||
>
|
||||
<ul class="vertical dropdown menu">
|
||||
<li v-if="currentUser.accounts.length > 1">
|
||||
<li v-if="showChangeAccountOption">
|
||||
<button
|
||||
class="button clear change-accounts--button"
|
||||
@click="changeAccount"
|
||||
|
@ -58,12 +58,12 @@
|
|||
<div class="current-user" @click.prevent="showOptions()">
|
||||
<thumbnail
|
||||
:src="currentUser.avatar_url"
|
||||
:username="currentUser.name"
|
||||
:username="currentUserAvailableName"
|
||||
:status="currentUser.availability_status"
|
||||
/>
|
||||
<div class="current-user--data">
|
||||
<h3 class="current-user--name">
|
||||
{{ currentUser.name }}
|
||||
{{ currentUserAvailableName }}
|
||||
</h3>
|
||||
<h5 class="current-user--role">
|
||||
{{ currentRole }}
|
||||
|
@ -94,6 +94,58 @@
|
|||
</label>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="globalConfig.createNewAccountFromDashboard"
|
||||
class="modal-footer delete-item"
|
||||
>
|
||||
<button
|
||||
class="button success large expanded nice"
|
||||
@click="createAccount"
|
||||
>
|
||||
{{ $t('CREATE_ACCOUNT.NEW_ACCOUNT') }}
|
||||
</button>
|
||||
</div>
|
||||
</woot-modal>
|
||||
|
||||
<woot-modal
|
||||
:show="showCreateAccountModal"
|
||||
:on-close="onCloseCreate"
|
||||
class="account-selector--modal"
|
||||
>
|
||||
<div class="column content-box">
|
||||
<woot-modal-header
|
||||
:header-title="$t('CREATE_ACCOUNT.NEW_ACCOUNT')"
|
||||
:header-content="$t('CREATE_ACCOUNT.SELECTOR_SUBTITLE')"
|
||||
/>
|
||||
|
||||
<form class="row" @submit.prevent="addAccount()">
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.accountName.$error }">
|
||||
{{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model.trim="accountName"
|
||||
type="text"
|
||||
:placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')"
|
||||
@input="$v.accountName.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-footer medium-12 columns">
|
||||
<div class="medium-12 columns">
|
||||
<woot-submit-button
|
||||
:disabled="
|
||||
$v.accountName.$invalid ||
|
||||
$v.accountName.$invalid ||
|
||||
uiFlags.isCreating
|
||||
"
|
||||
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
|
||||
:loading="uiFlags.isCreating"
|
||||
button-class="large expanded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</aside>
|
||||
</template>
|
||||
|
@ -108,13 +160,16 @@ import SidebarItem from './SidebarItem';
|
|||
import { frontendURL } from '../../helper/URLHelper';
|
||||
import Thumbnail from '../widgets/Thumbnail';
|
||||
import { getSidebarItems } from '../../i18n/default-sidebar';
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
// import accountMixin from '../../../../../mixins/account';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SidebarItem,
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [clickaway, adminMixin],
|
||||
mixins: [clickaway, adminMixin, alertMixin],
|
||||
props: {
|
||||
route: {
|
||||
type: String,
|
||||
|
@ -125,8 +180,18 @@ export default {
|
|||
return {
|
||||
showOptionsMenu: false,
|
||||
showAccountModal: false,
|
||||
showCreateAccountModal: false,
|
||||
accountName: '',
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
accountName: {
|
||||
required,
|
||||
minLength: minLength(1),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
|
@ -134,8 +199,19 @@ export default {
|
|||
inboxes: 'inboxes/getInboxes',
|
||||
accountId: 'getCurrentAccountId',
|
||||
currentRole: 'getCurrentRole',
|
||||
uiFlags: 'agents/getUIFlags',
|
||||
accountLabels: 'labels/getLabelsOnSidebar',
|
||||
}),
|
||||
currentUserAvailableName() {
|
||||
const { available_name: availableName } = this.currentUser;
|
||||
return availableName;
|
||||
},
|
||||
showChangeAccountOption() {
|
||||
if (this.globalConfig.createNewAccountFromDashboard) {
|
||||
return true;
|
||||
}
|
||||
return this.currentUser.accounts.length > 1;
|
||||
},
|
||||
sidemenuItems() {
|
||||
return getSidebarItems(this.accountId);
|
||||
},
|
||||
|
@ -230,6 +306,29 @@ export default {
|
|||
onClose() {
|
||||
this.showAccountModal = false;
|
||||
},
|
||||
createAccount() {
|
||||
this.showAccountModal = false;
|
||||
this.showCreateAccountModal = true;
|
||||
},
|
||||
onCloseCreate() {
|
||||
this.showCreateAccountModal = false;
|
||||
},
|
||||
async addAccount() {
|
||||
try {
|
||||
const account_id = await this.$store.dispatch('accounts/create', {
|
||||
account_name: this.accountName,
|
||||
});
|
||||
this.onClose();
|
||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
|
||||
window.location = `/app/accounts/${account_id}/dashboard`;
|
||||
} catch (error) {
|
||||
if (error.response.status === 422) {
|
||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
|
||||
} else {
|
||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -57,13 +57,8 @@ import { mapGetters } from 'vuex';
|
|||
|
||||
import router from '../../routes';
|
||||
import adminMixin from '../../mixins/isAdmin';
|
||||
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
||||
|
||||
const INBOX_TYPES = {
|
||||
WEB: 'Channel::WebWidget',
|
||||
FB: 'Channel::FacebookPage',
|
||||
TWITTER: 'Channel::TwitterProfile',
|
||||
TWILIO: 'Channel::TwilioSms',
|
||||
};
|
||||
const getInboxClassByType = type => {
|
||||
switch (type) {
|
||||
case INBOX_TYPES.WEB:
|
||||
|
@ -78,6 +73,12 @@ const getInboxClassByType = type => {
|
|||
case INBOX_TYPES.TWILIO:
|
||||
return 'ion-android-textsms';
|
||||
|
||||
case INBOX_TYPES.API:
|
||||
return 'ion-cloud';
|
||||
|
||||
case INBOX_TYPES.EMAIL:
|
||||
return 'ion-email';
|
||||
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint no-unused-vars: ["error", { "args": "none" }] */
|
||||
|
||||
export default {
|
||||
name: 'WootTabs',
|
||||
props: {
|
||||
|
@ -8,7 +6,7 @@ export default {
|
|||
default: 0,
|
||||
},
|
||||
},
|
||||
render(h) {
|
||||
render() {
|
||||
const Tabs = this.$slots.default
|
||||
.filter(
|
||||
node =>
|
||||
|
|
|
@ -1,7 +1,19 @@
|
|||
/* eslint no-unused-vars: ["error", { "args": "none" }] */
|
||||
/* eslint prefer-template: 0 */
|
||||
/* eslint no-console: 0 */
|
||||
/* eslint func-names: 0 */
|
||||
<template>
|
||||
<li
|
||||
:class="{
|
||||
'tabs-title': true,
|
||||
'is-active': active,
|
||||
}"
|
||||
>
|
||||
<a @click="onTabClick">
|
||||
{{ name }}
|
||||
<span v-if="showBadge" class="badge">
|
||||
{{ getItemCount }}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</template>
|
||||
<script>
|
||||
import TWEEN from 'tween.js';
|
||||
|
||||
export default {
|
||||
|
@ -23,6 +35,10 @@ export default {
|
|||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
showBadge: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -48,12 +64,12 @@ export default {
|
|||
TWEEN.update(time);
|
||||
animationFrame = window.requestAnimationFrame(animate);
|
||||
};
|
||||
const that = this;
|
||||
new TWEEN.Tween({ tweeningNumber: oldValue })
|
||||
const tweeningNumber = { value: oldValue };
|
||||
new TWEEN.Tween(tweeningNumber)
|
||||
.easing(TWEEN.Easing.Quadratic.Out)
|
||||
.to({ tweeningNumber: newValue }, 500)
|
||||
.onUpdate(function() {
|
||||
that.animatedNumber = this.tweeningNumber.toFixed(0);
|
||||
.to({ value: newValue }, 500)
|
||||
.onUpdate(() => {
|
||||
this.animatedNumber = tweeningNumber.value.toFixed(0);
|
||||
})
|
||||
.onComplete(() => {
|
||||
window.cancelAnimationFrame(animationFrame);
|
||||
|
@ -62,28 +78,13 @@ export default {
|
|||
animationFrame = window.requestAnimationFrame(animate);
|
||||
},
|
||||
},
|
||||
|
||||
render(h) {
|
||||
return (
|
||||
<li
|
||||
class={{
|
||||
'tabs-title': true,
|
||||
'is-active': this.active,
|
||||
'uk-disabled': this.disabled,
|
||||
}}
|
||||
>
|
||||
<a
|
||||
on-click={event => {
|
||||
event.preventDefault();
|
||||
if (!this.disabled) {
|
||||
this.$parent.$emit('change', this.index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{`${this.name}`}
|
||||
<span class="badge">{this.getItemCount}</span>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
methods: {
|
||||
onTabClick(event) {
|
||||
event.preventDefault();
|
||||
if (!this.disabled) {
|
||||
this.$parent.$emit('change', this.index);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,35 +1,43 @@
|
|||
<template>
|
||||
<div
|
||||
class="small-3 columns channel"
|
||||
:class="{ inactive: !isActive(channel) }"
|
||||
:class="{ inactive: !isActive }"
|
||||
@click="onItemClick"
|
||||
>
|
||||
<img
|
||||
v-if="channel === 'facebook'"
|
||||
v-if="channel.key === 'facebook'"
|
||||
src="~dashboard/assets/images/channels/facebook.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel === 'twitter'"
|
||||
v-if="channel.key === 'twitter'"
|
||||
src="~dashboard/assets/images/channels/twitter.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel === 'telegram'"
|
||||
v-if="channel.key === 'telegram'"
|
||||
src="~dashboard/assets/images/channels/telegram.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel === 'line'"
|
||||
v-if="channel.key === 'api'"
|
||||
src="~dashboard/assets/images/channels/api.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel.key === 'email'"
|
||||
src="~dashboard/assets/images/channels/email.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel.key === 'line'"
|
||||
src="~dashboard/assets/images/channels/line.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel === 'website'"
|
||||
v-if="channel.key === 'website'"
|
||||
src="~dashboard/assets/images/channels/website.png"
|
||||
/>
|
||||
<img
|
||||
v-if="channel === 'twilio'"
|
||||
v-if="channel.key === 'twilio'"
|
||||
src="~dashboard/assets/images/channels/twilio.png"
|
||||
/>
|
||||
<h3 class="channel__title">
|
||||
{{ channel }}
|
||||
{{ channel.name }}
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -37,7 +45,7 @@
|
|||
export default {
|
||||
props: {
|
||||
channel: {
|
||||
type: String,
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
enabledFeatures: {
|
||||
|
@ -45,22 +53,28 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isActive(channel) {
|
||||
computed: {
|
||||
isActive() {
|
||||
const { key } = this.channel;
|
||||
if (Object.keys(this.enabledFeatures) === 0) {
|
||||
return false;
|
||||
}
|
||||
if (channel === 'facebook') {
|
||||
if (key === 'facebook') {
|
||||
return this.enabledFeatures.channel_facebook;
|
||||
}
|
||||
if (channel === 'twitter') {
|
||||
return this.enabledFeatures.channel_facebook;
|
||||
if (key === 'twitter') {
|
||||
return this.enabledFeatures.channel_twitter;
|
||||
}
|
||||
return ['website', 'twilio'].includes(channel);
|
||||
if (key === 'email') {
|
||||
return this.enabledFeatures.channel_email;
|
||||
}
|
||||
return ['website', 'twilio', 'api'].includes(key);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onItemClick() {
|
||||
if (this.isActive(this.channel)) {
|
||||
this.$emit('channel-item-click', this.channel);
|
||||
if (this.isActive) {
|
||||
this.$emit('channel-item-click', this.channel.key);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<multiselect
|
||||
v-model="currentChat.meta.assignee"
|
||||
:options="agentList"
|
||||
label="name"
|
||||
label="available_name"
|
||||
:allow-empty="true"
|
||||
deselect-label="Remove"
|
||||
placeholder="Select Agent"
|
||||
|
@ -95,7 +95,7 @@ export default {
|
|||
return [
|
||||
{
|
||||
confirmed: true,
|
||||
name: 'None',
|
||||
available_name: 'None',
|
||||
id: 0,
|
||||
role: 'agent',
|
||||
account_id: 0,
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
</span>
|
||||
</div>
|
||||
<!-- No conversation selected -->
|
||||
<div v-else-if="allConversations.length && currentChat.id === null">
|
||||
<div v-else-if="allConversations.length && !currentChat.id">
|
||||
<img src="~dashboard/assets/images/chat.svg" alt="No Chat" />
|
||||
<span>{{ $t('CONVERSATION.404') }}</span>
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
<bubble-text
|
||||
v-if="data.content"
|
||||
:message="message"
|
||||
:is-email="isEmailContentType"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<span v-if="hasAttachments">
|
||||
|
@ -21,19 +22,29 @@
|
|||
/>
|
||||
</span>
|
||||
</span>
|
||||
<i
|
||||
v-if="isPrivate"
|
||||
v-tooltip.top-start="toolTipMessage"
|
||||
class="icon ion-android-lock"
|
||||
@mouseenter="isHovered = true"
|
||||
@mouseleave="isHovered = false"
|
||||
<bubble-actions
|
||||
:id="data.id"
|
||||
:sender="data.sender"
|
||||
:is-a-tweet="isATweet"
|
||||
:is-email="isEmailContentType"
|
||||
:is-private="data.private"
|
||||
:message-type="data.message_type"
|
||||
:readable-time="readableTime"
|
||||
:source-id="data.source_id"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<div v-if="isATweet && isIncoming && sender" class="sender--info">
|
||||
<woot-thumbnail
|
||||
:src="sender.thumbnail"
|
||||
:username="sender.name"
|
||||
size="16px"
|
||||
/>
|
||||
<div class="sender--available-name">
|
||||
{{ sender.available_name || sender.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <img
|
||||
src="https://randomuser.me/api/portraits/women/94.jpg"
|
||||
class="sender--thumbnail"
|
||||
/> -->
|
||||
</li>
|
||||
</template>
|
||||
<script>
|
||||
|
@ -43,19 +54,27 @@ import timeMixin from '../../../mixins/time';
|
|||
import BubbleText from './bubble/Text';
|
||||
import BubbleImage from './bubble/Image';
|
||||
import BubbleFile from './bubble/File';
|
||||
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
||||
import BubbleActions from './bubble/Actions';
|
||||
import { MESSAGE_TYPE } from 'shared/constants/messageTypes';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BubbleActions,
|
||||
BubbleText,
|
||||
BubbleImage,
|
||||
BubbleFile,
|
||||
},
|
||||
mixins: [timeMixin, messageFormatterMixin],
|
||||
mixins: [timeMixin, messageFormatterMixin, contentTypeMixin],
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isATweet: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -64,7 +83,16 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
message() {
|
||||
return this.formatMessage(this.data.content);
|
||||
return this.formatMessage(this.data.content, this.isATweet);
|
||||
},
|
||||
sender() {
|
||||
return this.data.sender || {};
|
||||
},
|
||||
contentType() {
|
||||
const {
|
||||
data: { content_type: contentType },
|
||||
} = this;
|
||||
return contentType;
|
||||
},
|
||||
alignBubble() {
|
||||
return !this.data.message_type ? 'left' : 'right';
|
||||
|
@ -75,6 +103,9 @@ export default {
|
|||
isBubble() {
|
||||
return [0, 1, 3].includes(this.data.message_type);
|
||||
},
|
||||
isIncoming() {
|
||||
return this.data.message_type === MESSAGE_TYPE.INCOMING;
|
||||
},
|
||||
hasAttachments() {
|
||||
return !!(this.data.attachments && this.data.attachments.length > 0);
|
||||
},
|
||||
|
@ -86,19 +117,14 @@ export default {
|
|||
}
|
||||
return false;
|
||||
},
|
||||
isPrivate() {
|
||||
return this.data.private;
|
||||
},
|
||||
toolTipMessage() {
|
||||
return this.data.private
|
||||
? { content: this.$t('CONVERSATION.VISIBLE_TO_AGENTS'), classes: 'top' }
|
||||
: false;
|
||||
},
|
||||
sentByMessage() {
|
||||
return this.data.message_type === 1 &&
|
||||
!this.isHovered &&
|
||||
this.data.sender !== undefined
|
||||
? { content: `Sent by: ${this.data.sender.name}`, classes: 'top' }
|
||||
const { sender } = this;
|
||||
|
||||
return this.data.message_type === 1 && !this.isHovered && sender
|
||||
? {
|
||||
content: `Sent by: ${sender.available_name || sender.name}`,
|
||||
classes: 'top',
|
||||
}
|
||||
: false;
|
||||
},
|
||||
wrapClass() {
|
||||
|
@ -110,7 +136,7 @@ export default {
|
|||
bubbleClass() {
|
||||
return {
|
||||
bubble: this.isBubble,
|
||||
'is-private': this.isPrivate,
|
||||
'is-private': this.data.private,
|
||||
'is-image': this.hasImageAttachment,
|
||||
};
|
||||
},
|
||||
|
@ -120,17 +146,25 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/variables.scss';
|
||||
.wrap {
|
||||
.is-image {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
<style lang="scss">
|
||||
.wrap > .is-image.bubble {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.image {
|
||||
max-width: 32rem;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sender--info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--space-smaller) 0;
|
||||
|
||||
.sender--available-name {
|
||||
font-size: var(--font-size-mini);
|
||||
margin-left: var(--space-smaller);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,9 +5,38 @@
|
|||
:is-contact-panel-open="isContactPanelOpen"
|
||||
@contactPanelToggle="onToggleContactPanel"
|
||||
/>
|
||||
<div v-if="!currentChat.can_reply" class="banner messenger-policy--banner">
|
||||
<span>
|
||||
{{ $t('CONVERSATION.CANNOT_REPLY') }}
|
||||
<a
|
||||
href="https://developers.facebook.com/docs/messenger-platform/policy/policy-overview/"
|
||||
rel="noopener noreferrer nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t('CONVERSATION.24_HOURS_WINDOW') }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="isATweet" class="banner">
|
||||
<span v-if="!selectedTweetId">
|
||||
{{ $t('CONVERSATION.LAST_INCOMING_TWEET') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('CONVERSATION.REPLYING_TO') }}
|
||||
{{ selectedTweet }}
|
||||
</span>
|
||||
<button
|
||||
v-if="selectedTweetId"
|
||||
class="banner-close-button"
|
||||
@click="removeTweetSelection"
|
||||
>
|
||||
<i v-tooltip="$t('CONVERSATION.REMOVE_SELECTION')" class="ion-close" />
|
||||
</button>
|
||||
</div>
|
||||
<ul class="conversation-panel">
|
||||
<transition name="slide-up">
|
||||
<li>
|
||||
<li class="spinner--container">
|
||||
<span v-if="shouldShowSpinner" class="spinner message" />
|
||||
</li>
|
||||
</transition>
|
||||
|
@ -15,6 +44,7 @@
|
|||
v-for="message in getReadMessages"
|
||||
:key="message.id"
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
/>
|
||||
<li v-show="getUnreadCount != 0" class="unread--toast">
|
||||
<span>
|
||||
|
@ -25,6 +55,7 @@
|
|||
v-for="message in getUnReadMessages"
|
||||
:key="message.id"
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
/>
|
||||
</ul>
|
||||
<div class="conversation-footer">
|
||||
|
@ -40,14 +71,14 @@
|
|||
</div>
|
||||
<ReplyBox
|
||||
:conversation-id="currentChat.id"
|
||||
@scrollToMessage="focusLastMessage"
|
||||
:in-reply-to="selectedTweetId"
|
||||
@scrollToMessage="scrollToBottom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* global bus */
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import ConversationHeader from './ConversationHeader';
|
||||
|
@ -55,6 +86,7 @@ import ReplyBox from './ReplyBox';
|
|||
import Message from './Message';
|
||||
import conversationMixin from '../../../mixins/conversations';
|
||||
import { getTypingUsersText } from '../../../helper/commons';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -81,6 +113,7 @@ export default {
|
|||
isLoadingPrevious: true,
|
||||
heightBeforeLoad: null,
|
||||
conversationPanel: null,
|
||||
selectedTweetId: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -139,39 +172,82 @@ export default {
|
|||
shouldLoadMoreChats() {
|
||||
return !this.listLoadingStatus && !this.isLoadingPrevious;
|
||||
},
|
||||
|
||||
conversationType() {
|
||||
const { additional_attributes: additionalAttributes } = this.currentChat;
|
||||
const type = additionalAttributes ? additionalAttributes.type : '';
|
||||
return type || '';
|
||||
},
|
||||
|
||||
isATweet() {
|
||||
return this.conversationType === 'tweet';
|
||||
},
|
||||
|
||||
selectedTweet() {
|
||||
if (this.selectedTweetId) {
|
||||
const { messages = [] } = this.getMessages;
|
||||
const [selectedMessage = {}] = messages.filter(
|
||||
message => message.id === this.selectedTweetId
|
||||
);
|
||||
return selectedMessage.content || '';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
currentChat(newChat, oldChat) {
|
||||
if (newChat.id === oldChat.id) {
|
||||
return;
|
||||
}
|
||||
this.selectedTweetId = null;
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
bus.$on('scrollToMessage', () => {
|
||||
this.focusLastMessage();
|
||||
setTimeout(() => this.scrollToBottom(), 0);
|
||||
this.makeMessagesRead();
|
||||
});
|
||||
|
||||
bus.$on(BUS_EVENTS.SET_TWEET_REPLY, selectedTweetId => {
|
||||
this.selectedTweetId = selectedTweetId;
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
focusLastMessage() {
|
||||
setTimeout(() => {
|
||||
this.attachListner();
|
||||
}, 0);
|
||||
},
|
||||
mounted() {
|
||||
this.addScrollListener();
|
||||
},
|
||||
|
||||
unmounted() {
|
||||
this.removeScrollListener();
|
||||
},
|
||||
|
||||
methods: {
|
||||
addScrollListener() {
|
||||
this.conversationPanel = this.$el.querySelector('.conversation-panel');
|
||||
this.setScrollParams();
|
||||
this.conversationPanel.addEventListener('scroll', this.handleScroll);
|
||||
this.scrollToBottom();
|
||||
this.isLoadingPrevious = false;
|
||||
},
|
||||
removeScrollListener() {
|
||||
this.conversationPanel.removeEventListener('scroll', this.handleScroll);
|
||||
},
|
||||
scrollToBottom() {
|
||||
this.conversationPanel.scrollTop = this.conversationPanel.scrollHeight;
|
||||
},
|
||||
onToggleContactPanel() {
|
||||
this.$emit('contactPanelToggle');
|
||||
},
|
||||
|
||||
attachListner() {
|
||||
this.conversationPanel = this.$el.querySelector('.conversation-panel');
|
||||
this.heightBeforeLoad =
|
||||
this.getUnreadCount === 0
|
||||
? this.conversationPanel.scrollHeight
|
||||
: this.$el.querySelector('.conversation-panel .unread--toast')
|
||||
.offsetTop - 56;
|
||||
this.conversationPanel.scrollTop = this.heightBeforeLoad;
|
||||
this.conversationPanel.addEventListener('scroll', this.handleScroll);
|
||||
this.isLoadingPrevious = false;
|
||||
setScrollParams() {
|
||||
this.heightBeforeLoad = this.conversationPanel.scrollHeight;
|
||||
this.scrollTopBeforeLoad = this.conversationPanel.scrollTop;
|
||||
},
|
||||
|
||||
handleScroll(e) {
|
||||
this.setScrollParams();
|
||||
|
||||
const dataFetchCheck =
|
||||
this.getMessages.dataFetched === true && this.shouldLoadMoreChats;
|
||||
if (
|
||||
|
@ -186,27 +262,53 @@ export default {
|
|||
before: this.getMessages.messages[0].id,
|
||||
})
|
||||
.then(() => {
|
||||
const heightDifference =
|
||||
this.conversationPanel.scrollHeight - this.heightBeforeLoad;
|
||||
this.conversationPanel.scrollTop =
|
||||
this.conversationPanel.scrollHeight -
|
||||
(this.heightBeforeLoad - this.conversationPanel.scrollTop);
|
||||
this.scrollTopBeforeLoad + heightDifference;
|
||||
this.isLoadingPrevious = false;
|
||||
this.heightBeforeLoad =
|
||||
this.getUnreadCount === 0
|
||||
? this.conversationPanel.scrollHeight
|
||||
: this.$el.querySelector('.conversation-panel .unread--toast')
|
||||
.offsetTop - 56;
|
||||
this.setScrollParams();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
makeMessagesRead() {
|
||||
if (this.getUnreadCount !== 0 && this.getMessages !== undefined) {
|
||||
this.$store.dispatch('markMessagesRead', {
|
||||
id: this.currentChat.id,
|
||||
lastSeen: this.getMessages.messages.last().created_at,
|
||||
});
|
||||
}
|
||||
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
|
||||
},
|
||||
removeTweetSelection() {
|
||||
this.selectedTweetId = null;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.banner {
|
||||
background: var(--b-500);
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-mini);
|
||||
padding: var(--space-slab) var(--space-normal);
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-mini);
|
||||
}
|
||||
|
||||
&.messenger-policy--banner {
|
||||
background: var(--r-400);
|
||||
}
|
||||
|
||||
.banner-close-button {
|
||||
cursor: pointer;
|
||||
margin-left: var(--space--two);
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
|
||||
.spinner--container {
|
||||
min-height: var(--space-jumbo);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
ref="messageInput"
|
||||
v-model="message"
|
||||
class="input"
|
||||
:placeholder="$t(messagePlaceHolder())"
|
||||
:placeholder="messagePlaceHolder"
|
||||
:min-height="4"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
|
@ -25,46 +25,43 @@
|
|||
<file-upload
|
||||
v-if="showFileUpload"
|
||||
:size="4096 * 4096"
|
||||
accept="jpg,jpeg,png,mp3,ogg,amr,pdf,mp4"
|
||||
accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg"
|
||||
@input-file="onFileUpload"
|
||||
>
|
||||
<i
|
||||
v-if="!isUploading.image"
|
||||
class="icon ion-android-attach attachment"
|
||||
/>
|
||||
<woot-spinner v-if="isUploading.image" />
|
||||
<i v-if="!isUploading" class="icon ion-android-attach attachment" />
|
||||
<woot-spinner v-if="isUploading" />
|
||||
</file-upload>
|
||||
<i
|
||||
class="icon ion-happy-outline"
|
||||
:class="{ active: showEmojiPicker }"
|
||||
@click="toggleEmojiPicker()"
|
||||
@click="toggleEmojiPicker"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="reply-box__bottom">
|
||||
<ul class="tabs">
|
||||
<li class="tabs-title" :class="{ 'is-active': !isPrivate }">
|
||||
<a href="#" @click="makeReply">{{
|
||||
<a href="#" @click="setReplyMode">{{
|
||||
$t('CONVERSATION.REPLYBOX.REPLY')
|
||||
}}</a>
|
||||
</li>
|
||||
<li class="tabs-title is-private" :class="{ 'is-active': isPrivate }">
|
||||
<a href="#" @click="makePrivate">{{
|
||||
$t('CONVERSATION.REPLYBOX.PRIVATE_NOTE')
|
||||
}}</a>
|
||||
<a href="#" @click="setPrivateReplyMode">
|
||||
{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="message.length" class="tabs-title message-length">
|
||||
<a :class="{ 'message-error': message.length > maxLength - 40 }">
|
||||
{{ message.length }} / {{ maxLength }}
|
||||
<a :class="{ 'message-error': isMessageLengthReachingThreshold }">
|
||||
{{ characterCountIndicator }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
type="button"
|
||||
class="button send-button"
|
||||
:disabled="disableButton()"
|
||||
:disabled="isReplyButtonDisabled"
|
||||
:class="{
|
||||
disabled: message.length === 0 || message.length > maxLength,
|
||||
disabled: isReplyButtonDisabled,
|
||||
warning: isPrivate,
|
||||
}"
|
||||
@click="sendMessage"
|
||||
|
@ -93,6 +90,13 @@ import FileUpload from 'vue-upload-component';
|
|||
import EmojiInput from '../emoji/EmojiInput';
|
||||
import CannedResponse from './CannedResponse';
|
||||
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
||||
import {
|
||||
isEscape,
|
||||
isEnter,
|
||||
hasPressedShift,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -101,27 +105,55 @@ export default {
|
|||
FileUpload,
|
||||
ResizableTextArea,
|
||||
},
|
||||
mixins: [clickaway],
|
||||
mixins: [clickaway, inboxMixin],
|
||||
props: {
|
||||
inReplyTo: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
message: '',
|
||||
isPrivate: false,
|
||||
isPrivateTabActive: false,
|
||||
isFocused: false,
|
||||
showEmojiPicker: false,
|
||||
showCannedResponsesList: false,
|
||||
isUploading: {
|
||||
audio: false,
|
||||
video: false,
|
||||
image: false,
|
||||
},
|
||||
isUploading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
channelType() {
|
||||
return this.currentChat.meta.channel;
|
||||
...mapGetters({ currentChat: 'getSelectedChat' }),
|
||||
isPrivate() {
|
||||
if (this.currentChat.can_reply) {
|
||||
return this.isPrivateTabActive;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
inboxId() {
|
||||
return this.currentChat.inbox_id;
|
||||
},
|
||||
inbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
||||
},
|
||||
messagePlaceHolder() {
|
||||
return this.isPrivate
|
||||
? this.$t('CONVERSATION.FOOTER.PRIVATE_MSG_INPUT')
|
||||
: this.$t('CONVERSATION.FOOTER.MSG_INPUT');
|
||||
},
|
||||
isMessageLengthReachingThreshold() {
|
||||
return this.message.length > this.maxLength - 40;
|
||||
},
|
||||
characterCountIndicator() {
|
||||
return `${this.message.length} / ${this.maxLength}`;
|
||||
},
|
||||
isReplyButtonDisabled() {
|
||||
const isMessageEmpty = !this.message.trim().replace(/\n/g, '').length;
|
||||
return (
|
||||
isMessageEmpty ||
|
||||
this.message.length === 0 ||
|
||||
this.message.length > this.maxLength
|
||||
);
|
||||
},
|
||||
conversationType() {
|
||||
const { additional_attributes: additionalAttributes } = this.currentChat;
|
||||
|
@ -129,18 +161,29 @@ export default {
|
|||
return type || '';
|
||||
},
|
||||
maxLength() {
|
||||
if (this.channelType === 'Channel::FacebookPage') {
|
||||
return 640;
|
||||
if (this.isPrivate) {
|
||||
return MESSAGE_MAX_LENGTH.GENERAL;
|
||||
}
|
||||
if (this.channelType === 'Channel::TwitterProfile') {
|
||||
|
||||
if (this.isAFacebookInbox) {
|
||||
return MESSAGE_MAX_LENGTH.FACEBOOK;
|
||||
}
|
||||
if (this.isATwilioSMSChannel) {
|
||||
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
|
||||
}
|
||||
if (this.isATwitterInbox) {
|
||||
if (this.conversationType === 'tweet') {
|
||||
return 280;
|
||||
return MESSAGE_MAX_LENGTH.TWEET;
|
||||
}
|
||||
}
|
||||
return 10000;
|
||||
return MESSAGE_MAX_LENGTH.GENERAL;
|
||||
},
|
||||
showFileUpload() {
|
||||
return this.channelType === 'Channel::WebWidget';
|
||||
return (
|
||||
this.isAWebWidgetInbox ||
|
||||
this.isAFacebookInbox ||
|
||||
this.isATwilioWhatsappChannel
|
||||
);
|
||||
},
|
||||
replyButtonLabel() {
|
||||
if (this.isPrivate) {
|
||||
|
@ -158,20 +201,25 @@ export default {
|
|||
},
|
||||
},
|
||||
watch: {
|
||||
message(val) {
|
||||
currentChat(conversation) {
|
||||
if (conversation.can_reply) {
|
||||
this.isPrivateTabActive = false;
|
||||
} else {
|
||||
this.isPrivateTabActive = true;
|
||||
}
|
||||
},
|
||||
message(updatedMessage) {
|
||||
if (this.isPrivate) {
|
||||
return;
|
||||
}
|
||||
const isSlashCommand = val[0] === '/';
|
||||
const hasNextWord = val.includes(' ');
|
||||
const isSlashCommand = updatedMessage[0] === '/';
|
||||
const hasNextWord = updatedMessage.includes(' ');
|
||||
const isShortCodeActive = isSlashCommand && !hasNextWord;
|
||||
if (isShortCodeActive) {
|
||||
this.showCannedResponsesList = true;
|
||||
if (val.length > 1) {
|
||||
const searchKey = val.substr(1, val.length);
|
||||
this.$store.dispatch('getCannedResponse', {
|
||||
searchKey,
|
||||
});
|
||||
if (updatedMessage.length > 1) {
|
||||
const searchKey = updatedMessage.substr(1, updatedMessage.length);
|
||||
this.$store.dispatch('getCannedResponse', { searchKey });
|
||||
} else {
|
||||
this.$store.dispatch('getCannedResponse');
|
||||
}
|
||||
|
@ -188,37 +236,33 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
handleKeyEvents(e) {
|
||||
if (this.isEscape(e)) {
|
||||
if (isEscape(e)) {
|
||||
this.hideEmojiPicker();
|
||||
this.hideCannedResponse();
|
||||
} else if (this.isEnter(e)) {
|
||||
if (!e.shiftKey) {
|
||||
} else if (isEnter(e)) {
|
||||
if (!hasPressedShift(e)) {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
}
|
||||
},
|
||||
isEnter(e) {
|
||||
return e.keyCode === 13;
|
||||
},
|
||||
isEscape(e) {
|
||||
return e.keyCode === 27; // ESCAPE
|
||||
},
|
||||
async sendMessage() {
|
||||
const isMessageEmpty = !this.message.replace(/\n/g, '').length;
|
||||
if (isMessageEmpty) return;
|
||||
if (this.message.length > this.maxLength) {
|
||||
if (this.isReplyButtonDisabled) {
|
||||
return;
|
||||
}
|
||||
const newMessage = this.message;
|
||||
if (!this.showCannedResponsesList) {
|
||||
this.clearMessage();
|
||||
try {
|
||||
await this.$store.dispatch('sendMessage', {
|
||||
const messagePayload = {
|
||||
conversationId: this.currentChat.id,
|
||||
message: newMessage,
|
||||
private: this.isPrivate,
|
||||
});
|
||||
};
|
||||
if (this.inReplyTo) {
|
||||
messagePayload.contentAttributes = { in_reply_to: this.inReplyTo };
|
||||
}
|
||||
await this.$store.dispatch('sendMessage', messagePayload);
|
||||
this.$emit('scrollToMessage');
|
||||
} catch (error) {
|
||||
// Error
|
||||
|
@ -231,12 +275,12 @@ export default {
|
|||
this.message = message;
|
||||
}, 100);
|
||||
},
|
||||
makePrivate() {
|
||||
this.isPrivate = true;
|
||||
setPrivateReplyMode() {
|
||||
this.isPrivateTabActive = true;
|
||||
this.$refs.messageInput.focus();
|
||||
},
|
||||
makeReply() {
|
||||
this.isPrivate = false;
|
||||
setReplyMode() {
|
||||
this.isPrivateTabActive = false;
|
||||
this.$refs.messageInput.focus();
|
||||
},
|
||||
emojiOnClick(emoji) {
|
||||
|
@ -258,7 +302,6 @@ export default {
|
|||
hideCannedResponse() {
|
||||
this.showCannedResponsesList = false;
|
||||
},
|
||||
|
||||
onBlur() {
|
||||
this.isFocused = false;
|
||||
this.toggleTyping('off');
|
||||
|
@ -267,9 +310,8 @@ export default {
|
|||
this.isFocused = true;
|
||||
this.toggleTyping('on');
|
||||
},
|
||||
|
||||
toggleTyping(status) {
|
||||
if (this.channelType === 'Channel::WebWidget' && !this.isPrivate) {
|
||||
if (this.isAWebWidgetInbox && !this.isPrivate) {
|
||||
const conversationId = this.currentChat.id;
|
||||
this.$store.dispatch('conversationTypingStatus/toggleTyping', {
|
||||
status,
|
||||
|
@ -277,35 +319,22 @@ export default {
|
|||
});
|
||||
}
|
||||
},
|
||||
disableButton() {
|
||||
const messageHasOnlyNewLines = !this.message.replace(/\n/g, '').length;
|
||||
return (
|
||||
this.message.length === 0 ||
|
||||
this.message.length > 640 ||
|
||||
messageHasOnlyNewLines
|
||||
);
|
||||
},
|
||||
|
||||
messagePlaceHolder() {
|
||||
const placeHolder = this.isPrivate
|
||||
? 'CONVERSATION.FOOTER.PRIVATE_MSG_INPUT'
|
||||
: 'CONVERSATION.FOOTER.MSG_INPUT';
|
||||
return placeHolder;
|
||||
},
|
||||
|
||||
onFileUpload(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
this.isUploading.image = true;
|
||||
this.isUploading = true;
|
||||
this.$store
|
||||
.dispatch('sendAttachment', [this.currentChat.id, { file: file.file }])
|
||||
.dispatch('sendAttachment', [
|
||||
this.currentChat.id,
|
||||
{ file: file.file, isPrivate: this.isPrivate },
|
||||
])
|
||||
.then(() => {
|
||||
this.isUploading.image = false;
|
||||
this.isUploading = false;
|
||||
this.$emit('scrollToMessage');
|
||||
})
|
||||
.catch(() => {
|
||||
this.isUploading.image = false;
|
||||
this.isUploading = false;
|
||||
this.$emit('scrollToMessage');
|
||||
});
|
||||
},
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
<template>
|
||||
<div class="message-text--metadata">
|
||||
<span class="time">{{ readableTime }}</span>
|
||||
<i
|
||||
v-if="isEmail"
|
||||
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
|
||||
class="ion ion-android-mail"
|
||||
/>
|
||||
<i
|
||||
v-if="isPrivate"
|
||||
v-tooltip.top-start="$t('CONVERSATION.VISIBLE_TO_AGENTS')"
|
||||
class="icon ion-android-lock"
|
||||
@mouseenter="isHovered = true"
|
||||
@mouseleave="isHovered = false"
|
||||
/>
|
||||
<i
|
||||
v-if="isATweet && isIncoming"
|
||||
v-tooltip.top-start="$t('CHAT_LIST.REPLY_TO_TWEET')"
|
||||
class="icon ion-reply cursor-pointer"
|
||||
@click="onTweetReply"
|
||||
/>
|
||||
<a :href="linkToTweet" target="_blank" rel="noopener noreferrer nofollow">
|
||||
<i
|
||||
v-if="isATweet && isIncoming"
|
||||
v-tooltip.top-start="$t('CHAT_LIST.VIEW_TWEET_IN_TWITTER')"
|
||||
class="icon ion-android-open cursor-pointer"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { MESSAGE_TYPE } from 'shared/constants/messageTypes';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
sender: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
readableTime: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isEmail: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isPrivate: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
isATweet: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
messageType: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
sourceId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isIncoming() {
|
||||
return MESSAGE_TYPE.INCOMING === this.messageType;
|
||||
},
|
||||
screenName() {
|
||||
const { additional_attributes: additionalAttributes = {} } =
|
||||
this.sender || {};
|
||||
return additionalAttributes?.screen_name || '';
|
||||
},
|
||||
linkToTweet() {
|
||||
const { screenName, sourceId } = this;
|
||||
return `https://twitter.com/${screenName}/status/${sourceId}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTweetReply() {
|
||||
bus.$emit(BUS_EVENTS.SET_TWEET_REPLY, this.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.right {
|
||||
.message-text--metadata {
|
||||
.time {
|
||||
color: var(--w-100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
.message-text--metadata {
|
||||
.time {
|
||||
color: var(--s-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-text--metadata {
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
|
||||
.time {
|
||||
margin-right: var(--space-small);
|
||||
display: block;
|
||||
font-size: var(--font-size-micro);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
i {
|
||||
line-height: 1.4;
|
||||
padding-right: var(--space-small);
|
||||
padding-left: var(--space-small);
|
||||
color: var(--s-900);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--s-900);
|
||||
}
|
||||
}
|
||||
|
||||
.activity-wrap {
|
||||
.message-text--metadata {
|
||||
display: inline-block;
|
||||
|
||||
.time {
|
||||
color: var(--s-300);
|
||||
font-size: var(--font-size-micro);
|
||||
margin-left: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-image {
|
||||
.message-text--metadata {
|
||||
.time {
|
||||
bottom: var(--space-smaller);
|
||||
color: var(--white);
|
||||
position: absolute;
|
||||
right: var(--space-small);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-private {
|
||||
.message-text--metadata {
|
||||
align-items: flex-end;
|
||||
|
||||
.time {
|
||||
color: var(--s-400);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-image {
|
||||
.time {
|
||||
position: inherit;
|
||||
padding-left: var(--space-one);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,41 +0,0 @@
|
|||
<template>
|
||||
<div class="audio message-text__wrap">
|
||||
<a-player
|
||||
:music="playerOptions"
|
||||
mode="order"
|
||||
/>
|
||||
<span class="time">{{readableTime}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import APlayer from 'vue-aplayer';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
APlayer,
|
||||
},
|
||||
props: [
|
||||
'url',
|
||||
'readableTime',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
musicObj: {
|
||||
title: ' ',
|
||||
author: ' ',
|
||||
autoplay: false,
|
||||
narrow: true,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
playerOptions() {
|
||||
return {
|
||||
...this.musicObj,
|
||||
url: this.url,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -16,13 +16,17 @@
|
|||
{{ $t('CONVERSATION.DOWNLOAD') }}
|
||||
</a>
|
||||
</div>
|
||||
<span class="time">{{ readableTime }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['url', 'readableTime'],
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fileName() {
|
||||
const filename = this.url.substring(this.url.lastIndexOf('/') + 1);
|
||||
|
@ -31,7 +35,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
openLink() {
|
||||
const win = window.open(this.url, '_blank');
|
||||
const win = window.open(this.url, '_blank', 'noopener');
|
||||
win.focus();
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,25 +1,20 @@
|
|||
<template>
|
||||
<div class="image message-text__wrap">
|
||||
<img
|
||||
:src="url"
|
||||
v-on:click="onClick"
|
||||
/>
|
||||
<span class="time">{{readableTime}}</span>
|
||||
<img :src="url" @click="onClick" />
|
||||
<woot-modal :show.sync="show" :on-close="onClose">
|
||||
<img
|
||||
:src="url"
|
||||
class="modal-image"
|
||||
/>
|
||||
<img :src="url" class="modal-image" />
|
||||
</woot-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'url',
|
||||
'readableTime',
|
||||
],
|
||||
props: {
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
<template>
|
||||
<div class="map message-text__wrap">
|
||||
<img
|
||||
:src="locUrl"
|
||||
/>
|
||||
<span class="locname">{{label || ' '}}</span>
|
||||
<span class="time">{{readableTime}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: [
|
||||
'lat',
|
||||
'lng',
|
||||
'label',
|
||||
'readableTime',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
accessToken: 'pk.eyJ1IjoiY2hhdHdvb3QiLCJhIjoiY2oyazVsM3d0MDBmYjJxbmkyYXlwY3hzZyJ9.uWUdfItb0sSZQ4nfwlmuPg',
|
||||
zoomLevel: 14,
|
||||
mapType: 'mapbox.streets',
|
||||
apiEndPoint: 'https://api.mapbox.com/v4/',
|
||||
h: 100,
|
||||
w: 150,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
locUrl() {
|
||||
const { apiEndPoint, mapType, lat, lng, zoomLevel, h, w, accessToken } = this;
|
||||
return `${apiEndPoint}${mapType}/${lng},${lat},${zoomLevel}/${w}x${h}.png?access_token=${accessToken}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,12 +1,24 @@
|
|||
<template>
|
||||
<span class="message-text__wrap">
|
||||
<span v-html="message"></span>
|
||||
<span class="time">{{ readableTime }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['message', 'readableTime'],
|
||||
props: {
|
||||
message: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
readableTime: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isEmail: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -12,5 +12,6 @@ export default {
|
|||
STATUS_TYPE: {
|
||||
OPEN: 'open',
|
||||
RESOLVED: 'resolved',
|
||||
BOT: 'bot',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import AuthAPI from '../api/auth';
|
||||
import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector';
|
||||
/* global bus */
|
||||
|
||||
class ActionCableConnector extends BaseActionCableConnector {
|
||||
constructor(app, pubsubToken) {
|
||||
|
|
|
@ -9,6 +9,7 @@ import ml from './locale/ml';
|
|||
import pt from './locale/pt';
|
||||
import pt_BR from './locale/pt_BR';
|
||||
import ro from './locale/ro';
|
||||
import fa from './locale/fa';
|
||||
import ta from './locale/ta';
|
||||
import it from './locale/it';
|
||||
|
||||
|
@ -24,6 +25,7 @@ export default {
|
|||
pt_BR,
|
||||
pt,
|
||||
ro,
|
||||
fa,
|
||||
ta,
|
||||
it,
|
||||
};
|
||||
|
|
|
@ -52,15 +52,15 @@
|
|||
"LABEL": "Διαθεσιμότητα",
|
||||
"STATUSES_LIST": [
|
||||
{
|
||||
"value": "στη γραμμή",
|
||||
"value": "online",
|
||||
"label": "Στην Γραμμή"
|
||||
},
|
||||
{
|
||||
"value": "απασχολημένος",
|
||||
"value": "busy",
|
||||
"label": "Απασχολημένος"
|
||||
},
|
||||
{
|
||||
"value": "εκτός",
|
||||
"value": "offline",
|
||||
"label": "Εκτός"
|
||||
}
|
||||
]
|
||||
|
|
|
@ -10,7 +10,8 @@
|
|||
"SEARCH": {
|
||||
"INPUT": "Search for People, Chats, Saved Replies .."
|
||||
},
|
||||
"STATUS_TABS": [{
|
||||
"STATUS_TABS": [
|
||||
{
|
||||
"NAME": "Open",
|
||||
"KEY": "openCount"
|
||||
},
|
||||
|
@ -19,8 +20,8 @@
|
|||
"KEY": "allConvCount"
|
||||
}
|
||||
],
|
||||
|
||||
"ASSIGNEE_TYPE_TABS": [{
|
||||
"ASSIGNEE_TYPE_TABS": [
|
||||
{
|
||||
"NAME": "Mine",
|
||||
"KEY": "me",
|
||||
"COUNT_KEY": "mineCount"
|
||||
|
@ -36,17 +37,20 @@
|
|||
"COUNT_KEY": "allCount"
|
||||
}
|
||||
],
|
||||
|
||||
"CHAT_STATUS_ITEMS": [{
|
||||
"CHAT_STATUS_ITEMS": [
|
||||
{
|
||||
"TEXT": "Open",
|
||||
"VALUE": "open"
|
||||
},
|
||||
{
|
||||
"TEXT": "Resolved",
|
||||
"VALUE": "resolved"
|
||||
},
|
||||
{
|
||||
"TEXT": "Bot",
|
||||
"VALUE": "bot"
|
||||
}
|
||||
],
|
||||
|
||||
"ATTACHMENTS": {
|
||||
"image": {
|
||||
"ICON": "ion-image",
|
||||
|
@ -72,6 +76,9 @@
|
|||
"ICON": "ion-link",
|
||||
"CONTENT": "has shared a url"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RECEIVED_VIA_EMAIL": "Received via email",
|
||||
"VIEW_TWEET_IN_TWITTER": "View tweet in Twitter",
|
||||
"REPLY_TO_TWEET": "Reply to this tweet"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,11 @@
|
|||
"CLICK_HERE": "Click here",
|
||||
"LOADING_INBOXES": "Loading inboxes",
|
||||
"LOADING_CONVERSATIONS": "Loading Conversations",
|
||||
"CANNOT_REPLY": "You cannot reply due to",
|
||||
"24_HOURS_WINDOW": "24 hour message window restriction",
|
||||
"LAST_INCOMING_TWEET": "You are replying to the last incoming tweet",
|
||||
"REPLYING_TO": "You are replying to:",
|
||||
"REMOVE_SELECTION": "Remove Selection",
|
||||
"DOWNLOAD": "Download",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Resolve",
|
||||
|
|
|
@ -24,8 +24,8 @@
|
|||
"ERROR": ""
|
||||
},
|
||||
"DOMAIN": {
|
||||
"LABEL": "Domain",
|
||||
"PLACEHOLDER": "Your website domain",
|
||||
"LABEL": "Incoming Email Domain",
|
||||
"PLACEHOLDER": "The domain where you will receive the emails",
|
||||
"ERROR": ""
|
||||
},
|
||||
"SUPPORT_EMAIL": {
|
||||
|
@ -33,14 +33,9 @@
|
|||
"PLACEHOLDER": "Your company's support email",
|
||||
"ERROR": ""
|
||||
},
|
||||
"ENABLE_DOMAIN_EMAIL": {
|
||||
"LABEL": "Enable domain email",
|
||||
"PLACEHOLDER": "Enable the custom domain email",
|
||||
"ERROR": "",
|
||||
"OPTIONS": {
|
||||
"ENABLED": "Enabled",
|
||||
"DISABLED": "Disabled"
|
||||
}
|
||||
"FEATURES": {
|
||||
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",
|
||||
"CUSTOM_EMAIL_DOMAIN_ENABLED": "You can receive emails in your custom domain now."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -115,6 +115,43 @@
|
|||
"ERROR_MESSAGE": "We were not able to authenticate Twilio credentials, please try again"
|
||||
}
|
||||
},
|
||||
"API_CHANNEL": {
|
||||
"TITLE": "API Channel",
|
||||
"DESC": "Integrate with API channel and start supporting your customers.",
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "Channel Name",
|
||||
"PLACEHOLDER": "Please enter a channel name",
|
||||
"ERROR": "This field is required"
|
||||
},
|
||||
"WEBHOOK_URL": {
|
||||
"LABEL": "Webhook URL",
|
||||
"SUBTITLE": "Configure the URL where you want to recieve callbacks on events.",
|
||||
"PLACEHOLDER": "Webhook URL"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create API Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the api channel"
|
||||
}
|
||||
},
|
||||
"EMAIL_CHANNEL": {
|
||||
"TITLE": "Email Channel",
|
||||
"DESC": "Integrate you email inbox.",
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "Channel Name",
|
||||
"PLACEHOLDER": "Please enter a channel name",
|
||||
"ERROR": "This field is required"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Email",
|
||||
"SUBTITLE": "Email where your customers sends you support tickets",
|
||||
"PLACEHOLDER": "Email"
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create Email Channel",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the email channel"
|
||||
},
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
||||
},
|
||||
"AUTH": {
|
||||
"TITLE": "Channels",
|
||||
"DESC": "Currently we support Website live chat widgets, Facebook Pages and Twitter profiles as platforms. We have more platforms like Whatsapp, Email, Telegram and Line in the works, which will be out soon."
|
||||
|
@ -175,7 +212,17 @@
|
|||
"ERROR_MESSAGE": "Could not delete inbox. Please try again later."
|
||||
}
|
||||
},
|
||||
"TABS": {
|
||||
"SETTINGS": "Settings",
|
||||
"COLLABORATORS": "Collaborators",
|
||||
"CONFIGURATION": "Configuration"
|
||||
},
|
||||
"SETTINGS": "Settings",
|
||||
"FEATURES": {
|
||||
"LABEL": "Features",
|
||||
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
|
||||
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget"
|
||||
},
|
||||
"SETTINGS_POPUP": {
|
||||
"MESSENGER_HEADING": "Messenger Script",
|
||||
"MESSENGER_SUB_HEAD": "Place this button inside your body tag",
|
||||
|
|
|
@ -44,9 +44,14 @@
|
|||
"LABEL": "Profile Image"
|
||||
},
|
||||
"NAME": {
|
||||
"LABEL": "Your name",
|
||||
"ERROR": "Please enter a valid name",
|
||||
"PLACEHOLDER": "Please enter your name, this would be displayed in conversations"
|
||||
"LABEL": "Your full name",
|
||||
"ERROR": "Please enter a valid full name",
|
||||
"PLACEHOLDER": "Please enter your full name"
|
||||
},
|
||||
"DISPLAY_NAME": {
|
||||
"LABEL": "Display name",
|
||||
"ERROR": "Please enter a valid display name",
|
||||
"PLACEHOLDER": "Please enter a display name, this would be displayed in conversations"
|
||||
},
|
||||
"AVAILABILITY": {
|
||||
"LABEL": "Availability",
|
||||
|
@ -122,5 +127,22 @@
|
|||
"INTEGRATIONS": "Integrations",
|
||||
"ACCOUNT_SETTINGS": "Account Settings",
|
||||
"LABELS": "Labels"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "New Account",
|
||||
"SELECTOR_SUBTITLE": "Create a new account",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Account created successfully",
|
||||
"EXIST_MESSAGE": "Account already exists",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
},
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Account Name",
|
||||
"PLACEHOLDER": "Wayne Enterprises"
|
||||
},
|
||||
"SUBMIT": "Submit"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
102
app/javascript/dashboard/i18n/locale/fa/agentMgmt.json
Normal file
102
app/javascript/dashboard/i18n/locale/fa/agentMgmt.json
Normal file
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
"AGENT_MGMT": {
|
||||
"HEADER": "اپراتورها",
|
||||
"HEADER_BTN_TXT": "اضافه کردن اپراتور",
|
||||
"LOADING": "دریافت لیست اپراتورها",
|
||||
"SIDEBAR_TXT": "<p><b>اپراتورها</b></p> <p> یک <b>اپراتور</b> یکی از اعضای تیم پشتیبانی است. </p><p> اپراتورها میتوانند پیامهای کاربران را ببینند و به آنها پاسخ بدهند. این لیست حاوی تمام اپراتورهایی است که در حساب شما تعریف شده اند. </p><p> با زدن روی دکمه <b>اضافه کردن اپراتور</b> میتوانید یک اپراتور جدید معرفی کنید. به ایمیل اپراتوری که معرفی میکنید یک دعوتنامه ارسال میشود که بعد از پذیرفتن آن اپراتور میتواند به پیامهای کاربران پاسخ بدهد. </p><p> بسته به سطح دسترسی تعیین شده یک اپراتور میتواند به بخشهای مشخصی از اکانت دسترسی پیدا کند </p><p> <b>اپراتور</b> - اپراتورهایی که این نقش را داشته باشند تنها میتوانند به صندوقهای ورودی، گزارشات و گفتگوها دسترسی داشته باشند. آنها میتوانند یک مکالمه را به اپراتور دیگر یا خودشان تخصیص دهند و یا یک مکالمه را حل شده اعلام کنند.</p><p> <b>مدیر</b> - مدیران میتوانند علاوه بر تمام بخشهایی که یک اپراتور دسترسی دارد، به تمام بخشهایی که در حساب کاربری شما وجود دارد دسترسی داشته باشند.</p>",
|
||||
"AGENT_TYPES": [
|
||||
{
|
||||
"name": "administrator",
|
||||
"label": "مدیر"
|
||||
},
|
||||
{
|
||||
"name": "agent",
|
||||
"label": "اپراتور"
|
||||
}
|
||||
],
|
||||
"LIST": {
|
||||
"404": "در حال حاضر هیچ اپراتوری برای این حساب معرفی نشده است.",
|
||||
"TITLE": "مدیریت اپراتورها",
|
||||
"DESC": "میتوانید به تیمتان اپراتور اضافه کرده یا اپراتورهای فعلی را حذف کنید",
|
||||
"NAME": "نام",
|
||||
"EMAIL": "ایمیل",
|
||||
"STATUS": "وضعیت",
|
||||
"ACTIONS": "عملیات",
|
||||
"VERIFIED": "تایید شده",
|
||||
"VERIFICATION_PENDING": "در انتظار تایید"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "اضافه کردن اپراتور به تیم",
|
||||
"DESC": "میتوانید افرادی را معرفی کنید که مسئول پشتیبانی آنلاین صندوقهای ورودی باشند",
|
||||
"CANCEL_BUTTON_TEXT": "انصراف",
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "اسم اپراتور",
|
||||
"PLACEHOLDER": "لطفا اسم اپراتور را وارد نمایید"
|
||||
},
|
||||
"AGENT_TYPE": {
|
||||
"LABEL": "نوع اپراتور",
|
||||
"PLACEHOLDER": "لطفا نوع دسترسی اپراتور را مشخص کنید",
|
||||
"ERROR": "تعیین کردن نوع اپراتور الزامی است"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "ایمیل",
|
||||
"PLACEHOLDER": "لطفا آدرس ایمیل اپراتور را وارد نمایید"
|
||||
},
|
||||
"SUBMIT": "اضافه کردن اپراتور"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "اپراتور معرفی شد",
|
||||
"EXIST_MESSAGE": "این اپراتور قبلا معرفی شده، لطفا ایمیل دیگری را امتحان کنید",
|
||||
"ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "حذف",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "اپراتور حذف شد",
|
||||
"ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "تاییدیه حذف",
|
||||
"MESSAGE": "مطمئن هستید که حذف شود ",
|
||||
"YES": "بله، حذف شود ",
|
||||
"NO": "نه، بماند "
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "تغییر مشخصات اپراتور",
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "اسم اپراتور",
|
||||
"PLACEHOLDER": "لطفا اسم اپراتور را وارد کنید"
|
||||
},
|
||||
"AGENT_TYPE": {
|
||||
"LABEL": "نوع اپراتور",
|
||||
"PLACEHOLDER": "لطفا نوع اپراتور را انتخاب کنید",
|
||||
"ERROR": "تعیین کردن نوع اپراتور الزامی است"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "ایمیل",
|
||||
"PLACEHOLDER": "لطفا ایمیل اپراتور را وارد کنید"
|
||||
},
|
||||
"SUBMIT": "تغییر اپراتور"
|
||||
},
|
||||
"BUTTON_TEXT": "ویرایش",
|
||||
"CANCEL_BUTTON_TEXT": "انصراف",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "اطلاعات اپراتور تغییر یافت",
|
||||
"ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید"
|
||||
},
|
||||
"PASSWORD_RESET": {
|
||||
"ADMIN_RESET_BUTTON": "تغییر رمز عبور",
|
||||
"ADMIN_SUCCESS_MESSAGE": "یک ایمیل حاوی روش تغییر دادن رمز عبور برای اپراتور ارسال شد",
|
||||
"SUCCESS_MESSAGE": "تغییر رمز عبور اپراتور با موفقیت انجام شد",
|
||||
"ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید"
|
||||
}
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": "اپراتوری یافت نشد."
|
||||
}
|
||||
}
|
||||
}
|
76
app/javascript/dashboard/i18n/locale/fa/cannedMgmt.json
Normal file
76
app/javascript/dashboard/i18n/locale/fa/cannedMgmt.json
Normal file
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"CANNED_MGMT": {
|
||||
"HEADER": "پاسخهای آماده",
|
||||
"HEADER_BTN_TXT": "اضافه کردن پاسخ آماده",
|
||||
"LOADING": "دریافت پاسخهای آماده",
|
||||
"SEARCH_404": "هیچ آیتمی با این مشخصات یافت نشد",
|
||||
"SIDEBAR_TXT": "<p><b>پاسخهای آماده</b> </p><p> پاسخهای آماده قالبهایی متنی هستند که برای جواب دادن سریع به یک گفتگو به کار میآیند. </p><p> برای ساختن یک جواب آماده، روی دکمه <b>اضافه کردن جواب آماده</b> کلیک کنید. همچنین با زدن روی دکمه «تغییر» یا «حذف» میتوانید یک پاسخ آماده را تغییر داده یا حذف کنید. </p><p> پاسخهای آماده با استفاده و با کمک <b>کدهای کوتاه</b> ساخته شدهاند. اپراتورها با زدن کلید <b>'/'</b> از صفحه کلید میتوانند به لیست پاسخهای آماده دسترسی پیدا کنند. </p>",
|
||||
"LIST": {
|
||||
"404": "هیچ پاسخ آمادهای برای این حساب تعریف نشده است",
|
||||
"TITLE": "مدیریت پاسخهای آماده",
|
||||
"DESC": "پاسخهای آماده قالبهای متنی پیش آمادهای هستند که برای پاسخگویی سریع به یک گفتگو میتوانند مفید واقع شوند",
|
||||
"TABLE_HEADER": [
|
||||
"کدهای کوتاه",
|
||||
"محتوا",
|
||||
"عملیات"
|
||||
]
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "اضافه کردن پاسخ آماده",
|
||||
"DESC": "پاسخهای آماده قالبهای متنی پیش آمادهای هستند که برای پاسخگویی سریع به یک گفتگو میتوانند مفید واقع شوند",
|
||||
"CANCEL_BUTTON_TEXT": "انصراف",
|
||||
"FORM": {
|
||||
"SHORT_CODE": {
|
||||
"LABEL": "کد کوتاه",
|
||||
"PLACEHOLDER": "لطفا یک کد کوتاه وارد کنید",
|
||||
"ERROR": "وجود کد کوتاه ضروری است"
|
||||
},
|
||||
"CONTENT": {
|
||||
"LABEL": "محتوا",
|
||||
"PLACEHOLDER": "لطفا محتوای این پاسخ را تایپ کنید",
|
||||
"ERROR": "محتوا ضروری است"
|
||||
},
|
||||
"SUBMIT": "ثبت"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "پاسخ آماده با موفقیت ثبت شد",
|
||||
"ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "ویرایش پاسخ آماده",
|
||||
"CANCEL_BUTTON_TEXT": "انصراف",
|
||||
"FORM": {
|
||||
"SHORT_CODE": {
|
||||
"LABEL": "کد کوتاه",
|
||||
"PLACEHOLDER": "لطفا یک کد کوتاه وارد کنید",
|
||||
"ERROR": "وجود کد کوتاه ضروری است"
|
||||
},
|
||||
"CONTENT": {
|
||||
"LABEL": "محتوا",
|
||||
"PLACEHOLDER": "لطفا محتوای این پاسخ را تایپ کنید",
|
||||
"ERROR": "محتوا ضروری است"
|
||||
},
|
||||
"SUBMIT": "ثبت"
|
||||
},
|
||||
"BUTTON_TEXT": "ویرایش",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "پاسخ آماده تغییر داده شد",
|
||||
"ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "حذف",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "پاسخ آماده با موفقیت حذف شد",
|
||||
"ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "تاییدیه حذف",
|
||||
"MESSAGE": "مطمئن هستید حذف شود؟ ",
|
||||
"YES": "بله، حذف شود ",
|
||||
"NO": "نه، بماند "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
77
app/javascript/dashboard/i18n/locale/fa/chatlist.json
Normal file
77
app/javascript/dashboard/i18n/locale/fa/chatlist.json
Normal file
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"CHAT_LIST": {
|
||||
"LOADING": "در حال دریافت گفتگوها",
|
||||
"LOAD_MORE_CONVERSATIONS": "دریافت گفتگوهای بیشتر",
|
||||
"EOF": "همه گفتگوها دریافت شدند 🎉",
|
||||
"LIST": {
|
||||
"404": "هیچ گفتگوی فعالی در این گروه نیست"
|
||||
},
|
||||
"TAB_HEADING": "گفتگوها",
|
||||
"SEARCH": {
|
||||
"INPUT": "پیدا کردن افراد، گفتگوها و پاسخهای از پیش نوشته شده..."
|
||||
},
|
||||
"STATUS_TABS": [{
|
||||
"NAME": "باز",
|
||||
"KEY": "openCount"
|
||||
},
|
||||
{
|
||||
"NAME": "حل شده",
|
||||
"KEY": "allConvCount"
|
||||
}
|
||||
],
|
||||
|
||||
"ASSIGNEE_TYPE_TABS": [{
|
||||
"NAME": "من",
|
||||
"KEY": "me",
|
||||
"COUNT_KEY": "mineCount"
|
||||
},
|
||||
{
|
||||
"NAME": "تخصیص داده نشده",
|
||||
"KEY": "unassigned",
|
||||
"COUNT_KEY": "unAssignedCount"
|
||||
},
|
||||
{
|
||||
"NAME": "همه",
|
||||
"KEY": "all",
|
||||
"COUNT_KEY": "allCount"
|
||||
}
|
||||
],
|
||||
|
||||
"CHAT_STATUS_ITEMS": [{
|
||||
"TEXT": "باز",
|
||||
"VALUE": "open"
|
||||
},
|
||||
{
|
||||
"TEXT": "حل شده",
|
||||
"VALUE": "resolved"
|
||||
}
|
||||
],
|
||||
|
||||
"ATTACHMENTS": {
|
||||
"image": {
|
||||
"ICON": "ion-image",
|
||||
"CONTENT": "پیام تصویری"
|
||||
},
|
||||
"audio": {
|
||||
"ICON": "ion-volume-high",
|
||||
"CONTENT": "پیام صوتی"
|
||||
},
|
||||
"video": {
|
||||
"ICON": "ion-ios-videocam",
|
||||
"CONTENT": "پیام ویدیویی"
|
||||
},
|
||||
"file": {
|
||||
"ICON": "ion-document",
|
||||
"CONTENT": "فایل الصاقی"
|
||||
},
|
||||
"location": {
|
||||
"ICON": "ion-ios-location",
|
||||
"CONTENT": "Location"
|
||||
},
|
||||
"fallback": {
|
||||
"ICON": "ion-link",
|
||||
"CONTENT": "یک آدرس URL به اشتراک گذاشته شده"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
app/javascript/dashboard/i18n/locale/fa/contact.json
Normal file
21
app/javascript/dashboard/i18n/locale/fa/contact.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"CONTACT_PANEL": {
|
||||
"CONVERSATION_TITLE": "جزئیات مکالمه",
|
||||
"BROWSER": "مرورگر",
|
||||
"OS": "سیستم عامل",
|
||||
"INITIATED_FROM": "شروع شده از",
|
||||
"INITIATED_AT": "شروع شده در",
|
||||
"CONVERSATIONS": {
|
||||
"NO_RECORDS_FOUND": "این اولین گفتگوی این کاربر است",
|
||||
"TITLE": "گفتگوهای قبلی"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "برچسبهای گفتگو",
|
||||
"UPDATE_BUTTON": "تغییر برچسبها",
|
||||
"UPDATE_ERROR": "برچسبها تغییری نکردند، لطفا بعدا امتحان کنید",
|
||||
"TAG_PLACEHOLDER": "برچسب جدید",
|
||||
"PLACEHOLDER": "پیدا کردن یا اضافه کردن برچسب"
|
||||
},
|
||||
"MUTE_CONTACT": "بیصدا کردن گفتگو"
|
||||
}
|
||||
}
|
35
app/javascript/dashboard/i18n/locale/fa/conversation.json
Normal file
35
app/javascript/dashboard/i18n/locale/fa/conversation.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"CONVERSATION": {
|
||||
"404": "Please select a conversation from left pane",
|
||||
"NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.",
|
||||
"NO_MESSAGE_2": " to send a message to your page!",
|
||||
"NO_INBOX_1": "Hola! Looks like you haven't added any inboxes yet.",
|
||||
"NO_INBOX_2": " to get started",
|
||||
"NO_INBOX_AGENT": "Uh Oh! Looks like you are not part of any inbox. Please contact your administrator",
|
||||
"CLICK_HERE": "Click here",
|
||||
"LOADING_INBOXES": "Loading inboxes",
|
||||
"LOADING_CONVERSATIONS": "Loading Conversations",
|
||||
"DOWNLOAD": "Download",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Resolve",
|
||||
"REOPEN_ACTION": "Reopen",
|
||||
"OPEN": "More",
|
||||
"CLOSE": "Close",
|
||||
"DETAILS": "details"
|
||||
},
|
||||
"FOOTER": {
|
||||
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
|
||||
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents"
|
||||
},
|
||||
"REPLYBOX": {
|
||||
"REPLY": "Reply",
|
||||
"PRIVATE_NOTE": "Private Note",
|
||||
"SEND": "Send",
|
||||
"CREATE": "Add Note",
|
||||
"TWEET": "Tweet"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||
"CHANGE_STATUS": "Conversation status changed",
|
||||
"CHANGE_AGENT": "Conversation Assignee changed"
|
||||
}
|
||||
}
|
46
app/javascript/dashboard/i18n/locale/fa/generalSettings.json
Normal file
46
app/javascript/dashboard/i18n/locale/fa/generalSettings.json
Normal file
|
@ -0,0 +1,46 @@
|
|||
{
|
||||
"GENERAL_SETTINGS": {
|
||||
"TITLE": "تنظیمات حساب",
|
||||
"SUBMIT": "تغییر تنظیمات",
|
||||
"UPDATE": {
|
||||
"ERROR": "تنظیمات تغییری نکرد، دوباره امتحان کنید!",
|
||||
"SUCCESS": "تنظیمات با موفقیت اعمال شد"
|
||||
},
|
||||
"FORM": {
|
||||
"ERROR": "لطفا ایرادات فرم را برطرف کنید",
|
||||
"GENERAL_SECTION": {
|
||||
"TITLE": "تنظیمات عمومی",
|
||||
"NOTE": ""
|
||||
},
|
||||
"NAME": {
|
||||
"LABEL": "عنوان حساب",
|
||||
"PLACEHOLDER": "عنوان حساب شما",
|
||||
"ERROR": "لطفا عنوان حساب را به درستی وارد نمایید"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"LABEL": "زبان سایت (آزمایشی)",
|
||||
"PLACEHOLDER": "زبان نمایش المانهای متنی سایت",
|
||||
"ERROR": ""
|
||||
},
|
||||
"DOMAIN": {
|
||||
"LABEL": "دامنه",
|
||||
"PLACEHOLDER": "دامنه سایت شما",
|
||||
"ERROR": ""
|
||||
},
|
||||
"SUPPORT_EMAIL": {
|
||||
"LABEL": "ایمیل پشتیبانی",
|
||||
"PLACEHOLDER": "ایمیل پشتیبانی شرکت شما",
|
||||
"ERROR": ""
|
||||
},
|
||||
"ENABLE_DOMAIN_EMAIL": {
|
||||
"LABEL": "فعال کردن ایمیل دامنه",
|
||||
"PLACEHOLDER": "فعال کردن ایمیل دامنههای اختصاصی",
|
||||
"ERROR": "",
|
||||
"OPTIONS": {
|
||||
"ENABLED": "فعال",
|
||||
"DISABLED": "غیرفعال"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
191
app/javascript/dashboard/i18n/locale/fa/inboxMgmt.json
Normal file
191
app/javascript/dashboard/i18n/locale/fa/inboxMgmt.json
Normal file
|
@ -0,0 +1,191 @@
|
|||
{
|
||||
"INBOX_MGMT": {
|
||||
"HEADER": "صندوقهای ورودی",
|
||||
"SIDEBAR_TXT": "<p><b>صندوق ورودی</b></p> <p> وقتی چت ووت به یک وب سایت یا یک صفحه فیس بوک متصل شود به آن <b>صندوق ورودی</b> میگوید. شما در حساب چت ووت خود میتوانید بینهایت صندوق ورودی داشته باشید. </p><p> روی دکمه <b>اضافه کردن صندوق ورودی</b> کلیک کنید تا به یک وب سایت یا یک صفحه فیس بوک وصل شوید. </p><p> در داشبورد، میتوانید گفتگوهای همه صندوقهای ورودی را یکجا ببینید و در تب «گفتگوها» به آنها پاسخ بدهید. </p><p> همچنین میتوانید با کلیک کردن روی اسم صندوق ورودی از قسمت سمت چپ، فقط گفتگوهای همان صندوق را ببینید. </p>",
|
||||
"LIST": {
|
||||
"404": "برای این حساب هیچ صندوق ورودی معرفی نشده است."
|
||||
},
|
||||
"CREATE_FLOW": [
|
||||
{
|
||||
"title": "کانال ورودی را انتخاب کنید",
|
||||
"route": "settings_inbox_new",
|
||||
"body": "جایی که قرار است امکان گفتگوی آنلاین در آنجا فراهم شود را انتخاب کنید"
|
||||
},
|
||||
{
|
||||
"title": "ساخت صندوق ورودی",
|
||||
"route": "settings_inboxes_page_channel",
|
||||
"body": "به حساب کاربری وارد شوید و صندوق ورودی بسازید."
|
||||
},
|
||||
{
|
||||
"title": "معرفی اپراتور",
|
||||
"route": "settings_inboxes_add_agents",
|
||||
"body": "اپراتورها را به صندوق ورودی ساخته شده تخصیص میدهد"
|
||||
},
|
||||
{
|
||||
"title": "ماشالله!",
|
||||
"route": "settings_inbox_finish",
|
||||
"body": "دیگه میتونی بترکونی"
|
||||
}
|
||||
],
|
||||
"ADD": {
|
||||
"FB": {
|
||||
"HELP": "پانویس: با وارد شدن ما فقط به پیامهای صفحه دسترسی پیدا میکنیم. پیامهای خصوصی شما را هرگز نخواهیم دید",
|
||||
"CHOOSE_PAGE": "انتخاب صفحه",
|
||||
"CHOOSE_PLACEHOLDER": "از لیست صفحه مورد نظر را انتخاب کنید",
|
||||
"INBOX_NAME": "عنوان صندوق ورودی",
|
||||
"ADD_NAME": "یک اسم به صندوق ورودی خود اضافه کنید",
|
||||
"PICK_NAME": "یک اسم برای صندوق ورودی خود انتخاب کنید",
|
||||
"PICK_A_VALUE": "یک مقدار انتخاب کنید"
|
||||
},
|
||||
"TWITTER": {
|
||||
"HELP": "برای اضافه کردن امکان گفتگو از صفحه پروفایل توییترتان، لازم است با زدن دکمه `ورود با توییتر` پروفایل توییتر خود را شناسایی کنید' "
|
||||
},
|
||||
"WEBSITE_CHANNEL": {
|
||||
"TITLE": "کانال وب سایت",
|
||||
"DESC": "یک کانال به وب سایت خود بسازید تا مشتریان بتوانند از طریق ویجت سایت با شما گفتگو کنند.",
|
||||
"LOADING_MESSAGE": "در حال ساخت کانال پشتیبانی آنلاین سایت",
|
||||
"CHANNEL_AVATAR": {
|
||||
"LABEL": "آواتار کانال"
|
||||
},
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "عنوان سایت",
|
||||
"PLACEHOLDER": "عنوان سایت خود را وارد کنید (به عنوان مثال: Acme Inc)"
|
||||
},
|
||||
"CHANNEL_DOMAIN": {
|
||||
"LABEL": "دامنه سایت",
|
||||
"PLACEHOLDER": "دامنه سایت خود را وارد کنید (به عنوان مثال: acme.com)"
|
||||
},
|
||||
"CHANNEL_WELCOME_TITLE": {
|
||||
"LABEL": "تیتر خوش آمدگویی",
|
||||
"PLACEHOLDER": "سلام!"
|
||||
},
|
||||
"CHANNEL_WELCOME_TAGLINE": {
|
||||
"LABEL": "زیرتیتر خوش آمدگویی",
|
||||
"PLACEHOLDER": "دسترسی به ما ساده است. هر سوالی پیش آمد همینجا از ما بپرسید."
|
||||
},
|
||||
"CHANNEL_GREETING_MESSAGE": {
|
||||
"LABEL": "پیام پاسخگویی کانال",
|
||||
"PLACEHOLDER": "شرکت ما در اسرع وقت به پیامها پاسخ میدهد"
|
||||
},
|
||||
"CHANNEL_GREETING_TOGGLE": {
|
||||
"LABEL": "فعال کردن پیام پاسخگویی",
|
||||
"HELP_TEXT": "به محض اینکه کاربر گفتگویی را شروع کرد، پیام مشخصی در جواب او ارسال میشود",
|
||||
"ENABLED": "فعال",
|
||||
"DISABLED": "غیرفعال"
|
||||
},
|
||||
"WIDGET_COLOR": {
|
||||
"LABEL": "رنگ ویجت",
|
||||
"PLACEHOLDER": "رنگی که در ویجت استفاده میشود را تعیین کنید"
|
||||
},
|
||||
"SUBMIT_BUTTON": "ساختن صندوق ورودی"
|
||||
},
|
||||
"TWILIO": {
|
||||
"TITLE": "کانال اس ام اس تولیو/واتساپ",
|
||||
"DESC": "به Twilio متصل شوید و مشتریان خود را از طریق پیامک یا واتساپ پشتیبانی کنید",
|
||||
"ACCOUNT_SID": {
|
||||
"LABEL": "شناسه SID",
|
||||
"PLACEHOLDER": "لطفا شناسه SID حساب Twilio خود را وارد کنید",
|
||||
"ERROR": "پر کردن این فیلد ضروری است"
|
||||
},
|
||||
"CHANNEL_TYPE": {
|
||||
"LABEL": "نوع کانال",
|
||||
"ERROR": "لطفا نوع کانال را انتخاب کنید"
|
||||
},
|
||||
"AUTH_TOKEN": {
|
||||
"LABEL": "Auth توکن",
|
||||
"PLACEHOLDER": "لطفا توکن Auth حساب Twilio خود را وارد کنید",
|
||||
"ERROR": "پر کردن این فیلد ضروری است"
|
||||
},
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "عنوان کانال",
|
||||
"PLACEHOLDER": "لطفا اسم یک کانال را وارد کنید",
|
||||
"ERROR": "پر کردن این فیلد ضروری است"
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"LABEL": "شماره تلفن",
|
||||
"PLACEHOLDER": "لطفا شمارهای که پیام میبایست به آن ارسال شود را وارد کنید",
|
||||
"ERROR": "لطفا شماره تلفن را به شکل صحیح وارد کنید. شماره میبایست با کاراکتر `+` شروع شود"
|
||||
},
|
||||
"API_CALLBACK": {
|
||||
"TITLE": "آدرس URL مربوط به API",
|
||||
"SUBTITLE": "لازم است آدرس Callback URL موجود در Twilio را به آنچه که در اینجا ذکر شده تنظیم کنید"
|
||||
},
|
||||
"SUBMIT_BUTTON": "ساختن کانال Twilio",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "متاسفانه Twilio اطلاعات ارائه شده را تایید نمی کند، لطفا اصلاح و مجددا تلاش کنید"
|
||||
}
|
||||
},
|
||||
"AUTH": {
|
||||
"TITLE": "کانالها",
|
||||
"DESC": "در حال حاضر ما ویجتهای پشتیبانی آنلاین سایتها، صفحات فیس بوک و پروفایلهای توییتر را پشتیبانی میکنیم. پلتفرمهای دیگری مثل واتساپ، ایمیل، تلگرام و لاین در برنامه کاری ما قرار دارد و به زودی آماده خواهند شد."
|
||||
},
|
||||
"AGENTS": {
|
||||
"TITLE": "اپراتورها",
|
||||
"DESC": "در اینجا میتوانید اپراتورها را به صندوق ورودی خود اختصاص دهید. توجه داشته باشید که فقط اپراتورهایی که در اینجا معرفی شده باشند میتوانند به پیامهای این صندوق پاسخ بدهند.دیگر اپراتورها نخواهند توانست پیامهای این صندوق را ببینید یا به آنها پاسخی بدهند. <br> <b>پانویس:</b>به عنوان مدیر اگر میخواهید به همه صندوقهای ورودی دسترسی داشته باشید میبایست خود را به عنوان اپراتور به همه صندوقها اضافه کنید."
|
||||
},
|
||||
"DETAILS": {
|
||||
"TITLE": "جزئیات صندوق ورودی",
|
||||
"DESC": "از کادر زیر صفحه فیس بوکی که میخواهید به چت ووت متصل شود انتخاب کنید. همچنین میتوانید برای تشخیص بهتر یک اسم مشخص برای این صندوق تعیین کنید."
|
||||
},
|
||||
"FINISH": {
|
||||
"TITLE": "ماشالله!",
|
||||
"DESC": "چت ووت با موفقیت به فیس بوک متصل شد. از به بعد هر مشتری که پیامی در این صفحه بنویسید، آن پیام در صندوق ورودی چت ووت ظاهر میشود و گفتگویی ایجاد میشود..<br>فراموش نکنید که ما یک ویجت برای سایتها هم داریم که با قرار دادن آن در سایت، مشتری میتواند به صورت آنلاین با شما گفتگو کند و پیامها در صندوق ورودی آن ظاهر میشود<br>باحال نیست؟ ما همیشه باحالیم! :)"
|
||||
}
|
||||
},
|
||||
"DETAILS": {
|
||||
"LOADING_FB": "در حال احراز هویت با فیس بوک...",
|
||||
"ERROR_FB_AUTH": "اشکالی پیش آمد.. لطفا دوباره سعی کنید...",
|
||||
"CREATING_CHANNEL": "در حال ساخت صندوق ورودی...",
|
||||
"TITLE": "تنظیمات صفحه ورودی",
|
||||
"DESC": ""
|
||||
},
|
||||
"AGENTS": {
|
||||
"BUTTON_TEXT": "اضافه کردن اپراتور",
|
||||
"ADD_AGENTS": "اضافه کردن اپراتور به صندوق ورودی..."
|
||||
},
|
||||
"FINISH": {
|
||||
"TITLE": "صندوق ورودی حاضره!",
|
||||
"MESSAGE": "حالا از طریق این کانال جدید میتوانید با مشتریان صحبت کنید. به امید موفقیت ",
|
||||
"BUTTON_TEXT": "نشانم بده",
|
||||
"WEBSITE_SUCCESS": "ساختن کانال وب سایت با موفقیت انجام شد. قطعه کد زیر را کپی کرده و در سایت خود قرار دهید. در صورتیکه مشتری از ویجت پشتیبانی آنلاین استفاده کند گفتگوی شما در این صندوق ورودی ظاهر میشود."
|
||||
},
|
||||
"REAUTH": "احراز هویت مجدد",
|
||||
"VIEW": "نمایش",
|
||||
"EDIT": {
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "تنظمیات صندوق ورودی اعمال شد",
|
||||
"AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "وضعیت واگذاری خودکار گفتگو به اپراتورها تنظیم شد",
|
||||
"ERROR_MESSAGE": "در حال حاضر امکان تغییر رنگ ویجت امکانپذیر نیست. لطفا بعدا امتحان کنید."
|
||||
},
|
||||
"AUTO_ASSIGNMENT": {
|
||||
"ENABLED": "فعال",
|
||||
"DISABLED": "غیرفعال"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "حذف",
|
||||
"CONFIRM": {
|
||||
"TITLE": "تاییدیه حذف",
|
||||
"MESSAGE": "مطمئن هستید که حذف شود ",
|
||||
"YES": "بله، حذف شود ",
|
||||
"NO": "نه، بماند "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "صندوق ورودی حذف شد",
|
||||
"ERROR_MESSAGE": "صندوق ورودی حذف نشد، لطفا بعدا امتحان کنید"
|
||||
}
|
||||
},
|
||||
"SETTINGS": "تنظیمات",
|
||||
"SETTINGS_POPUP": {
|
||||
"MESSENGER_HEADING": "اسکریپت ویجت",
|
||||
"MESSENGER_SUB_HEAD": "این دکمه را در تگ body قرار دهید",
|
||||
"INBOX_AGENTS": "اپراتورها",
|
||||
"INBOX_AGENTS_SUB_TEXT": "اضافه کردن یا حذف کردن دسترسی اپراتور به صندوق ورودی",
|
||||
"UPDATE": "اعمال شود",
|
||||
"AUTO_ASSIGNMENT": "فعال کردن واگذاری خودکار گفتگو به اپراتورها",
|
||||
"INBOX_UPDATE_TITLE": "تنظیمات صندوق ورودی",
|
||||
"INBOX_UPDATE_SUB_TEXT": "تغییر پارامترهای صندوق ورودی",
|
||||
"AUTO_ASSIGNMENT_SUB_TEXT": "فعال کردن یا غیرفعال کردن واگذاری خودکار گفتگوها به اپراتورهای عضو این صندوق ورودی."
|
||||
}
|
||||
}
|
||||
}
|
32
app/javascript/dashboard/i18n/locale/fa/index.js
Normal file
32
app/javascript/dashboard/i18n/locale/fa/index.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
/* eslint-disable */
|
||||
import { default as _agentMgmt } from './agentMgmt.json';
|
||||
import { default as _cannedMgmt } from './cannedMgmt.json';
|
||||
import { default as _chatlist } from './chatlist.json';
|
||||
import { default as _contact } from './contact.json';
|
||||
import { default as _conversation } from './conversation.json';
|
||||
import { default as _inboxMgmt } from './inboxMgmt.json';
|
||||
import { default as _login } from './login.json';
|
||||
import { default as _report } from './report.json';
|
||||
import { default as _resetPassword } from './resetPassword.json';
|
||||
import { default as _setNewPassword } from './setNewPassword.json';
|
||||
import { default as _settings } from './settings.json';
|
||||
import { default as _signup } from './signup.json';
|
||||
import { default as _integrations } from './integrations.json';
|
||||
import { default as _generalSettings } from './generalSettings.json';
|
||||
|
||||
export default {
|
||||
..._agentMgmt,
|
||||
..._cannedMgmt,
|
||||
..._chatlist,
|
||||
..._contact,
|
||||
..._conversation,
|
||||
..._inboxMgmt,
|
||||
..._login,
|
||||
..._report,
|
||||
..._resetPassword,
|
||||
..._setNewPassword,
|
||||
..._settings,
|
||||
..._signup,
|
||||
..._integrations,
|
||||
..._generalSettings,
|
||||
};
|
54
app/javascript/dashboard/i18n/locale/fa/integrations.json
Normal file
54
app/javascript/dashboard/i18n/locale/fa/integrations.json
Normal file
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"HEADER": "برنامههای تلفیق شده",
|
||||
"WEBHOOK": {
|
||||
"TITLE": "وب هوک",
|
||||
"CONFIGURE": "تنظیمات",
|
||||
"HEADER": "تنظیمات وب هوک",
|
||||
"HEADER_BTN_TXT": "اضافه کردن یک وب هوک جدید",
|
||||
"INTEGRATION_TXT": "وب هوک، امکان دسترسی لحظهای به اطلاعات حساب چت وت شما را فراهم میکند. وب هوکها را میتوان به دیگر ابزارها مثل اسلک یا گیتهاب متصل کرد. دکمه تنظیمات را بزنید تا وب هوک خود را تنظیم کنید ",
|
||||
"LOADING": "درحال دریافت اطلاعات وب هوک",
|
||||
"SEARCH_404": "هیچ گزینهای با این شرایط پیدا نشد",
|
||||
"SIDEBAR_TXT": "<p><b>وب هوکها</b> </p> <p>وب هوکها اجرا کنندهی درخواستهای HTTP هستند که برای هر حسابی قابل تنظیم شدن هستند. به عنوان مثال میتوان وقتی گفتگوی جدیدی ایجاد شد یک وب سرویس صدا زده شود. برای هر حساب میتوان چند وب هوک ایجاد کرد. <br /><br /> برای ساختن یک <b>وب هوک</b>, روی دکمه <b>اضافه کردن وب هوک جدید</b> کلیک کنید. همچنین با زدن دکمه «حذف» میتوانید وب هوک ساخته شده را حذف کنید.</p>",
|
||||
"LIST": {
|
||||
"404": "هیچ وب هوکی برای این حساب ساخته نشده است",
|
||||
"TITLE": "مدیریت وب هوکها",
|
||||
"DESC": "وب هوکها قالبهای پاسخگویی آمادهای هستند که میتوانند برای پاسخگویی سریع به تیکتها به کار گرفته شوند.",
|
||||
"TABLE_HEADER": [
|
||||
"آدرس مقصد وب هوک",
|
||||
"رویدادها"
|
||||
]
|
||||
},
|
||||
"ADD": {
|
||||
"CANCEL": "انصراف",
|
||||
"TITLE": "اضافه کردن وب هوک جدید",
|
||||
"DESC": "رویدادهای وب هوک اطلاعات لحظهای حساب چت ووت شما را منتقل میکنند. لطفا آدرس URL صحیحی وارد کنید.",
|
||||
"FORM": {
|
||||
"END_POINT": {
|
||||
"LABEL": "آدرس URL وب هوک",
|
||||
"PLACEHOLDER": "به عنوان مثال: https://example/api/webhook",
|
||||
"ERROR": "لطفا آدرس URL صحیحی وارد کنید"
|
||||
},
|
||||
"SUBMIT": "ساخت وب هوک"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "وب هوک ساخته شد",
|
||||
"ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "حذف",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "وب هوک حذف شد",
|
||||
"ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "تاییدیه حذف",
|
||||
"MESSAGE": "مطمئن هستید که میخواهید حذف شود ",
|
||||
"YES": "بله، حذف شود",
|
||||
"NO": "خیر، بماند"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
21
app/javascript/dashboard/i18n/locale/fa/login.json
Normal file
21
app/javascript/dashboard/i18n/locale/fa/login.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"LOGIN": {
|
||||
"TITLE": "ورود به چت ووت",
|
||||
"EMAIL": {
|
||||
"LABEL": "ایمیل",
|
||||
"PLACEHOLDER": "ایمیل به عنوان مثال: someone@example.com"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "رمز عبور",
|
||||
"PLACEHOLDER": "رمز عبور"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "ورود موفق",
|
||||
"ERROR_MESSAGE": "متاسفانه ارتباط با سرور برقرار نشد، مجددا امتحان کنید",
|
||||
"UNAUTH": "این نام کاربری و رمز عبور صحیح نیست"
|
||||
},
|
||||
"FORGOT_PASSWORD": "رمز عبورتان را فراموش کردید؟",
|
||||
"CREATE_NEW_ACCOUNT": "حساب جدید بسازید",
|
||||
"SUBMIT": "ورود"
|
||||
}
|
||||
}
|
19
app/javascript/dashboard/i18n/locale/fa/report.json
Normal file
19
app/javascript/dashboard/i18n/locale/fa/report.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"REPORT": {
|
||||
"HEADER": "گزارشات",
|
||||
"LOADING_CHART": "در حال دریافت اطلاعات...",
|
||||
"NO_ENOUGH_DATA": "متاسفانه اطلاعات کافی دریافت نشد، لطفا بعدا دوباره امتحان کنید",
|
||||
"METRICS": [
|
||||
{ "NAME": "گفتگوها", "KEY": "conversations_count", "DESC": "( جمع کل )" },
|
||||
{ "NAME": "پیامهای ورودی", "KEY": "incoming_messages_count", "DESC": "( جمع کل )" },
|
||||
{ "NAME": "پیامهای خروجی", "KEY": "outgoing_messages_count", "DESC": "( جمع کل )" },
|
||||
{ "NAME": "زمان تا اولین پاسخ", "KEY": "avg_first_response_time", "DESC": "( میانگین )" },
|
||||
{ "NAME": "زمان تا حل شدن مساله", "KEY": "avg_resolution_time", "DESC": "( میانگین )" },
|
||||
{ "NAME": "تعداد مسائل حل شده", "KEY": "resolutions_count", "DESC": "( جمع کل )" }
|
||||
],
|
||||
"DATE_RANGE": [
|
||||
{ "id": 0, "name": "در ۷ روز گذشته" },
|
||||
{ "id": 1, "name": "در ۳۰ روز گذشته" }
|
||||
]
|
||||
}
|
||||
}
|
15
app/javascript/dashboard/i18n/locale/fa/resetPassword.json
Normal file
15
app/javascript/dashboard/i18n/locale/fa/resetPassword.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"RESET_PASSWORD": {
|
||||
"TITLE": "ریست کردن رمز عبور",
|
||||
"EMAIL": {
|
||||
"LABEL": "ایمیل",
|
||||
"PLACEHOLDER": "لطفا ایمیل خود را وارد کنید",
|
||||
"ERROR": "ظاهرا این ایمیل صحیح نیست، لطفا اصلاح کنید"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "لینک ریست کردن رمز عبور به ایمیلتان ارسال شد",
|
||||
"ERROR_MESSAGE": "ارتباط با سرور برقرار نشد، لطفا مجددا امتحان کنید"
|
||||
},
|
||||
"SUBMIT": "ثبت"
|
||||
}
|
||||
}
|
20
app/javascript/dashboard/i18n/locale/fa/setNewPassword.json
Normal file
20
app/javascript/dashboard/i18n/locale/fa/setNewPassword.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"SET_NEW_PASSWORD": {
|
||||
"TITLE": "گذاشتن رمز جدید",
|
||||
"PASSWORD": {
|
||||
"LABEL": "رمز عبور",
|
||||
"PLACEHOLDER": "رمز عبور",
|
||||
"ERROR": "رمز عبور خیلی کوتاه است"
|
||||
},
|
||||
"CONFIRM_PASSWORD": {
|
||||
"LABEL": "تکرار رمز عبور",
|
||||
"PLACEHOLDER": "لطفا رمز عبور را مجددا وارد کنید",
|
||||
"ERROR": "تکرار رمز عبور میبایست با رمز عبور یکسان باشد"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "رمز عوض شد",
|
||||
"ERROR_MESSAGE": "ارتباط با سرور برقرار نشد، لطفا مجددا امتحان کنید"
|
||||
},
|
||||
"SUBMIT": "ثبت"
|
||||
}
|
||||
}
|
108
app/javascript/dashboard/i18n/locale/fa/settings.json
Normal file
108
app/javascript/dashboard/i18n/locale/fa/settings.json
Normal file
|
@ -0,0 +1,108 @@
|
|||
{
|
||||
"PROFILE_SETTINGS": {
|
||||
"LINK": "تنظیمات پروفایل",
|
||||
"TITLE": "تنظیمات پروفایل",
|
||||
"BTN_TEXT": "تغییر دادن پروفایل",
|
||||
"AFTER_EMAIL_CHANGED": "پروفایلتان با موفقیت تغییر یافت، اطلاعات ورود به سیستم تغییر کرده لذا لطفا مجددا به سیستم وارد شوید",
|
||||
"FORM": {
|
||||
"AVATAR": "عکس پروفایل",
|
||||
"ERROR": "لطفا ایرادات ذکر شده را برطرف کنید",
|
||||
"REMOVE_IMAGE": "حذف",
|
||||
"UPLOAD_IMAGE": "آپلود عکس",
|
||||
"UPDATE_IMAGE": "تغییر عکس",
|
||||
"PROFILE_SECTION": {
|
||||
"TITLE": "پروفایل",
|
||||
"NOTE": "ایمیل عامل شناسایی شما برای ورود به سیستم است"
|
||||
},
|
||||
"PASSWORD_SECTION": {
|
||||
"TITLE": "رمز عبور",
|
||||
"NOTE": "تغییر دادن رمز عبور باعث میشود مجبور شوید دوباره به سیستم وارد شوید"
|
||||
},
|
||||
"ACCESS_TOKEN": {
|
||||
"TITLE": "توکن دسترسی",
|
||||
"NOTE": "از این توکن برای دسترسی از طریق API استفاده میشود"
|
||||
},
|
||||
"EMAIL_NOTIFICATIONS_SECTION": {
|
||||
"TITLE": "اعلامیه به ایمیل",
|
||||
"NOTE": "اینجا میتوانید تنظیمات اعلامیههایی که به ایمیل ارسال میشود تغییر دهید",
|
||||
"CONVERSATION_ASSIGNMENT": "هر وقت گفتگویی به من اختصاص داده شد، ایمیل بفرست",
|
||||
"CONVERSATION_CREATION": "هر وقت گفتگوی جدیدی شروع شد برای من ایمیل بفرست"
|
||||
},
|
||||
"API": {
|
||||
"UPDATE_SUCCESS": "تغییرات تنظیمات اعلامیهها با موفقیت ثبت شد",
|
||||
"UPDATE_ERROR": "در ثبت تغییرات اعلامیهها اشکالی پیش آمد، لطفا دوباره امتحان کنید"
|
||||
},
|
||||
"PUSH_NOTIFICATIONS_SECTION": {
|
||||
"TITLE": "پوش نوتیفیکیشن",
|
||||
"NOTE": "اینجا میتوانید تنظیمات پوش نوتیفیکیشن را تغییر دهید",
|
||||
"CONVERSATION_ASSIGNMENT": "هر وقت گفتگویی به من اختصاص داده شد، برای من پوش نوتیفیکیشن بفرست",
|
||||
"CONVERSATION_CREATION": "هر وقت گفتگوی جدیدی شروع شد برای من پوش نوتیفیکیشن بفرست",
|
||||
"HAS_ENABLED_PUSH": "در این مرورگر پوش نوتیفیکیشن را فعال کردهاید",
|
||||
"REQUEST_PUSH": "فعال کردن پوش نوتیفیکیشن"
|
||||
},
|
||||
"PROFILE_IMAGE": {
|
||||
"LABEL": "عکس پروفایل"
|
||||
},
|
||||
"NAME": {
|
||||
"LABEL": "اسم شما",
|
||||
"ERROR": "لطفا اسم خود را به شکل صحیح وارد کنید",
|
||||
"PLACEHOLDER": "لطفا اسم خود را وارد کنید، این اسم در گفتگوها دیده میشود"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "ایمیل شما",
|
||||
"ERROR": "لطفا ایمیل خود را به شکل صحیح وارد کنید",
|
||||
"PLACEHOLDER": "لطفا ایمیل خود را وارد کنید، این ایمیل در گفتگوها دیده میشود"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "رمز عبور",
|
||||
"ERROR": "رمز عبور میبایست ۶ کاراکتر یا بیشتر باشد",
|
||||
"PLACEHOLDER": "لطفا رمز عبور جدیدی وارد کنید"
|
||||
},
|
||||
"PASSWORD_CONFIRMATION": {
|
||||
"LABEL": "تکرار رمز عبور",
|
||||
"ERROR": "تکرار رمز عبور میبایست با رمز عبور یکسان باشد",
|
||||
"PLACEHOLDER": "لطفا رمز عبور را مجددا وارد کنید"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SIDEBAR_ITEMS": {
|
||||
"CHANGE_ACCOUNTS": "سوییچ به یک حساب دیگر",
|
||||
"SELECTOR_SUBTITLE": "از لیست یکی از حسابها را انتخاب کنید",
|
||||
"PROFILE_SETTINGS": "تنظیمات پروفایل",
|
||||
"LOGOUT": "خروج از حساب"
|
||||
},
|
||||
"APP_GLOBAL": {
|
||||
"TRIAL_MESSAGE": "روز تا اتمام دوره آزمایشی باقی است.",
|
||||
"TRAIL_BUTTON": "الان بخرید"
|
||||
},
|
||||
"COMPONENTS": {
|
||||
"CODE": {
|
||||
"BUTTON_TEXT": "کپی",
|
||||
"COPY_SUCCESSFUL": "کد به حافظه کپی شد"
|
||||
},
|
||||
"FILE_BUBBLE": {
|
||||
"DOWNLOAD": "دانلود",
|
||||
"UPLOADING": "در حال آپلود..."
|
||||
},
|
||||
"FORM_BUBBLE": {
|
||||
"SUBMIT": "ثبت"
|
||||
}
|
||||
},
|
||||
"CONFIRM_EMAIL": "در حال تایید...",
|
||||
"SETTINGS": {
|
||||
"INBOXES": {
|
||||
"NEW_INBOX": "اضافه کردن صندوق ورودی"
|
||||
}
|
||||
},
|
||||
"SIDEBAR": {
|
||||
"CONVERSATIONS": "گفتگوها",
|
||||
"REPORTS": "گزارشات",
|
||||
"SETTINGS": "تنظیمات",
|
||||
"HOME": "صفحه اصلی",
|
||||
"AGENTS": "اپراتورها",
|
||||
"INBOXES": "صندوقهای ورودی",
|
||||
"CANNED_RESPONSES": "پاسخهای آماده",
|
||||
"INTEGRATIONS": "برنامههای تلفیق شده",
|
||||
"ACCOUNT_SETTINGS": "تنظیمات حساب"
|
||||
}
|
||||
}
|
32
app/javascript/dashboard/i18n/locale/fa/signup.json
Normal file
32
app/javascript/dashboard/i18n/locale/fa/signup.json
Normal file
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"REGISTER": {
|
||||
"TRY_WOOT": "ثبت حساب",
|
||||
"TITLE": "ثبت نام",
|
||||
"TERMS_ACCEPT": "با ثبت نام، اعلام میدارید که <a href=\"https://www.chatwoot.com/terms\">قوانین</a> و <a href=\"https://www.chatwoot.com/privacy-policy\">شرایط استفاده</a> از ما را تایید کرده و میپذیرید.",
|
||||
"ACCOUNT_NAME": {
|
||||
"LABEL": "عنوان حساب",
|
||||
"PLACEHOLDER": "شرکت ایران ناسیونال",
|
||||
"ERROR": "عنوان خیلی کوتاه است"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "ایمیل",
|
||||
"PLACEHOLDER": "bruce@wayne.enterprises",
|
||||
"ERROR": "ایمیل اشتباه است"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "رمز عبور",
|
||||
"PLACEHOLDER": "رمز عبور",
|
||||
"ERROR": "رمز عبور خیلی کوتاه است"
|
||||
},
|
||||
"CONFIRM_PASSWORD": {
|
||||
"LABEL": "تکرار رمز عبور",
|
||||
"PLACEHOLDER": "تکرار رمز عبور",
|
||||
"ERROR": "رمز عبور و تکرار رمز عبور یکسان نیستند"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "ثبت نام با موفقیت انجام شد",
|
||||
"ERROR_MESSAGE": "ارتباط با سرور برقرار نشد، لطفا بعدا امتحان کنید"
|
||||
},
|
||||
"SUBMIT": "ثبت"
|
||||
}
|
||||
}
|
5
app/javascript/dashboard/i18n/locale/fa/webhooks.json
Normal file
5
app/javascript/dashboard/i18n/locale/fa/webhooks.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"WEBHOOKS_SETTINGS": {
|
||||
"HEADER": "تنظیمات وب هوک"
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@
|
|||
<div class="row align-center">
|
||||
<div class="medium-5 column">
|
||||
<ul class="signup--features">
|
||||
<li><i class="ion-beer beer"></i>Unlimited Facebook Pages</li>
|
||||
<li><i class="ion-beer beer"></i>Unlimited inboxes</li>
|
||||
<li><i class="ion-stats-bars report"></i>Robust Reporting</li>
|
||||
<li><i class="ion-chatbox-working canned"></i>Canned Responses</li>
|
||||
<li><i class="ion-loop uptime"></i>Auto Assignment</li>
|
||||
|
|
|
@ -106,6 +106,9 @@ export default {
|
|||
this.isEditing = false;
|
||||
},
|
||||
async fetchLabels(conversationId) {
|
||||
if (!conversationId) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('conversationLabels/get', conversationId);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -36,7 +36,15 @@
|
|||
{{ $t('GENERAL_SETTINGS.FORM.LANGUAGE.ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<label v-if="featureInboundEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.FEATURES.INBOUND_EMAIL_ENABLED') }}
|
||||
</label>
|
||||
<label v-if="featureCustomDomainEmailEnabled">
|
||||
{{
|
||||
$t('GENERAL_SETTINGS.FORM.FEATURES.CUSTOM_EMAIL_DOMAIN_ENABLED')
|
||||
}}
|
||||
</label>
|
||||
<label v-if="featureCustomDomainEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.DOMAIN.LABEL') }}
|
||||
<input
|
||||
v-model="domain"
|
||||
|
@ -44,29 +52,7 @@
|
|||
:placeholder="$t('GENERAL_SETTINGS.FORM.DOMAIN.PLACEHOLDER')"
|
||||
/>
|
||||
</label>
|
||||
<label v-if="featureInboundEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.ENABLE_DOMAIN_EMAIL.LABEL') }}
|
||||
<select v-model="domainEmailsEnabled">
|
||||
<option value="true">
|
||||
{{
|
||||
$t(
|
||||
'GENERAL_SETTINGS.FORM.ENABLE_DOMAIN_EMAIL.OPTIONS.ENABLED'
|
||||
)
|
||||
}}
|
||||
</option>
|
||||
<option value="false">
|
||||
{{
|
||||
$t(
|
||||
'GENERAL_SETTINGS.FORM.ENABLE_DOMAIN_EMAIL.OPTIONS.DISABLED'
|
||||
)
|
||||
}}
|
||||
</option>
|
||||
</select>
|
||||
<p class="help-text">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.ENABLE_DOMAIN_EMAIL.PLACEHOLDER') }}
|
||||
</p>
|
||||
</label>
|
||||
<label v-if="featureInboundEmailEnabled">
|
||||
<label v-if="featureCustomDomainEmailEnabled">
|
||||
{{ $t('GENERAL_SETTINGS.FORM.SUPPORT_EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model="supportEmail"
|
||||
|
@ -78,6 +64,10 @@
|
|||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="current-version">
|
||||
{{ `v${globalConfig.appVersion}` }}
|
||||
</div>
|
||||
|
||||
<woot-submit-button
|
||||
class="button nice success button--fixed-right-top"
|
||||
:button-text="$t('GENERAL_SETTINGS.SUBMIT')"
|
||||
|
@ -106,7 +96,6 @@ export default {
|
|||
name: '',
|
||||
locale: 'en',
|
||||
domain: '',
|
||||
domainEmailsEnabled: false,
|
||||
supportEmail: '',
|
||||
features: {},
|
||||
};
|
||||
|
@ -121,6 +110,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
getAccount: 'accounts/getAccount',
|
||||
uiFlags: 'accounts/getUIFlags',
|
||||
}),
|
||||
|
@ -132,6 +122,10 @@ export default {
|
|||
featureInboundEmailEnabled() {
|
||||
return !!this.features.inbound_emails;
|
||||
},
|
||||
|
||||
featureCustomDomainEmailEnabled() {
|
||||
return this.featureInboundEmailEnabled && !!this.customEmailDomainEnabled;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (!this.id) {
|
||||
|
@ -148,7 +142,7 @@ export default {
|
|||
id,
|
||||
domain,
|
||||
support_email,
|
||||
domain_emails_enabled,
|
||||
custom_email_domain_enabled,
|
||||
features,
|
||||
} = this.getAccount(this.accountId);
|
||||
|
||||
|
@ -158,7 +152,7 @@ export default {
|
|||
this.id = id;
|
||||
this.domain = domain;
|
||||
this.supportEmail = support_email;
|
||||
this.domainEmailsEnabled = domain_emails_enabled;
|
||||
this.customEmailDomainEnabled = custom_email_domain_enabled;
|
||||
this.features = features;
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
|
@ -177,7 +171,6 @@ export default {
|
|||
name: this.name,
|
||||
domain: this.domain,
|
||||
support_email: this.supportEmail,
|
||||
domain_emails_enabled: this.domainEmailsEnabled,
|
||||
});
|
||||
Vue.config.lang = this.locale;
|
||||
this.showAlert(this.$t('GENERAL_SETTINGS.UPDATE.SUCCESS'));
|
||||
|
@ -208,4 +201,10 @@ export default {
|
|||
padding: $space-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.current-version {
|
||||
font-size: var(--font-size-small);
|
||||
text-align: center;
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
v-model="selectedAgents"
|
||||
:options="agentList"
|
||||
track-by="id"
|
||||
label="name"
|
||||
label="available_name"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
|
|
|
@ -30,12 +30,14 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
channelList: [
|
||||
'website',
|
||||
'facebook',
|
||||
'twitter',
|
||||
'twilio',
|
||||
'telegram',
|
||||
'line',
|
||||
{ key: 'website', name: 'Website' },
|
||||
{ key: 'facebook', name: 'Facebook' },
|
||||
{ key: 'twitter', name: 'Twitter' },
|
||||
{ key: 'twilio', name: 'Twilio' },
|
||||
{ key: 'email', name: 'Email' },
|
||||
{ key: 'api', name: 'API' },
|
||||
{ key: 'telegram', name: 'Telegram' },
|
||||
{ key: 'line', name: 'Line' },
|
||||
],
|
||||
enabledFeatures: {},
|
||||
};
|
||||
|
|
|
@ -21,6 +21,14 @@
|
|||
>
|
||||
</woot-code>
|
||||
</div>
|
||||
<div class="medium-6 small-offset-3">
|
||||
<woot-code
|
||||
v-if="isAEmailInbox"
|
||||
lang="html"
|
||||
:script="currentInbox.forward_to_address"
|
||||
>
|
||||
</woot-code>
|
||||
</div>
|
||||
<router-link
|
||||
class="button success nice"
|
||||
:to="{
|
||||
|
@ -53,6 +61,9 @@ export default {
|
|||
isATwilioInbox() {
|
||||
return this.currentInbox.channel_type === 'Channel::TwilioSms';
|
||||
},
|
||||
isAEmailInbox() {
|
||||
return this.currentInbox.channel_type === 'Channel::Email';
|
||||
},
|
||||
message() {
|
||||
if (this.isATwilioInbox) {
|
||||
return `${this.$t('INBOX_MGMT.FINISH.MESSAGE')}. ${this.$t(
|
||||
|
@ -60,6 +71,10 @@ export default {
|
|||
)}`;
|
||||
}
|
||||
|
||||
if (this.isAEmailInbox) {
|
||||
return this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.FINISH_MESSAGE');
|
||||
}
|
||||
|
||||
if (!this.currentInbox.website_token) {
|
||||
return this.$t('INBOX_MGMT.FINISH.MESSAGE');
|
||||
}
|
||||
|
|
|
@ -45,6 +45,12 @@
|
|||
<span v-if="item.channel_type === 'Channel::TwilioSms'">
|
||||
Twilio SMS
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::Email'">
|
||||
Email
|
||||
</span>
|
||||
<span v-if="item.channel_type === 'Channel::Api'">
|
||||
Api
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
|
|
|
@ -3,9 +3,18 @@
|
|||
<woot-modal-header
|
||||
:header-image="inbox.avatarUrl"
|
||||
:header-title="inboxName"
|
||||
/>
|
||||
>
|
||||
<woot-tabs :index="selectedTabIndex" @change="onTabChange">
|
||||
<woot-tabs-item
|
||||
v-for="tab in tabs"
|
||||
:key="tab.key"
|
||||
:name="tab.name"
|
||||
:show-badge="false"
|
||||
/>
|
||||
</woot-tabs>
|
||||
</woot-modal-header>
|
||||
|
||||
<div class="settings--content">
|
||||
<div v-if="selectedTabKey === 'inbox_settings'" class="settings--content">
|
||||
<settings-section
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_UPDATE_SUB_TEXT')"
|
||||
|
@ -16,7 +25,7 @@
|
|||
@change="handleImageUpload"
|
||||
/>
|
||||
<woot-input
|
||||
v-if="isAWidgetInbox"
|
||||
v-if="isAWebWidgetInbox"
|
||||
v-model.trim="selectedInboxName"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_NAME.LABEL')"
|
||||
|
@ -25,7 +34,7 @@
|
|||
"
|
||||
/>
|
||||
<woot-input
|
||||
v-if="isAWidgetInbox"
|
||||
v-if="isAWebWidgetInbox"
|
||||
v-model.trim="channelWebsiteUrl"
|
||||
class="medium-9 columns"
|
||||
:label="$t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.CHANNEL_DOMAIN.LABEL')"
|
||||
|
@ -34,7 +43,7 @@
|
|||
"
|
||||
/>
|
||||
<woot-input
|
||||
v-if="isAWidgetInbox"
|
||||
v-if="isAWebWidgetInbox"
|
||||
v-model.trim="channelWelcomeTitle"
|
||||
class="medium-9 columns"
|
||||
:label="
|
||||
|
@ -48,7 +57,7 @@
|
|||
/>
|
||||
|
||||
<woot-input
|
||||
v-if="isAWidgetInbox"
|
||||
v-if="isAWebWidgetInbox"
|
||||
v-model.trim="channelWelcomeTagline"
|
||||
class="medium-9 columns"
|
||||
:label="
|
||||
|
@ -61,7 +70,7 @@
|
|||
"
|
||||
/>
|
||||
|
||||
<label v-if="isAWidgetInbox" class="medium-9 columns">
|
||||
<label v-if="isAWebWidgetInbox" class="medium-9 columns">
|
||||
{{ $t('INBOX_MGMT.ADD.WEBSITE_CHANNEL.WIDGET_COLOR.LABEL') }}
|
||||
<woot-color-picker v-model="inbox.widget_color" />
|
||||
</label>
|
||||
|
@ -94,7 +103,6 @@
|
|||
}}
|
||||
</p>
|
||||
</label>
|
||||
|
||||
<woot-input
|
||||
v-if="greetingEnabled"
|
||||
v-model.trim="greetingMessage"
|
||||
|
@ -122,6 +130,33 @@
|
|||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.AUTO_ASSIGNMENT_SUB_TEXT') }}
|
||||
</p>
|
||||
</label>
|
||||
|
||||
<label v-if="isAWebWidgetInbox">
|
||||
{{ $t('INBOX_MGMT.FEATURES.LABEL') }}
|
||||
</label>
|
||||
<div v-if="isAWebWidgetInbox" class="widget--feature-flag">
|
||||
<input
|
||||
v-model="selectedFeatureFlags"
|
||||
type="checkbox"
|
||||
value="attachments"
|
||||
@input="handleFeatureFlag"
|
||||
/>
|
||||
<label for="attachments">
|
||||
{{ $t('INBOX_MGMT.FEATURES.DISPLAY_FILE_PICKER') }}
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="isAWebWidgetInbox">
|
||||
<input
|
||||
v-model="selectedFeatureFlags"
|
||||
type="checkbox"
|
||||
value="emoji_picker"
|
||||
@input="handleFeatureFlag"
|
||||
/>
|
||||
<label for="emoji_picker">
|
||||
{{ $t('INBOX_MGMT.FEATURES.DISPLAY_EMOJI_PICKER') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<woot-submit-button
|
||||
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
|
||||
:loading="uiFlags.isUpdatingInbox"
|
||||
|
@ -132,7 +167,7 @@
|
|||
|
||||
<!-- update agents in inbox -->
|
||||
|
||||
<div class="settings--content">
|
||||
<div v-if="selectedTabKey === 'collaborators'" class="settings--content">
|
||||
<settings-section
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS')"
|
||||
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.INBOX_AGENTS_SUB_TEXT')"
|
||||
|
@ -141,7 +176,7 @@
|
|||
v-model="selectedAgents"
|
||||
:options="agentList"
|
||||
track-by="id"
|
||||
label="name"
|
||||
label="available_name"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:clear-on-select="false"
|
||||
|
@ -157,56 +192,43 @@
|
|||
/>
|
||||
</settings-section>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="inbox.channel_type === 'Channel::TwilioSms'"
|
||||
class="settings--content"
|
||||
>
|
||||
<settings-section
|
||||
:title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.SUBTITLE')"
|
||||
>
|
||||
<woot-code :script="twilioCallbackURL" lang="html"></woot-code>
|
||||
</settings-section>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="inbox.channel_type === 'Channel::FacebookPage'"
|
||||
class="settings--content"
|
||||
>
|
||||
<settings-section
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')"
|
||||
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')"
|
||||
>
|
||||
<woot-code :script="messengerScript"></woot-code>
|
||||
</settings-section>
|
||||
</div>
|
||||
<div v-else-if="inbox.channel_type === 'Channel::WebWidget'">
|
||||
<div class="settings--content">
|
||||
<div v-if="selectedTabKey === 'configuration'">
|
||||
<div v-if="isATwilioChannel" class="settings--content">
|
||||
<settings-section
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')"
|
||||
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')"
|
||||
:title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.TITLE')"
|
||||
:sub-title="$t('INBOX_MGMT.ADD.TWILIO.API_CALLBACK.SUBTITLE')"
|
||||
>
|
||||
<woot-code :script="inbox.web_widget_script"></woot-code>
|
||||
<woot-code :script="twilioCallbackURL" lang="html"></woot-code>
|
||||
</settings-section>
|
||||
</div>
|
||||
<div v-else-if="isAWebWidgetInbox">
|
||||
<div class="settings--content">
|
||||
<settings-section
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_HEADING')"
|
||||
:sub-title="$t('INBOX_MGMT.SETTINGS_POPUP.MESSENGER_SUB_HEAD')"
|
||||
>
|
||||
<woot-code :script="inbox.web_widget_script"></woot-code>
|
||||
</settings-section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint no-console: 0 */
|
||||
/* global bus */
|
||||
import { mapGetters } from 'vuex';
|
||||
import { createMessengerScript } from 'dashboard/helper/scriptGenerator';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import SettingsSection from '../../../../components/SettingsSection';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsSection,
|
||||
},
|
||||
mixins: [configMixin],
|
||||
mixins: [alertMixin, configMixin, inboxMixin],
|
||||
data() {
|
||||
return {
|
||||
avatarFile: null,
|
||||
|
@ -220,6 +242,7 @@ export default {
|
|||
channelWebsiteUrl: '',
|
||||
channelWelcomeTitle: '',
|
||||
channelWelcomeTagline: '',
|
||||
selectedFeatureFlags: [],
|
||||
autoAssignmentOptions: [
|
||||
{
|
||||
value: true,
|
||||
|
@ -230,6 +253,7 @@ export default {
|
|||
label: this.$t('INBOX_MGMT.EDIT.AUTO_ASSIGNMENT.DISABLED'),
|
||||
},
|
||||
],
|
||||
selectedTabIndex: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -237,17 +261,41 @@ export default {
|
|||
agentList: 'agents/getAgents',
|
||||
uiFlags: 'inboxes/getUIFlags',
|
||||
}),
|
||||
selectedTabKey() {
|
||||
return this.tabs[this.selectedTabIndex]?.key;
|
||||
},
|
||||
tabs() {
|
||||
const visibleToAllChannelTabs = [
|
||||
{
|
||||
key: 'inbox_settings',
|
||||
name: this.$t('INBOX_MGMT.TABS.SETTINGS'),
|
||||
},
|
||||
{
|
||||
key: 'collaborators',
|
||||
name: this.$t('INBOX_MGMT.TABS.COLLABORATORS'),
|
||||
},
|
||||
];
|
||||
|
||||
if (this.isAWebWidgetInbox || this.isATwilioChannel) {
|
||||
return [
|
||||
...visibleToAllChannelTabs,
|
||||
{
|
||||
key: 'configuration',
|
||||
name: this.$t('INBOX_MGMT.TABS.CONFIGURATION'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return visibleToAllChannelTabs;
|
||||
},
|
||||
currentInboxId() {
|
||||
return this.$route.params.inboxId;
|
||||
},
|
||||
inbox() {
|
||||
return this.$store.getters['inboxes/getInbox'](this.currentInboxId);
|
||||
},
|
||||
isAWidgetInbox() {
|
||||
return this.inbox.channel_type === 'Channel::WebWidget';
|
||||
},
|
||||
inboxName() {
|
||||
if (this.inbox.channel_type === 'Channel::TwilioSms') {
|
||||
if (this.isATwilioSMSChannel || this.isATwilioWhatsappChannel) {
|
||||
return `${this.inbox.name} (${this.inbox.phone_number})`;
|
||||
}
|
||||
return this.inbox.name;
|
||||
|
@ -267,10 +315,25 @@ export default {
|
|||
this.fetchInboxSettings();
|
||||
},
|
||||
methods: {
|
||||
showAlert(message) {
|
||||
bus.$emit('newToastMessage', message);
|
||||
handleFeatureFlag(e) {
|
||||
console.log(e.target.value);
|
||||
this.selectedFeatureFlags = this.toggleInput(
|
||||
this.selectedFeatureFlags,
|
||||
e.target.value
|
||||
);
|
||||
},
|
||||
toggleInput(selected, current) {
|
||||
if (selected.includes(current)) {
|
||||
const newSelectedFlags = selected.filter(flag => flag !== current);
|
||||
return newSelectedFlags;
|
||||
}
|
||||
return [...selected, current];
|
||||
},
|
||||
onTabChange(selectedTabIndex) {
|
||||
this.selectedTabIndex = selectedTabIndex;
|
||||
},
|
||||
fetchInboxSettings() {
|
||||
this.selectedTabIndex = 0;
|
||||
this.selectedAgents = [];
|
||||
this.$store.dispatch('agents/get');
|
||||
this.$store.dispatch('inboxes/get').then(() => {
|
||||
|
@ -283,6 +346,7 @@ export default {
|
|||
this.channelWebsiteUrl = this.inbox.website_url;
|
||||
this.channelWelcomeTitle = this.inbox.welcome_title;
|
||||
this.channelWelcomeTagline = this.inbox.welcome_tagline;
|
||||
this.selectedFeatureFlags = this.inbox.selected_feature_flags || [];
|
||||
});
|
||||
},
|
||||
async fetchAttachedAgents() {
|
||||
|
@ -291,16 +355,9 @@ export default {
|
|||
inboxId: this.currentInboxId,
|
||||
});
|
||||
const {
|
||||
data: { payload },
|
||||
data: { payload: inboxMembers },
|
||||
} = response;
|
||||
payload.forEach(el => {
|
||||
const [item] = this.agentList.filter(
|
||||
agent => agent.id === el.user_id
|
||||
);
|
||||
if (item) {
|
||||
this.selectedAgents.push(item);
|
||||
}
|
||||
});
|
||||
this.selectedAgents = inboxMembers;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
@ -332,6 +389,7 @@ export default {
|
|||
website_url: this.channelWebsiteUrl,
|
||||
welcome_title: this.channelWelcomeTitle || '',
|
||||
welcome_tagline: this.channelWelcomeTagline || '',
|
||||
selectedFeatureFlags: this.selectedFeatureFlags,
|
||||
},
|
||||
};
|
||||
if (this.avatarFile) {
|
||||
|
@ -377,7 +435,16 @@ export default {
|
|||
.page-top-bar {
|
||||
@include background-light;
|
||||
@include border-normal-bottom;
|
||||
padding: $space-normal $space-larger;
|
||||
padding: $space-normal $space-large 0;
|
||||
|
||||
.tabs {
|
||||
padding: 0;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.widget--feature-flag {
|
||||
padding-top: var(--space-small);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,12 +2,16 @@ import Facebook from './channels/Facebook';
|
|||
import Website from './channels/Website';
|
||||
import Twitter from './channels/Twitter';
|
||||
import Twilio from './channels/Twilio';
|
||||
import Api from './channels/Api';
|
||||
import Email from './channels/Email';
|
||||
|
||||
const channelViewList = {
|
||||
facebook: Facebook,
|
||||
website: Website,
|
||||
twitter: Twitter,
|
||||
twilio: Twilio,
|
||||
api: Api,
|
||||
email: Email,
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
<template>
|
||||
<div class="wizard-body small-9 columns">
|
||||
<page-header
|
||||
:header-title="$t('INBOX_MGMT.ADD.API_CHANNEL.TITLE')"
|
||||
:header-content="$t('INBOX_MGMT.ADD.API_CHANNEL.DESC')"
|
||||
/>
|
||||
<form class="row" @submit.prevent="createChannel()">
|
||||
<div class="medium-8 columns">
|
||||
<label :class="{ error: $v.channelName.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.API_CHANNEL.CHANNEL_NAME.LABEL') }}
|
||||
<input
|
||||
v-model.trim="channelName"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.API_CHANNEL.CHANNEL_NAME.PLACEHOLDER')
|
||||
"
|
||||
@blur="$v.channelName.$touch"
|
||||
/>
|
||||
<span v-if="$v.channelName.$error" class="message">{{
|
||||
$t('INBOX_MGMT.ADD.API_CHANNEL.CHANNEL_NAME.ERROR')
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="medium-8 columns">
|
||||
<label :class="{ error: $v.webhookUrl.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.API_CHANNEL.WEBHOOK_URL.LABEL') }}
|
||||
<input
|
||||
v-model.trim="webhookUrl"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.API_CHANNEL.WEBHOOK_URL.PLACEHOLDER')
|
||||
"
|
||||
@blur="$v.webhookUrl.$touch"
|
||||
/>
|
||||
</label>
|
||||
<p class="help-text">
|
||||
{{ $t('INBOX_MGMT.ADD.API_CHANNEL.WEBHOOK_URL.SUBTITLE') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="medium-12 columns">
|
||||
<woot-submit-button
|
||||
:loading="uiFlags.isCreating"
|
||||
:button-text="$t('INBOX_MGMT.ADD.API_CHANNEL.SUBMIT_BUTTON')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
import router from '../../../../index';
|
||||
import PageHeader from '../../SettingsSubPageHeader';
|
||||
|
||||
const shouldBeWebhookUrl = (value = '') => value.startsWith('http');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PageHeader,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
channelName: '',
|
||||
webhookUrl: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'inboxes/getUIFlags',
|
||||
}),
|
||||
},
|
||||
validations: {
|
||||
channelName: { required },
|
||||
webhookUrl: { required, shouldBeWebhookUrl },
|
||||
},
|
||||
methods: {
|
||||
async createChannel() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const apiChannel = await this.$store.dispatch('inboxes/createChannel', {
|
||||
name: this.channelName,
|
||||
channel: {
|
||||
type: 'api',
|
||||
webhook_url: this.webhookUrl,
|
||||
},
|
||||
});
|
||||
|
||||
router.replace({
|
||||
name: 'settings_inboxes_add_agents',
|
||||
params: {
|
||||
page: 'new',
|
||||
inbox_id: apiChannel.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('INBOX_MGMT.ADD.API_CHANNEL.API.ERROR_MESSAGE'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<div class="wizard-body small-9 columns">
|
||||
<page-header
|
||||
:header-title="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.TITLE')"
|
||||
:header-content="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.DESC')"
|
||||
/>
|
||||
<form class="row" @submit.prevent="createChannel()">
|
||||
<div class="medium-8 columns">
|
||||
<label :class="{ error: $v.channelName.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.EMAIL_CHANNEL.CHANNEL_NAME.LABEL') }}
|
||||
<input
|
||||
v-model.trim="channelName"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.CHANNEL_NAME.PLACEHOLDER')
|
||||
"
|
||||
@blur="$v.channelName.$touch"
|
||||
/>
|
||||
<span v-if="$v.channelName.$error" class="message">{{
|
||||
$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.CHANNEL_NAME.ERROR')
|
||||
}}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="medium-8 columns">
|
||||
<label :class="{ error: $v.email.$error }">
|
||||
{{ $t('INBOX_MGMT.ADD.EMAIL_CHANNEL.EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model.trim="email"
|
||||
type="text"
|
||||
:placeholder="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.EMAIL.PLACEHOLDER')"
|
||||
@blur="$v.email.$touch"
|
||||
/>
|
||||
</label>
|
||||
<p class="help-text">
|
||||
{{ $t('INBOX_MGMT.ADD.EMAIL_CHANNEL.EMAIL.SUBTITLE') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="medium-12 columns">
|
||||
<woot-submit-button
|
||||
:loading="uiFlags.isCreating"
|
||||
:button-text="$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.SUBMIT_BUTTON')"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { required } from 'vuelidate/lib/validators';
|
||||
import router from '../../../../index';
|
||||
import PageHeader from '../../SettingsSubPageHeader';
|
||||
|
||||
const validEmail = (value = '') => value.includes('@');
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PageHeader,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
channelName: '',
|
||||
email: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'inboxes/getUIFlags',
|
||||
}),
|
||||
},
|
||||
validations: {
|
||||
channelName: { required },
|
||||
email: { required, validEmail },
|
||||
},
|
||||
methods: {
|
||||
async createChannel() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const emailChannel = await this.$store.dispatch(
|
||||
'inboxes/createChannel',
|
||||
{
|
||||
name: this.channelName,
|
||||
channel: {
|
||||
type: 'email',
|
||||
email: this.email,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
router.replace({
|
||||
name: 'settings_inboxes_add_agents',
|
||||
params: {
|
||||
page: 'new',
|
||||
inbox_id: emailChannel.id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
this.$t('INBOX_MGMT.ADD.EMAIL_CHANNEL.API.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="column content-box">
|
||||
<div class="row">
|
||||
<div class="small-8 columns integrations-wrap">
|
||||
<div class="small-12 columns integrations-wrap">
|
||||
<div class="row integrations">
|
||||
<div
|
||||
v-for="item in integrationsList"
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue