Compare commits
109 commits
snyk-fix-4
...
develop
Author | SHA1 | Date | |
---|---|---|---|
|
98c289dc3e | ||
|
3e91765472 | ||
|
1bf23055df | ||
|
2af337be10 | ||
|
dbb6c0a074 | ||
|
8c88344170 | ||
|
6a78254701 | ||
|
26ada8b342 | ||
|
3c6bd2c8fd | ||
|
2c2c47d7fd | ||
|
34f7405689 | ||
|
3ebfb3a140 | ||
|
2dfe38ae4d | ||
|
ca88eb55f4 | ||
|
d1a26e80f4 | ||
|
022d0b0ea3 | ||
|
5541d9e00b | ||
|
38587b3aa1 | ||
|
4d2b7c37a0 | ||
|
aaacf9d4d2 | ||
|
82d3398932 | ||
|
9106f6278d | ||
|
f8e6308caf | ||
|
72fcaa739c | ||
|
9292653bf9 | ||
|
2a1a38f986 | ||
|
2972319026 | ||
|
26e05de642 | ||
|
8222a47154 | ||
|
9d78f0d6c6 | ||
|
86958278cd | ||
|
823c836906 | ||
|
6200559123 | ||
|
7dc790a7e0 | ||
|
431e2931c4 | ||
|
52ea201070 | ||
|
779bcf5e0d | ||
|
6064aad38f | ||
|
caa45d1d92 | ||
|
f1d1bb84fd | ||
|
01cc3d7c9c | ||
|
89cfc5bbf3 | ||
|
a82b9991b3 | ||
|
06434bc655 | ||
|
b9fd1d88ea | ||
|
8004f67efe | ||
|
87ef39ad9c | ||
|
c3b6e1a732 | ||
|
c9cae01cb4 | ||
|
613fb0b064 | ||
|
0b5c82ad5f | ||
|
c8ec397c79 | ||
|
a08099bbcc | ||
|
e35638588a | ||
|
3083f74d45 | ||
|
85b52a1d3f | ||
|
c94ba16565 | ||
|
0cad3bed71 | ||
|
edcbd53425 | ||
|
a397f01692 | ||
|
4755031e1d | ||
|
fc9fc5a661 | ||
|
b05d06a28a | ||
|
8813c77907 | ||
|
8ea0660862 | ||
|
606fc9046a | ||
|
e593e516b8 | ||
|
b5f8524167 | ||
|
b765e17457 | ||
|
db37bfea06 | ||
|
16bfd68d95 | ||
|
33aacb3401 | ||
|
47676c3cce | ||
|
9bfbd528ef | ||
|
e85f998a08 | ||
|
66044a0dc3 | ||
|
9b9c019de0 | ||
|
86e0ff76c5 | ||
|
abbb6ac676 | ||
|
42b466bda2 | ||
|
956837ded5 | ||
|
f0ef497005 | ||
|
8e2da837d4 | ||
|
e7f1a9ab4d | ||
|
826a735cdb | ||
|
38ab3c36db | ||
|
b5f7be0cd2 | ||
|
efceaec950 | ||
|
6aba352e0d | ||
|
9eb861a3b7 | ||
|
3184c8964d | ||
|
865346223b | ||
|
4f82859bba | ||
|
47c90e2085 | ||
|
7352b928da | ||
|
5febdde938 | ||
|
d39ace5a6b | ||
|
e2059cfc5b | ||
|
2e42821c48 | ||
|
16d59f4bb0 | ||
|
0d9ed0674b | ||
|
6ff0c93659 | ||
|
20406dce01 | ||
|
b50890d1b5 | ||
|
48373628a1 | ||
|
479d88a480 | ||
|
526722dffa | ||
|
a23974d8b9 | ||
|
894234e777 |
1004 changed files with 33751 additions and 4025 deletions
|
@ -56,13 +56,14 @@ RAILS_MAX_THREADS=5
|
||||||
|
|
||||||
# The email from which all outgoing emails are sent
|
# The email from which all outgoing emails are sent
|
||||||
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
|
# could user either `email@yourdomain.com` or `BrandName <email@yourdomain.com>`
|
||||||
MAILER_SENDER_EMAIL="Chatwoot <accounts@chatwoot.com>"
|
MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
|
||||||
|
|
||||||
#SMTP domain key is set up for HELO checking
|
#SMTP domain key is set up for HELO checking
|
||||||
SMTP_DOMAIN=chatwoot.com
|
SMTP_DOMAIN=chatwoot.com
|
||||||
# the default value is set "mailhog" and is used by docker-compose for development environments,
|
# Set the value to "mailhog" if using docker-compose for development environments,
|
||||||
# Set the value as "localhost" or your SMTP address in other environments
|
# Set the value as "localhost" or your SMTP address in other environments
|
||||||
SMTP_ADDRESS=mailhog
|
# If SMTP_ADDRESS is empty, Chatwoot would try to use sendmail(postfix)
|
||||||
|
SMTP_ADDRESS=
|
||||||
SMTP_PORT=1025
|
SMTP_PORT=1025
|
||||||
SMTP_USERNAME=
|
SMTP_USERNAME=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -60,3 +60,5 @@ test/cypress/videos/*
|
||||||
|
|
||||||
/config/master.key
|
/config/master.key
|
||||||
/config/*.enc
|
/config/*.enc
|
||||||
|
|
||||||
|
.vscode/settings.json
|
||||||
|
|
|
@ -16,7 +16,6 @@ Metrics/ClassLength:
|
||||||
- 'app/models/message.rb'
|
- 'app/models/message.rb'
|
||||||
- 'app/builders/messages/facebook/message_builder.rb'
|
- 'app/builders/messages/facebook/message_builder.rb'
|
||||||
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
- 'app/controllers/api/v1/accounts/contacts_controller.rb'
|
||||||
- 'app/controllers/api/v1/accounts/conversations_controller.rb'
|
|
||||||
- 'app/listeners/action_cable_listener.rb'
|
- 'app/listeners/action_cable_listener.rb'
|
||||||
- 'app/models/conversation.rb'
|
- 'app/models/conversation.rb'
|
||||||
RSpec/ExampleLength:
|
RSpec/ExampleLength:
|
||||||
|
|
1
.vscode/settings.json
vendored
Normal file
1
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
8
Gemfile
8
Gemfile
|
@ -4,7 +4,7 @@ ruby '3.0.4'
|
||||||
|
|
||||||
##-- base gems for rails --##
|
##-- base gems for rails --##
|
||||||
gem 'rack-cors', require: 'rack/cors'
|
gem 'rack-cors', require: 'rack/cors'
|
||||||
gem 'rails', '~>6.1'
|
gem 'rails', '~> 6.1', '>= 6.1.6.1'
|
||||||
# Reduces boot times through caching; required in config/boot.rb
|
# Reduces boot times through caching; required in config/boot.rb
|
||||||
gem 'bootsnap', require: false
|
gem 'bootsnap', require: false
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ gem 'activerecord-import'
|
||||||
gem 'dotenv-rails'
|
gem 'dotenv-rails'
|
||||||
gem 'foreman'
|
gem 'foreman'
|
||||||
gem 'puma'
|
gem 'puma'
|
||||||
gem 'webpacker', '~> 5.x'
|
gem 'webpacker', '~> 5.4', '>= 5.4.3'
|
||||||
# metrics on heroku
|
# metrics on heroku
|
||||||
gem 'barnes'
|
gem 'barnes'
|
||||||
|
|
||||||
|
@ -94,7 +94,7 @@ gem 'ddtrace'
|
||||||
gem 'elastic-apm'
|
gem 'elastic-apm'
|
||||||
gem 'newrelic_rpm'
|
gem 'newrelic_rpm'
|
||||||
gem 'scout_apm'
|
gem 'scout_apm'
|
||||||
gem 'sentry-rails', '~> 5.3'
|
gem 'sentry-rails', '~> 5.3', '>= 5.3.1'
|
||||||
gem 'sentry-ruby', '~> 5.3'
|
gem 'sentry-ruby', '~> 5.3'
|
||||||
gem 'sentry-sidekiq', '~> 5.3'
|
gem 'sentry-sidekiq', '~> 5.3'
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ group :development, :test do
|
||||||
gem 'mock_redis'
|
gem 'mock_redis'
|
||||||
gem 'pry-rails'
|
gem 'pry-rails'
|
||||||
gem 'rspec_junit_formatter'
|
gem 'rspec_junit_formatter'
|
||||||
gem 'rspec-rails', '~> 5.0.0'
|
gem 'rspec-rails', '~> 5.0.3'
|
||||||
gem 'rubocop', require: false
|
gem 'rubocop', require: false
|
||||||
gem 'rubocop-performance', require: false
|
gem 'rubocop-performance', require: false
|
||||||
gem 'rubocop-rails', require: false
|
gem 'rubocop-rails', require: false
|
||||||
|
|
24
Gemfile.lock
24
Gemfile.lock
|
@ -398,7 +398,7 @@ GEM
|
||||||
llhttp-ffi (0.4.0)
|
llhttp-ffi (0.4.0)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
loofah (2.18.0)
|
loofah (2.19.1)
|
||||||
crass (~> 1.0.2)
|
crass (~> 1.0.2)
|
||||||
nokogiri (>= 1.5.9)
|
nokogiri (>= 1.5.9)
|
||||||
mail (2.7.1)
|
mail (2.7.1)
|
||||||
|
@ -427,14 +427,14 @@ GEM
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
newrelic_rpm (8.9.0)
|
newrelic_rpm (8.9.0)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.13.9)
|
nokogiri (1.13.10)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.9-arm64-darwin)
|
nokogiri (1.13.10-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.9-x86_64-darwin)
|
nokogiri (1.13.10-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.9-x86_64-linux)
|
nokogiri (1.13.10-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth (0.5.10)
|
oauth (0.5.10)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
|
@ -459,7 +459,7 @@ GEM
|
||||||
pundit (2.2.0)
|
pundit (2.2.0)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.6.0)
|
racc (1.6.1)
|
||||||
rack (2.2.4)
|
rack (2.2.4)
|
||||||
rack-attack (6.6.1)
|
rack-attack (6.6.1)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
|
@ -488,8 +488,8 @@ GEM
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.4.3)
|
rails-html-sanitizer (1.4.4)
|
||||||
loofah (~> 2.3)
|
loofah (~> 2.19, >= 2.19.1)
|
||||||
railties (6.1.6.1)
|
railties (6.1.6.1)
|
||||||
actionpack (= 6.1.6.1)
|
actionpack (= 6.1.6.1)
|
||||||
activesupport (= 6.1.6.1)
|
activesupport (= 6.1.6.1)
|
||||||
|
@ -765,12 +765,12 @@ DEPENDENCIES
|
||||||
rack-attack
|
rack-attack
|
||||||
rack-cors
|
rack-cors
|
||||||
rack-timeout
|
rack-timeout
|
||||||
rails (~> 6.1)
|
rails (~> 6.1, >= 6.1.6.1)
|
||||||
redis
|
redis
|
||||||
redis-namespace
|
redis-namespace
|
||||||
responders
|
responders
|
||||||
rest-client
|
rest-client
|
||||||
rspec-rails (~> 5.0.0)
|
rspec-rails (~> 5.0.3)
|
||||||
rspec_junit_formatter
|
rspec_junit_formatter
|
||||||
rubocop
|
rubocop
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
|
@ -778,7 +778,7 @@ DEPENDENCIES
|
||||||
rubocop-rspec
|
rubocop-rspec
|
||||||
scout_apm
|
scout_apm
|
||||||
seed_dump
|
seed_dump
|
||||||
sentry-rails (~> 5.3)
|
sentry-rails (~> 5.3, >= 5.3.1)
|
||||||
sentry-ruby (~> 5.3)
|
sentry-ruby (~> 5.3)
|
||||||
sentry-sidekiq (~> 5.3)
|
sentry-sidekiq (~> 5.3)
|
||||||
shoulda-matchers
|
shoulda-matchers
|
||||||
|
@ -799,7 +799,7 @@ DEPENDENCIES
|
||||||
valid_email2
|
valid_email2
|
||||||
web-console
|
web-console
|
||||||
webmock
|
webmock
|
||||||
webpacker (~> 5.x)
|
webpacker (~> 5.4, >= 5.4.3)
|
||||||
webpush
|
webpush
|
||||||
wisper (= 2.0.0)
|
wisper (= 2.0.0)
|
||||||
working_hours
|
working_hours
|
||||||
|
|
14
app.json
14
app.json
|
@ -41,16 +41,24 @@
|
||||||
"formation": {
|
"formation": {
|
||||||
"web": {
|
"web": {
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"size": "FREE"
|
"size": "basic"
|
||||||
},
|
},
|
||||||
"worker": {
|
"worker": {
|
||||||
"quantity": 1,
|
"quantity": 1,
|
||||||
"size": "FREE"
|
"size": "basic"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stack": "heroku-20",
|
"stack": "heroku-20",
|
||||||
"image": "heroku/ruby",
|
"image": "heroku/ruby",
|
||||||
"addons": [ "heroku-redis", "heroku-postgresql"],
|
"addons": [
|
||||||
|
{
|
||||||
|
"plan": "heroku-redis:mini"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"plan": "heroku-postgresql:mini"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"stack": "heroku-20",
|
||||||
"buildpacks": [
|
"buildpacks": [
|
||||||
{
|
{
|
||||||
"url": "heroku/ruby"
|
"url": "heroku/ruby"
|
||||||
|
|
40
app/builders/conversation_builder.rb
Normal file
40
app/builders/conversation_builder.rb
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
class ConversationBuilder
|
||||||
|
pattr_initialize [:params!, :contact_inbox!]
|
||||||
|
|
||||||
|
def perform
|
||||||
|
look_up_exising_conversation || create_new_conversation
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def look_up_exising_conversation
|
||||||
|
return unless @contact_inbox.inbox.lock_to_single_conversation?
|
||||||
|
|
||||||
|
@contact_inbox.conversations.last
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_new_conversation
|
||||||
|
::Conversation.create!(conversation_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversation_params
|
||||||
|
additional_attributes = params[:additional_attributes]&.permit! || {}
|
||||||
|
custom_attributes = params[:custom_attributes]&.permit! || {}
|
||||||
|
status = params[:status].present? ? { status: params[:status] } : {}
|
||||||
|
|
||||||
|
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||||
|
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
|
||||||
|
# status = { status: 'pending' } if status[:status] == 'bot'
|
||||||
|
{
|
||||||
|
account_id: @contact_inbox.inbox.account_id,
|
||||||
|
inbox_id: @contact_inbox.inbox_id,
|
||||||
|
contact_id: @contact_inbox.contact_id,
|
||||||
|
contact_inbox_id: @contact_inbox.id,
|
||||||
|
additional_attributes: additional_attributes,
|
||||||
|
custom_attributes: custom_attributes,
|
||||||
|
snoozed_until: params[:snoozed_until],
|
||||||
|
assignee_id: params[:assignee_id],
|
||||||
|
team_id: params[:team_id]
|
||||||
|
}.merge(status)
|
||||||
|
end
|
||||||
|
end
|
|
@ -46,6 +46,7 @@ class Messages::Messenger::MessageBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_attachment_file_type(attachment)
|
def update_attachment_file_type(attachment)
|
||||||
|
return if @message.reload.attachments.blank?
|
||||||
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
|
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
|
||||||
|
|
||||||
attachment.file_type = file_type(attachment.file&.content_type)
|
attachment.file_type = file_type(attachment.file&.content_type)
|
||||||
|
@ -62,6 +63,7 @@ class Messages::Messenger::MessageBuilder
|
||||||
story_sender = result['from']['username']
|
story_sender = result['from']['username']
|
||||||
message.content_attributes[:story_sender] = story_sender
|
message.content_attributes[:story_sender] = story_sender
|
||||||
message.content_attributes[:story_id] = story_id
|
message.content_attributes[:story_id] = story_id
|
||||||
|
message.content_attributes[:image_type] = 'story_mention'
|
||||||
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
||||||
message.save!
|
message.save!
|
||||||
end
|
end
|
||||||
|
@ -74,6 +76,7 @@ class Messages::Messenger::MessageBuilder
|
||||||
raise
|
raise
|
||||||
rescue Koala::Facebook::ClientError => e
|
rescue Koala::Facebook::ClientError => e
|
||||||
# The exception occurs when we are trying fetch the deleted story or blocked story.
|
# The exception occurs when we are trying fetch the deleted story or blocked story.
|
||||||
|
@message.attachments.destroy_all
|
||||||
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
|
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
|
||||||
Rails.logger.error e
|
Rails.logger.error e
|
||||||
{}
|
{}
|
||||||
|
|
|
@ -2,7 +2,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
|
||||||
def index
|
def index
|
||||||
@conversations = Current.account.conversations.includes(
|
@conversations = Current.account.conversations.includes(
|
||||||
:assignee, :contact, :inbox, :taggings
|
:assignee, :contact, :inbox, :taggings
|
||||||
).where(inbox_id: inbox_ids, contact_id: @contact.id)
|
).where(inbox_id: inbox_ids, contact_id: @contact.id).order(id: :desc).limit(20)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
@ -24,7 +24,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
|
|
||||||
def create
|
def create
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@conversation = ::Conversation.create!(conversation_params)
|
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform
|
||||||
Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present?
|
Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -75,10 +75,13 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_last_seen
|
def update_last_seen
|
||||||
# rubocop:disable Rails/SkipsModelValidations
|
update_last_seen_on_conversation(DateTime.now.utc, assignee?)
|
||||||
@conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
|
end
|
||||||
@conversation.update_column(:assignee_last_seen_at, DateTime.now.utc) if assignee?
|
|
||||||
# rubocop:enable Rails/SkipsModelValidations
|
def unread
|
||||||
|
last_incoming_message = @conversation.messages.incoming.last
|
||||||
|
last_seen_at = last_incoming_message.created_at - 1.second if last_incoming_message.present?
|
||||||
|
update_last_seen_on_conversation(last_seen_at, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
def custom_attributes
|
def custom_attributes
|
||||||
|
@ -88,9 +91,18 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def update_last_seen_on_conversation(last_seen_at, update_assignee)
|
||||||
|
# rubocop:disable Rails/SkipsModelValidations
|
||||||
|
@conversation.update_column(:agent_last_seen_at, last_seen_at)
|
||||||
|
@conversation.update_column(:assignee_last_seen_at, last_seen_at) if update_assignee.present?
|
||||||
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
|
end
|
||||||
|
|
||||||
def set_conversation_status
|
def set_conversation_status
|
||||||
status = params[:status] == 'bot' ? 'pending' : params[:status]
|
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||||
@conversation.status = status
|
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
|
||||||
|
# status = params[:status] == 'bot' ? 'pending' : params[:status]
|
||||||
|
@conversation.status = params[:status]
|
||||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -142,31 +154,11 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_params
|
|
||||||
additional_attributes = params[:additional_attributes]&.permit! || {}
|
|
||||||
custom_attributes = params[:custom_attributes]&.permit! || {}
|
|
||||||
status = params[:status].present? ? { status: params[:status] } : {}
|
|
||||||
|
|
||||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
|
||||||
status = { status: 'pending' } if status[:status] == 'bot'
|
|
||||||
{
|
|
||||||
account_id: Current.account.id,
|
|
||||||
inbox_id: @contact_inbox.inbox_id,
|
|
||||||
contact_id: @contact_inbox.contact_id,
|
|
||||||
contact_inbox_id: @contact_inbox.id,
|
|
||||||
additional_attributes: additional_attributes,
|
|
||||||
custom_attributes: custom_attributes,
|
|
||||||
snoozed_until: params[:snoozed_until],
|
|
||||||
assignee_id: params[:assignee_id],
|
|
||||||
team_id: params[:team_id]
|
|
||||||
}.merge(status)
|
|
||||||
end
|
|
||||||
|
|
||||||
def conversation_finder
|
def conversation_finder
|
||||||
@conversation_finder ||= ConversationFinder.new(current_user, params)
|
@conversation_finder ||= ConversationFinder.new(Current.user, params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def assignee?
|
def assignee?
|
||||||
@conversation.assignee_id? && current_user == @conversation.assignee
|
@conversation.assignee_id? && Current.user == @conversation.assignee
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -113,7 +113,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
def inbox_attributes
|
def inbox_attributes
|
||||||
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved]
|
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
||||||
|
:lock_to_single_conversation]
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params(channel_attributes = [])
|
def permitted_params(channel_attributes = [])
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
before_action :check_authorization
|
|
||||||
before_action :fetch_macro, only: [:show, :update, :destroy, :execute]
|
before_action :fetch_macro, only: [:show, :update, :destroy, :execute]
|
||||||
|
before_action :check_authorization, only: [:show, :update, :destroy, :execute]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@macros = Macro.with_visibility(current_user, params)
|
@macros = Macro.with_visibility(current_user, params)
|
||||||
|
@ -55,6 +55,8 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def process_attachments
|
def process_attachments
|
||||||
actions = @macro.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
|
actions = @macro.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
|
||||||
return if actions.blank?
|
return if actions.blank?
|
||||||
|
@ -80,4 +82,8 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||||
def fetch_macro
|
def fetch_macro
|
||||||
@macro = Current.account.macros.find_by(id: params[:id])
|
@macro = Current.account.macros.find_by(id: params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_authorization
|
||||||
|
authorize(@macro) if @macro.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,6 +21,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@portal = Current.account.portals.build(portal_params)
|
@portal = Current.account.portals.build(portal_params)
|
||||||
|
@portal.custom_domain = parsed_custom_domain
|
||||||
@portal.save!
|
@portal.save!
|
||||||
process_attached_logo
|
process_attached_logo
|
||||||
end
|
end
|
||||||
|
@ -28,6 +29,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
def update
|
def update
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@portal.update!(portal_params) if params[:portal].present?
|
@portal.update!(portal_params) if params[:portal].present?
|
||||||
|
# @portal.custom_domain = parsed_custom_domain
|
||||||
process_attached_logo
|
process_attached_logo
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error e
|
Rails.logger.error e
|
||||||
|
@ -73,4 +75,9 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
def set_current_page
|
def set_current_page
|
||||||
@current_page = params[:page] || 1
|
@current_page = params[:page] || 1
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def parsed_custom_domain
|
||||||
|
domain = URI.parse(@portal.custom_domain)
|
||||||
|
domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,10 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auto_offline
|
||||||
|
@user.account_users.find_by!(account_id: auto_offline_params[:account_id]).update!(auto_offline: auto_offline_params[:auto_offline] || false)
|
||||||
|
end
|
||||||
|
|
||||||
def availability
|
def availability
|
||||||
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
|
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
|
||||||
end
|
end
|
||||||
|
@ -37,6 +41,10 @@ class Api::V1::ProfilesController < Api::BaseController
|
||||||
params.require(:profile).permit(:account_id, :availability)
|
params.require(:profile).permit(:account_id, :availability)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def auto_offline_params
|
||||||
|
params.require(:profile).permit(:account_id, :auto_offline)
|
||||||
|
end
|
||||||
|
|
||||||
def profile_params
|
def profile_params
|
||||||
params.require(:profile).permit(
|
params.require(:profile).permit(
|
||||||
:email,
|
:email,
|
||||||
|
|
|
@ -50,7 +50,9 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_name
|
def contact_name
|
||||||
params[:contact][:name] || contact_email.split('@')[0] if contact_email.present?
|
return if @contact.email.present? || @contact.phone_number.present? || @contact.identifier.present?
|
||||||
|
|
||||||
|
permitted_params.dig(:contact, :name) || (contact_email.split('@')[0] if contact_email.present?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def contact_phone_number
|
def contact_phone_number
|
||||||
|
|
|
@ -16,8 +16,7 @@ class DashboardController < ActionController::Base
|
||||||
@global_config = GlobalConfig.get(
|
@global_config = GlobalConfig.get(
|
||||||
'LOGO', 'LOGO_THUMBNAIL',
|
'LOGO', 'LOGO_THUMBNAIL',
|
||||||
'INSTALLATION_NAME',
|
'INSTALLATION_NAME',
|
||||||
'WIDGET_BRAND_URL',
|
'WIDGET_BRAND_URL', 'TERMS_URL',
|
||||||
'TERMS_URL',
|
|
||||||
'PRIVACY_URL',
|
'PRIVACY_URL',
|
||||||
'DISPLAY_MANIFEST',
|
'DISPLAY_MANIFEST',
|
||||||
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
|
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
|
||||||
|
@ -25,12 +24,12 @@ class DashboardController < ActionController::Base
|
||||||
'API_CHANNEL_NAME',
|
'API_CHANNEL_NAME',
|
||||||
'API_CHANNEL_THUMBNAIL',
|
'API_CHANNEL_THUMBNAIL',
|
||||||
'ANALYTICS_TOKEN',
|
'ANALYTICS_TOKEN',
|
||||||
'ANALYTICS_HOST',
|
|
||||||
'DIRECT_UPLOADS_ENABLED',
|
'DIRECT_UPLOADS_ENABLED',
|
||||||
'HCAPTCHA_SITE_KEY',
|
'HCAPTCHA_SITE_KEY',
|
||||||
'LOGOUT_REDIRECT_LINK',
|
'LOGOUT_REDIRECT_LINK',
|
||||||
'DISABLE_USER_PROFILE_UPDATE',
|
'DISABLE_USER_PROFILE_UPDATE',
|
||||||
'DEPLOYMENT_ENV'
|
'DEPLOYMENT_ENV',
|
||||||
|
'CSML_EDITOR_HOST'
|
||||||
).merge(app_config)
|
).merge(app_config)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
class Platform::Api::V1::AccountsController < PlatformController
|
class Platform::Api::V1::AccountsController < PlatformController
|
||||||
def create
|
def create
|
||||||
@resource = Account.new(account_params)
|
@resource = Account.create!(account_params)
|
||||||
@resource.save!
|
update_resource_features
|
||||||
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@resource.update!(account_params)
|
@resource.assign_attributes(account_params)
|
||||||
|
update_resource_features
|
||||||
|
@resource.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
def destroy
|
def destroy
|
||||||
|
@ -23,14 +25,18 @@ class Platform::Api::V1::AccountsController < PlatformController
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_params
|
def account_params
|
||||||
if permitted_params[:enabled_features]
|
permitted_params.except(:features)
|
||||||
return permitted_params.except(:enabled_features).merge(selected_feature_flags: permitted_params[:enabled_features].map(&:to_sym))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
permitted_params
|
def update_resource_features
|
||||||
|
return if permitted_params[:features].blank?
|
||||||
|
|
||||||
|
permitted_params[:features].each do |key, value|
|
||||||
|
value.present? ? @resource.enable_features(key) : @resource.disable_features(key)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.permit(:name, :locale, enabled_features: [], limits: {})
|
params.permit(:name, :locale, :domain, :support_email, :status, features: {}, limits: {}, custom_attributes: {})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,7 +7,7 @@ class Public::Api::V1::Inboxes::ContactsController < Public::Api::V1::InboxesCon
|
||||||
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||||
source_id: source_id,
|
source_id: source_id,
|
||||||
inbox: @inbox_channel.inbox,
|
inbox: @inbox_channel.inbox,
|
||||||
contact_attributes: permitted_params.except(:identifier, :identifier_hash)
|
contact_attributes: permitted_params.except(:identifier_hash)
|
||||||
).perform
|
).perform
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,6 @@ class ConversationFinder
|
||||||
filter_by_team if @team
|
filter_by_team if @team
|
||||||
filter_by_labels if params[:labels]
|
filter_by_labels if params[:labels]
|
||||||
filter_by_query if params[:q]
|
filter_by_query if params[:q]
|
||||||
filter_by_reply_status
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_inboxes
|
def set_inboxes
|
||||||
|
@ -76,12 +75,9 @@ class ConversationFinder
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_all_conversations
|
def find_all_conversations
|
||||||
if params[:conversation_type] == 'mention'
|
|
||||||
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
|
|
||||||
@conversations = current_account.conversations.where(id: conversation_ids)
|
|
||||||
else
|
|
||||||
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
|
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
|
||||||
end
|
filter_by_conversation_type if params[:conversation_type]
|
||||||
|
@conversations
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_assignee_type
|
def filter_by_assignee_type
|
||||||
|
@ -96,8 +92,15 @@ class ConversationFinder
|
||||||
@conversations
|
@conversations
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_reply_status
|
def filter_by_conversation_type
|
||||||
@conversations = @conversations.where(first_reply_created_at: nil) if params[:reply_status] == 'unattended'
|
case @params[:conversation_type]
|
||||||
|
when 'mention'
|
||||||
|
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
|
||||||
|
@conversations = @conversations.where(id: conversation_ids)
|
||||||
|
when 'unattended'
|
||||||
|
@conversations = @conversations.where(first_reply_created_at: nil)
|
||||||
|
end
|
||||||
|
@conversations
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_query
|
def filter_by_query
|
||||||
|
|
|
@ -21,7 +21,9 @@ class MessageFinder
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_messages
|
def current_messages
|
||||||
if @params[:before].present?
|
if @params[:after].present?
|
||||||
|
messages.reorder('created_at asc').where('id >= ?', @params[:before].to_i).limit(20)
|
||||||
|
elsif @params[:before].present?
|
||||||
messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse
|
messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse
|
||||||
else
|
else
|
||||||
messages.reorder('created_at desc').limit(20).reverse
|
messages.reorder('created_at desc').limit(20).reverse
|
||||||
|
|
|
@ -144,6 +144,12 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateAutoOffline(accountId, autoOffline = false) {
|
||||||
|
return axios.post(endPoints('autoOffline').url, {
|
||||||
|
profile: { account_id: accountId, auto_offline: autoOffline },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
deleteAvatar() {
|
deleteAvatar() {
|
||||||
return axios.delete(endPoints('deleteAvatar').url);
|
return axios.delete(endPoints('deleteAvatar').url);
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,6 +16,9 @@ const endPoints = {
|
||||||
availabilityUpdate: {
|
availabilityUpdate: {
|
||||||
url: '/api/v1/profile/availability',
|
url: '/api/v1/profile/availability',
|
||||||
},
|
},
|
||||||
|
autoOffline: {
|
||||||
|
url: '/api/v1/profile/auto_offline',
|
||||||
|
},
|
||||||
logout: {
|
logout: {
|
||||||
url: 'auth/sign_out',
|
url: 'auth/sign_out',
|
||||||
},
|
},
|
||||||
|
|
|
@ -68,6 +68,10 @@ class ConversationApi extends ApiClient {
|
||||||
return axios.post(`${this.url}/${id}/update_last_seen`);
|
return axios.post(`${this.url}/${id}/update_last_seen`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markMessagesUnread({ id }) {
|
||||||
|
return axios.post(`${this.url}/${id}/unread`);
|
||||||
|
}
|
||||||
|
|
||||||
toggleTyping({ conversationId, status, isPrivate }) {
|
toggleTyping({ conversationId, status, isPrivate }) {
|
||||||
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
|
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
|
||||||
typing_status: status,
|
typing_status: status,
|
||||||
|
@ -105,6 +109,16 @@ class ConversationApi extends ApiClient {
|
||||||
custom_attributes: customAttributes,
|
custom_attributes: customAttributes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchParticipants(conversationId) {
|
||||||
|
return axios.get(`${this.url}/${conversationId}/participants`);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateParticipants({ conversationId, userIds }) {
|
||||||
|
return axios.patch(`${this.url}/${conversationId}/participants`, {
|
||||||
|
user_ids: userIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ConversationApi();
|
export default new ConversationApi();
|
||||||
|
|
|
@ -13,6 +13,16 @@ class Inboxes extends ApiClient {
|
||||||
deleteInboxAvatar(inboxId) {
|
deleteInboxAvatar(inboxId) {
|
||||||
return axios.delete(`${this.url}/${inboxId}/avatar`);
|
return axios.delete(`${this.url}/${inboxId}/avatar`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAgentBot(inboxId) {
|
||||||
|
return axios.get(`${this.url}/${inboxId}/agent_bot`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAgentBot(inboxId, botId) {
|
||||||
|
return axios.post(`${this.url}/${inboxId}/set_agent_bot`, {
|
||||||
|
agent_bot: botId,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Inboxes();
|
export default new Inboxes();
|
||||||
|
|
|
@ -11,6 +11,8 @@ describe('#InboxesAPI', () => {
|
||||||
expect(inboxesAPI).toHaveProperty('update');
|
expect(inboxesAPI).toHaveProperty('update');
|
||||||
expect(inboxesAPI).toHaveProperty('delete');
|
expect(inboxesAPI).toHaveProperty('delete');
|
||||||
expect(inboxesAPI).toHaveProperty('getCampaigns');
|
expect(inboxesAPI).toHaveProperty('getCampaigns');
|
||||||
|
expect(inboxesAPI).toHaveProperty('getAgentBot');
|
||||||
|
expect(inboxesAPI).toHaveProperty('setAgentBot');
|
||||||
});
|
});
|
||||||
describeWithAPIMock('API calls', context => {
|
describeWithAPIMock('API calls', context => {
|
||||||
it('#getCampaigns', () => {
|
it('#getCampaigns', () => {
|
||||||
|
|
6
app/javascript/dashboard/api/testimonials.js
Normal file
6
app/javascript/dashboard/api/testimonials.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/* global axios */
|
||||||
|
import wootConstants from 'dashboard/constants';
|
||||||
|
|
||||||
|
export const getTestimonialContent = () => {
|
||||||
|
return axios.get(wootConstants.TESTIMONIAL_URL);
|
||||||
|
};
|
|
@ -74,8 +74,8 @@ Tahoma,
|
||||||
Arial,
|
Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
$body-antialiased: true;
|
$body-antialiased: true;
|
||||||
$global-margin: $space-one;
|
$global-margin: $space-small;
|
||||||
$global-padding: $space-one;
|
$global-padding: $space-micro;
|
||||||
$global-weight-normal: normal;
|
$global-weight-normal: normal;
|
||||||
$global-weight-bold: bold;
|
$global-weight-bold: bold;
|
||||||
$global-radius: 0;
|
$global-radius: 0;
|
||||||
|
|
|
@ -20,6 +20,24 @@
|
||||||
|
|
||||||
@include foundation-everything($flex: true);
|
@include foundation-everything($flex: true);
|
||||||
|
|
||||||
|
@include foundation-prototype-text-utilities;
|
||||||
|
@include foundation-prototype-text-transformation;
|
||||||
|
@include foundation-prototype-text-decoration;
|
||||||
|
@include foundation-prototype-font-styling;
|
||||||
|
@include foundation-prototype-list-style-type;
|
||||||
|
@include foundation-prototype-rounded;
|
||||||
|
@include foundation-prototype-bordered;
|
||||||
|
@include foundation-prototype-shadow;
|
||||||
|
@include foundation-prototype-separator;
|
||||||
|
@include foundation-prototype-overflow;
|
||||||
|
@include foundation-prototype-display;
|
||||||
|
@include foundation-prototype-position;
|
||||||
|
@include foundation-prototype-border-box;
|
||||||
|
@include foundation-prototype-border-none;
|
||||||
|
@include foundation-prototype-sizing;
|
||||||
|
@include foundation-prototype-spacing;
|
||||||
|
|
||||||
|
|
||||||
@import 'typography';
|
@import 'typography';
|
||||||
@import 'layout';
|
@import 'layout';
|
||||||
@import 'animations';
|
@import 'animations';
|
||||||
|
|
|
@ -155,12 +155,20 @@ $default-button-height: 4.0rem;
|
||||||
// Sizes
|
// Sizes
|
||||||
&.tiny {
|
&.tiny {
|
||||||
height: var(--space-medium);
|
height: var(--space-medium);
|
||||||
|
|
||||||
|
.icon+.button__content {
|
||||||
|
padding-left: var(--space-micro);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.small {
|
&.small {
|
||||||
height: var(--space-large);
|
height: var(--space-large);
|
||||||
padding-bottom: var(--space-smaller);
|
padding-bottom: var(--space-smaller);
|
||||||
padding-top: var(--space-smaller);
|
padding-top: var(--space-smaller);
|
||||||
|
|
||||||
|
.icon+.button__content {
|
||||||
|
padding-left: var(--space-smaller);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.large {
|
&.large {
|
||||||
|
@ -190,6 +198,10 @@ $default-button-height: 4.0rem;
|
||||||
height: auto;
|
height: auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,12 +59,8 @@
|
||||||
|
|
||||||
.hamburger--menu {
|
.hamburger--menu {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: none;
|
|
||||||
margin-right: $space-normal;
|
|
||||||
|
|
||||||
@media screen and (max-width: 1200px) {
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
margin-right: $space-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header--icon {
|
.header--icon {
|
||||||
|
|
|
@ -102,6 +102,7 @@
|
||||||
@assign-agent="onAssignAgent"
|
@assign-agent="onAssignAgent"
|
||||||
@update-conversations="onUpdateConversations"
|
@update-conversations="onUpdateConversations"
|
||||||
@assign-labels="onAssignLabels"
|
@assign-labels="onAssignLabels"
|
||||||
|
@assign-team="onAssignTeamsForBulk"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
ref="activeConversation"
|
ref="activeConversation"
|
||||||
|
@ -125,6 +126,7 @@
|
||||||
@assign-label="onAssignLabels"
|
@assign-label="onAssignLabels"
|
||||||
@update-conversation-status="toggleConversationStatus"
|
@update-conversation-status="toggleConversationStatus"
|
||||||
@context-menu-toggle="onContextMenuToggle"
|
@context-menu-toggle="onContextMenuToggle"
|
||||||
|
@mark-as-unread="markAsUnread"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="chatListLoading" class="text-center">
|
<div v-if="chatListLoading" class="text-center">
|
||||||
|
@ -184,6 +186,11 @@ import {
|
||||||
hasPressedAltAndJKey,
|
hasPressedAltAndJKey,
|
||||||
hasPressedAltAndKKey,
|
hasPressedAltAndKKey,
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
|
import { conversationListPageURL } from '../helper/URLHelper';
|
||||||
|
import {
|
||||||
|
isOnMentionsView,
|
||||||
|
isOnUnattendedView,
|
||||||
|
} from '../store/modules/conversations/helpers/actionHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -332,14 +339,15 @@ export default {
|
||||||
status: this.activeStatus,
|
status: this.activeStatus,
|
||||||
page: this.currentPage + 1,
|
page: this.currentPage + 1,
|
||||||
labels: this.label ? [this.label] : undefined,
|
labels: this.label ? [this.label] : undefined,
|
||||||
teamId: this.teamId ? this.teamId : undefined,
|
teamId: this.teamId || undefined,
|
||||||
conversationType: this.conversationType
|
conversationType: this.conversationType || undefined,
|
||||||
? this.conversationType
|
|
||||||
: undefined,
|
|
||||||
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
|
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
pageTitle() {
|
pageTitle() {
|
||||||
|
if (this.hasAppliedFilters) {
|
||||||
|
return this.$t('CHAT_LIST.TAB_HEADING');
|
||||||
|
}
|
||||||
if (this.inbox.name) {
|
if (this.inbox.name) {
|
||||||
return this.inbox.name;
|
return this.inbox.name;
|
||||||
}
|
}
|
||||||
|
@ -352,6 +360,9 @@ export default {
|
||||||
if (this.conversationType === 'mention') {
|
if (this.conversationType === 'mention') {
|
||||||
return this.$t('CHAT_LIST.MENTION_HEADING');
|
return this.$t('CHAT_LIST.MENTION_HEADING');
|
||||||
}
|
}
|
||||||
|
if (this.conversationType === 'unattended') {
|
||||||
|
return this.$t('CHAT_LIST.UNATTENDED_HEADING');
|
||||||
|
}
|
||||||
if (this.hasActiveFolders) {
|
if (this.hasActiveFolders) {
|
||||||
return this.activeFolder.name;
|
return this.activeFolder.name;
|
||||||
}
|
}
|
||||||
|
@ -431,9 +442,6 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onApplyFilter(payload) {
|
onApplyFilter(payload) {
|
||||||
if (this.$route.name !== 'home') {
|
|
||||||
this.$router.push({ name: 'home' });
|
|
||||||
}
|
|
||||||
this.resetBulkActions();
|
this.resetBulkActions();
|
||||||
this.foldersQuery = filterQueryGenerator(payload);
|
this.foldersQuery = filterQueryGenerator(payload);
|
||||||
this.$store.dispatch('conversationPage/reset');
|
this.$store.dispatch('conversationPage/reset');
|
||||||
|
@ -636,6 +644,35 @@ export default {
|
||||||
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
|
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async markAsUnread(conversationId) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('markMessagesUnread', {
|
||||||
|
id: conversationId,
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
params: { accountId, inbox_id: inboxId, label, teamId },
|
||||||
|
name,
|
||||||
|
} = this.$route;
|
||||||
|
let conversationType = '';
|
||||||
|
if (isOnMentionsView({ route: { name } })) {
|
||||||
|
conversationType = 'mention';
|
||||||
|
} else if (isOnUnattendedView({ route: { name } })) {
|
||||||
|
conversationType = 'unattended';
|
||||||
|
}
|
||||||
|
this.$router.push(
|
||||||
|
conversationListPageURL({
|
||||||
|
accountId,
|
||||||
|
conversationType: conversationType,
|
||||||
|
customViewId: this.foldersId,
|
||||||
|
inboxId,
|
||||||
|
label,
|
||||||
|
teamId,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
}
|
||||||
|
},
|
||||||
async onAssignTeam(team, conversationId = null) {
|
async onAssignTeam(team, conversationId = null) {
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('assignTeam', {
|
await this.$store.dispatch('assignTeam', {
|
||||||
|
@ -685,6 +722,21 @@ export default {
|
||||||
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
|
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async onAssignTeamsForBulk(team) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('bulkActions/process', {
|
||||||
|
type: 'Conversation',
|
||||||
|
ids: this.selectedConversations,
|
||||||
|
fields: {
|
||||||
|
team_id: team.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.selectedConversations = [];
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
|
||||||
|
} catch (err) {
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
|
||||||
|
}
|
||||||
|
},
|
||||||
async onUpdateConversations(status) {
|
async onUpdateConversations(status) {
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('bulkActions/process', {
|
await this.$store.dispatch('bulkActions/process', {
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<button @click="onMenuItemClick">
|
<woot-button
|
||||||
<fluent-icon class="hamburger--menu" icon="list" />
|
size="small"
|
||||||
</button>
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="list"
|
||||||
|
class="toggle-sidebar"
|
||||||
|
@click="onMenuItemClick"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -16,13 +21,8 @@ export default {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.hamburger--menu {
|
.toggle-sidebar {
|
||||||
cursor: pointer;
|
margin-right: var(--space-small);
|
||||||
display: none;
|
margin-left: var(--space-minus-small);
|
||||||
margin-right: var(--space-normal);
|
|
||||||
|
|
||||||
@media screen and (max-width: 1200px) {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,12 +18,35 @@
|
||||||
</woot-button>
|
</woot-button>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
<woot-dropdown-divider />
|
<woot-dropdown-divider />
|
||||||
|
<woot-dropdown-item class="auto-offline--toggle">
|
||||||
|
<div class="info-wrap">
|
||||||
|
<fluent-icon
|
||||||
|
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
|
||||||
|
icon="info"
|
||||||
|
size="14"
|
||||||
|
class="info-icon"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span class="auto-offline--text">
|
||||||
|
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<woot-switch
|
||||||
|
size="small"
|
||||||
|
class="auto-offline--switch"
|
||||||
|
:value="currentUserAutoOffline"
|
||||||
|
@input="updateAutoOffline"
|
||||||
|
/>
|
||||||
|
</woot-dropdown-item>
|
||||||
|
<woot-dropdown-divider />
|
||||||
</woot-dropdown-menu>
|
</woot-dropdown-menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
|
||||||
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
|
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
|
||||||
|
@ -41,7 +64,7 @@ export default {
|
||||||
AvailabilityStatusBadge,
|
AvailabilityStatusBadge,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [clickaway],
|
mixins: [clickaway, alertMixin],
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -54,6 +77,7 @@ export default {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
getCurrentUserAvailability: 'getCurrentUserAvailability',
|
||||||
currentAccountId: 'getCurrentAccountId',
|
currentAccountId: 'getCurrentAccountId',
|
||||||
|
currentUserAutoOffline: 'getCurrentUserAutoOffline',
|
||||||
}),
|
}),
|
||||||
availabilityDisplayLabel() {
|
availabilityDisplayLabel() {
|
||||||
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
|
||||||
|
@ -85,21 +109,30 @@ export default {
|
||||||
closeStatusMenu() {
|
closeStatusMenu() {
|
||||||
this.isStatusMenuOpened = false;
|
this.isStatusMenuOpened = false;
|
||||||
},
|
},
|
||||||
|
updateAutoOffline(autoOffline) {
|
||||||
|
this.$store.dispatch('updateAutoOffline', {
|
||||||
|
accountId: this.currentAccountId,
|
||||||
|
autoOffline,
|
||||||
|
});
|
||||||
|
},
|
||||||
changeAvailabilityStatus(availability) {
|
changeAvailabilityStatus(availability) {
|
||||||
const accountId = this.currentAccountId;
|
|
||||||
if (this.isUpdating) {
|
if (this.isUpdating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.isUpdating = true;
|
this.isUpdating = true;
|
||||||
this.$store
|
try {
|
||||||
.dispatch('updateAvailability', {
|
this.$store.dispatch('updateAvailability', {
|
||||||
availability: availability,
|
availability,
|
||||||
account_id: accountId,
|
account_id: this.currentAccountId,
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.isUpdating = false;
|
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.showAlert(
|
||||||
|
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isUpdating = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -143,4 +176,32 @@ export default {
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.auto-offline--toggle {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--space-smaller) 0 var(--space-smaller) var(--space-small);
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.info-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-offline--switch {
|
||||||
|
margin: -1px var(--space-micro) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-offline--text {
|
||||||
|
margin: 0 var(--space-smaller);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
color: var(--s-700);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -16,6 +16,8 @@ const conversations = accountId => ({
|
||||||
'conversation_through_mentions',
|
'conversation_through_mentions',
|
||||||
'folder_conversations',
|
'folder_conversations',
|
||||||
'conversations_through_folders',
|
'conversations_through_folders',
|
||||||
|
'conversation_unattended',
|
||||||
|
'conversation_through_unattended',
|
||||||
],
|
],
|
||||||
menuItems: [
|
menuItems: [
|
||||||
{
|
{
|
||||||
|
@ -33,6 +35,13 @@ const conversations = accountId => ({
|
||||||
toState: frontendURL(`accounts/${accountId}/mentions/conversations`),
|
toState: frontendURL(`accounts/${accountId}/mentions/conversations`),
|
||||||
toStateName: 'conversation_mentions',
|
toStateName: 'conversation_mentions',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'mail-unread',
|
||||||
|
label: 'UNATTENDED_CONVERSATIONS',
|
||||||
|
key: 'conversation_unattended',
|
||||||
|
toState: frontendURL(`accounts/${accountId}/unattended/conversations`),
|
||||||
|
toStateName: 'conversation_unattended',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { FEATURE_FLAGS } from '../../../../featureFlags';
|
||||||
import { frontendURL } from '../../../../helper/URLHelper';
|
import { frontendURL } from '../../../../helper/URLHelper';
|
||||||
|
|
||||||
const primaryMenuItems = accountId => [
|
const primaryMenuItems = accountId => [
|
||||||
|
@ -13,6 +14,7 @@ const primaryMenuItems = accountId => [
|
||||||
icon: 'book-contacts',
|
icon: 'book-contacts',
|
||||||
key: 'contacts',
|
key: 'contacts',
|
||||||
label: 'CONTACTS',
|
label: 'CONTACTS',
|
||||||
|
featureFlag: FEATURE_FLAGS.CRM,
|
||||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||||
toStateName: 'contacts_dashboard',
|
toStateName: 'contacts_dashboard',
|
||||||
roles: ['administrator', 'agent'],
|
roles: ['administrator', 'agent'],
|
||||||
|
@ -21,6 +23,7 @@ const primaryMenuItems = accountId => [
|
||||||
icon: 'arrow-trending-lines',
|
icon: 'arrow-trending-lines',
|
||||||
key: 'reports',
|
key: 'reports',
|
||||||
label: 'REPORTS',
|
label: 'REPORTS',
|
||||||
|
featureFlag: FEATURE_FLAGS.REPORTS,
|
||||||
toState: frontendURL(`accounts/${accountId}/reports`),
|
toState: frontendURL(`accounts/${accountId}/reports`),
|
||||||
toStateName: 'settings_account_reports',
|
toStateName: 'settings_account_reports',
|
||||||
roles: ['administrator'],
|
roles: ['administrator'],
|
||||||
|
@ -29,6 +32,7 @@ const primaryMenuItems = accountId => [
|
||||||
icon: 'megaphone',
|
icon: 'megaphone',
|
||||||
key: 'campaigns',
|
key: 'campaigns',
|
||||||
label: 'CAMPAIGNS',
|
label: 'CAMPAIGNS',
|
||||||
|
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
|
||||||
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
toState: frontendURL(`accounts/${accountId}/campaigns`),
|
||||||
toStateName: 'settings_account_campaigns',
|
toStateName: 'settings_account_campaigns',
|
||||||
roles: ['administrator'],
|
roles: ['administrator'],
|
||||||
|
@ -37,7 +41,7 @@ const primaryMenuItems = accountId => [
|
||||||
icon: 'library',
|
icon: 'library',
|
||||||
key: 'helpcenter',
|
key: 'helpcenter',
|
||||||
label: 'HELP_CENTER.TITLE',
|
label: 'HELP_CENTER.TITLE',
|
||||||
featureFlag: 'help_center',
|
featureFlag: FEATURE_FLAGS.HELP_CENTER,
|
||||||
toState: frontendURL(`accounts/${accountId}/portals`),
|
toState: frontendURL(`accounts/${accountId}/portals`),
|
||||||
toStateName: 'default_portal_articles',
|
toStateName: 'default_portal_articles',
|
||||||
roles: ['administrator'],
|
roles: ['administrator'],
|
||||||
|
|
|
@ -102,6 +102,7 @@ const settings = accountId => ({
|
||||||
label: 'AGENT_BOTS',
|
label: 'AGENT_BOTS',
|
||||||
beta: true,
|
beta: true,
|
||||||
hasSubMenu: false,
|
hasSubMenu: false,
|
||||||
|
globalConfigFlag: 'csmlEditorHost',
|
||||||
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
|
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
|
||||||
toStateName: 'agent_bots',
|
toStateName: 'agent_bots',
|
||||||
featureFlag: FEATURE_FLAGS.AGENT_BOTS,
|
featureFlag: FEATURE_FLAGS.AGENT_BOTS,
|
||||||
|
|
|
@ -61,6 +61,24 @@
|
||||||
</a>
|
</a>
|
||||||
</router-link>
|
</router-link>
|
||||||
</woot-dropdown-item>
|
</woot-dropdown-item>
|
||||||
|
<woot-dropdown-item v-if="currentUser.type === 'SuperAdmin'">
|
||||||
|
<a
|
||||||
|
href="/super_admin"
|
||||||
|
class="button small clear secondary"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener nofollow noreferrer"
|
||||||
|
@click="$emit('close')"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
icon="content-settings"
|
||||||
|
size="14"
|
||||||
|
class="icon icon--font"
|
||||||
|
/>
|
||||||
|
<span class="button__content">
|
||||||
|
{{ $t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE') }}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</woot-dropdown-item>
|
||||||
<woot-dropdown-item>
|
<woot-dropdown-item>
|
||||||
<woot-button
|
<woot-button
|
||||||
variant="clear"
|
variant="clear"
|
||||||
|
@ -135,7 +153,7 @@ export default {
|
||||||
.dropdown-pane {
|
.dropdown-pane {
|
||||||
left: var(--space-slab);
|
left: var(--space-slab);
|
||||||
bottom: var(--space-larger);
|
bottom: var(--space-larger);
|
||||||
min-width: 16.8rem;
|
min-width: 22rem;
|
||||||
z-index: var(--z-index-much-higher);
|
z-index: var(--z-index-low);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -261,14 +261,7 @@ export default {
|
||||||
width: 20rem;
|
width: 20rem;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
@include breakpoint(xlarge down) {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
@include breakpoint(xlarge up) {
|
|
||||||
position: unset;
|
position: unset;
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
|
@ -112,6 +112,7 @@ $label-badge-size: var(--space-slab);
|
||||||
padding: var(--space-smaller) var(--space-smaller);
|
padding: var(--space-smaller) var(--space-smaller);
|
||||||
margin: var(--space-smaller) 0;
|
margin: var(--space-smaller) 0;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
line-height: 1.2;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--s-25);
|
background: var(--s-25);
|
||||||
|
@ -135,8 +136,6 @@ $label-badge-size: var(--space-slab);
|
||||||
|
|
||||||
.menu-label {
|
.menu-label {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.inbox-icon {
|
.inbox-icon {
|
||||||
|
|
|
@ -87,6 +87,10 @@ import {
|
||||||
} from 'dashboard/helper/inbox';
|
} from 'dashboard/helper/inbox';
|
||||||
|
|
||||||
import SecondaryChildNavItem from './SecondaryChildNavItem';
|
import SecondaryChildNavItem from './SecondaryChildNavItem';
|
||||||
|
import {
|
||||||
|
isOnMentionsView,
|
||||||
|
isOnUnattendedView,
|
||||||
|
} from '../../../store/modules/conversations/helpers/actionHelpers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { SecondaryChildNavItem },
|
components: { SecondaryChildNavItem },
|
||||||
|
@ -102,32 +106,48 @@ export default {
|
||||||
activeInbox: 'getSelectedInbox',
|
activeInbox: 'getSelectedInbox',
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
|
||||||
|
globalConfig: 'globalConfig/get',
|
||||||
}),
|
}),
|
||||||
hasSubMenu() {
|
hasSubMenu() {
|
||||||
return !!this.menuItem.children;
|
return !!this.menuItem.children;
|
||||||
},
|
},
|
||||||
isMenuItemVisible() {
|
isMenuItemVisible() {
|
||||||
if (!this.menuItem.featureFlag) {
|
if (this.menuItem.globalConfigFlag) {
|
||||||
return true;
|
return !!this.globalConfig[this.menuItem.globalConfigFlag];
|
||||||
}
|
}
|
||||||
|
if (this.menuItem.featureFlag) {
|
||||||
return this.isFeatureEnabledonAccount(
|
return this.isFeatureEnabledonAccount(
|
||||||
this.accountId,
|
this.accountId,
|
||||||
this.menuItem.featureFlag
|
this.menuItem.featureFlag
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
isInboxConversation() {
|
isAllConversations() {
|
||||||
return (
|
return (
|
||||||
this.$store.state.route.name === 'inbox_conversation' &&
|
this.$store.state.route.name === 'inbox_conversation' &&
|
||||||
this.menuItem.toStateName === 'home'
|
this.menuItem.toStateName === 'home'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
isMentions() {
|
||||||
|
return (
|
||||||
|
isOnMentionsView({ route: this.$route }) &&
|
||||||
|
this.menuItem.toStateName === 'conversation_mentions'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
isUnattended() {
|
||||||
|
return (
|
||||||
|
isOnUnattendedView({ route: this.$route }) &&
|
||||||
|
this.menuItem.toStateName === 'conversation_unattended'
|
||||||
|
);
|
||||||
|
},
|
||||||
isTeamsSettings() {
|
isTeamsSettings() {
|
||||||
return (
|
return (
|
||||||
this.$store.state.route.name === 'settings_teams_edit' &&
|
this.$store.state.route.name === 'settings_teams_edit' &&
|
||||||
this.menuItem.toStateName === 'settings_teams_list'
|
this.menuItem.toStateName === 'settings_teams_list'
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
isInboxsSettings() {
|
isInboxSettings() {
|
||||||
return (
|
return (
|
||||||
this.$store.state.route.name === 'settings_inbox_show' &&
|
this.$store.state.route.name === 'settings_inbox_show' &&
|
||||||
this.menuItem.toStateName === 'settings_inbox_list'
|
this.menuItem.toStateName === 'settings_inbox_list'
|
||||||
|
@ -150,14 +170,20 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computedClass() {
|
computedClass() {
|
||||||
// If active Inbox is present
|
// If active inbox is present, do not highlight conversations
|
||||||
// donot highlight conversations
|
|
||||||
if (this.activeInbox) return ' ';
|
if (this.activeInbox) return ' ';
|
||||||
|
if (
|
||||||
|
this.isAllConversations ||
|
||||||
|
this.isMentions ||
|
||||||
|
this.isUnattended ||
|
||||||
|
this.isCurrentRoute
|
||||||
|
) {
|
||||||
|
return 'is-active';
|
||||||
|
}
|
||||||
if (this.hasSubMenu) {
|
if (this.hasSubMenu) {
|
||||||
if (
|
if (
|
||||||
this.isInboxConversation ||
|
|
||||||
this.isTeamsSettings ||
|
this.isTeamsSettings ||
|
||||||
this.isInboxsSettings ||
|
this.isInboxSettings ||
|
||||||
this.isIntegrationsSettings ||
|
this.isIntegrationsSettings ||
|
||||||
this.isApplicationsSettings
|
this.isApplicationsSettings
|
||||||
) {
|
) {
|
||||||
|
@ -166,10 +192,6 @@ export default {
|
||||||
return ' ';
|
return ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isCurrentRoute) {
|
|
||||||
return 'is-active';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`SidemenuIcon matches snapshot 1`] = `
|
exports[`SidemenuIcon matches snapshot 1`] = `
|
||||||
<button>
|
<woot-button
|
||||||
<fluent-icon
|
class="toggle-sidebar"
|
||||||
class="hamburger--menu"
|
color-scheme="secondary"
|
||||||
icon="list"
|
icon="list"
|
||||||
|
size="small"
|
||||||
|
variant="clear"
|
||||||
/>
|
/>
|
||||||
</button>
|
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<fluent-icon :icon="icon" size="12" class="label--icon" />
|
<fluent-icon :icon="icon" size="12" class="label--icon" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="variant === 'smooth'"
|
v-if="variant === 'smooth' && title && !icon"
|
||||||
:style="{ background: color }"
|
:style="{ background: color }"
|
||||||
class="label-color-dot"
|
class="label-color-dot"
|
||||||
/>
|
/>
|
||||||
|
@ -117,14 +117,16 @@ export default {
|
||||||
height: var(--space-medium);
|
height: var(--space-medium);
|
||||||
|
|
||||||
&.small {
|
&.small {
|
||||||
font-size: var(--font-size-micro);
|
font-size: var(--font-size-mini);
|
||||||
padding: var(--space-micro) var(--space-smaller);
|
padding: var(--space-micro) var(--space-smaller);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
letter-spacing: 0.15px;
|
height: var(--space-two);
|
||||||
}
|
}
|
||||||
|
|
||||||
.label--icon {
|
.label--icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.label-color-dot {
|
||||||
margin-right: var(--space-smaller);
|
margin-right: var(--space-smaller);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,8 +201,8 @@ export default {
|
||||||
|
|
||||||
&.smooth {
|
&.smooth {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 1px solid var(--s-75);
|
border: 1px solid var(--s-100);
|
||||||
color: var(--s-800);
|
color: var(--s-700);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,14 +223,22 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-action--button {
|
.label-action--button {
|
||||||
margin-bottom: var(--space-minus-micro);
|
display: flex;
|
||||||
|
margin-right: var(--space-smaller);
|
||||||
}
|
}
|
||||||
|
|
||||||
.label-color-dot {
|
.label-color-dot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: var(--space-one);
|
width: var(--space-slab);
|
||||||
height: var(--space-one);
|
height: var(--space-slab);
|
||||||
border-radius: var(--border-radius-small);
|
border-radius: var(--border-radius-small);
|
||||||
margin-right: var(--space-smaller);
|
margin-right: var(--space-smaller);
|
||||||
|
box-shadow: var(--shadow-small);
|
||||||
|
}
|
||||||
|
.label.small .label-color-dot {
|
||||||
|
width: var(--space-small);
|
||||||
|
height: var(--space-small);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
box-shadow: var(--shadow-small);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="toggle-button"
|
class="toggle-button"
|
||||||
:class="{ active: value }"
|
:class="{ active: value, small: size === 'small' }"
|
||||||
role="switch"
|
role="switch"
|
||||||
:aria-checked="value.toString()"
|
:aria-checked="value.toString()"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
|
@ -15,6 +15,7 @@
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
value: { type: Boolean, default: false },
|
value: { type: Boolean, default: false },
|
||||||
|
size: { type: String, default: '' },
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onClick() {
|
onClick() {
|
||||||
|
@ -45,6 +46,20 @@ export default {
|
||||||
background-color: var(--w-500);
|
background-color: var(--w-500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
width: 22px;
|
||||||
|
height: 14px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
height: var(--space-one);
|
||||||
|
width: var(--space-one);
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
transform: translate(var(--space-small), var(--space-zero));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
--space-one-point-five: 1.5rem;
|
--space-one-point-five: 1.5rem;
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
|
|
|
@ -18,14 +18,32 @@
|
||||||
<div v-if="showActionInput" class="filter__answer--wrap">
|
<div v-if="showActionInput" class="filter__answer--wrap">
|
||||||
<div v-if="inputType">
|
<div v-if="inputType">
|
||||||
<div
|
<div
|
||||||
v-if="inputType === 'multi_select'"
|
v-if="inputType === 'search_select'"
|
||||||
class="multiselect-wrap--small"
|
class="multiselect-wrap--small"
|
||||||
>
|
>
|
||||||
<multiselect
|
<multiselect
|
||||||
v-model="action_params"
|
v-model="action_params"
|
||||||
track-by="id"
|
track-by="id"
|
||||||
label="name"
|
label="name"
|
||||||
:placeholder="'Select'"
|
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
||||||
|
selected-label
|
||||||
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
deselect-label=""
|
||||||
|
:max-height="160"
|
||||||
|
:options="dropdownValues"
|
||||||
|
:allow-empty="false"
|
||||||
|
:option-height="104"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="inputType === 'multi_select'"
|
||||||
|
class="multiselect-wrap--small"
|
||||||
|
>
|
||||||
|
<multiselect
|
||||||
|
v-model="action_params"
|
||||||
|
track-by="id"
|
||||||
|
label="name"
|
||||||
|
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
selected-label
|
selected-label
|
||||||
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
|
||||||
|
@ -33,6 +51,7 @@
|
||||||
:max-height="160"
|
:max-height="160"
|
||||||
:options="dropdownValues"
|
:options="dropdownValues"
|
||||||
:allow-empty="false"
|
:allow-empty="false"
|
||||||
|
:option-height="104"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
|
@ -260,6 +279,6 @@ export default {
|
||||||
margin-bottom: var(--space-zero);
|
margin-bottom: var(--space-zero);
|
||||||
}
|
}
|
||||||
.action-message {
|
.action-message {
|
||||||
margin: var(--space-small) 0 0;
|
margin: var(--space-small) var(--space-zero) var(--space-zero);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -48,5 +48,6 @@ export default {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-image: linear-gradient(to top, var(--w-100) 0%, var(--w-75) 100%);
|
background-image: linear-gradient(to top, var(--w-100) 0%, var(--w-75) 100%);
|
||||||
color: var(--w-600);
|
color: var(--w-600);
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -67,6 +67,9 @@ export default {
|
||||||
if (Object.keys(this.enabledFeatures).length === 0) {
|
if (Object.keys(this.enabledFeatures).length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (key === 'website') {
|
||||||
|
return this.enabledFeatures.channel_website;
|
||||||
|
}
|
||||||
if (key === 'facebook') {
|
if (key === 'facebook') {
|
||||||
return this.enabledFeatures.channel_facebook;
|
return this.enabledFeatures.channel_facebook;
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,6 +61,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.colorpicker--selected {
|
.colorpicker--selected {
|
||||||
|
border: 1px solid var(--color-border-light);
|
||||||
border-radius: $space-smaller;
|
border-radius: $space-smaller;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: $space-large;
|
height: $space-large;
|
||||||
|
|
|
@ -5,6 +5,11 @@
|
||||||
:key="index"
|
:key="index"
|
||||||
class="dashboard-app--list"
|
class="dashboard-app--list"
|
||||||
>
|
>
|
||||||
|
<loading-state
|
||||||
|
v-if="iframeLoading"
|
||||||
|
:message="$t('DASHBOARD_APPS.LOADING_MESSAGE')"
|
||||||
|
class="dashboard-app_loading-container"
|
||||||
|
/>
|
||||||
<iframe
|
<iframe
|
||||||
v-if="configItem.type === 'frame' && configItem.url"
|
v-if="configItem.type === 'frame' && configItem.url"
|
||||||
:id="`dashboard-app--frame-${index}`"
|
:id="`dashboard-app--frame-${index}`"
|
||||||
|
@ -16,7 +21,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import LoadingState from 'dashboard/components/widgets/LoadingState';
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
LoadingState,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
config: {
|
config: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -27,16 +36,26 @@ export default {
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
iframeLoading: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
dashboardAppContext() {
|
dashboardAppContext() {
|
||||||
return {
|
return {
|
||||||
conversation: this.currentChat,
|
conversation: this.currentChat,
|
||||||
contact: this.$store.getters['contacts/getContact'](this.contactId),
|
contact: this.$store.getters['contacts/getContact'](this.contactId),
|
||||||
|
currentAgent: this.currentAgent,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
contactId() {
|
contactId() {
|
||||||
return this.currentChat?.meta?.sender?.id;
|
return this.currentChat?.meta?.sender?.id;
|
||||||
},
|
},
|
||||||
|
currentAgent() {
|
||||||
|
const { id, name, email } = this.$store.getters.getCurrentUser;
|
||||||
|
return { id, name, email };
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -57,6 +76,7 @@ export default {
|
||||||
);
|
);
|
||||||
const eventData = { event: 'appContext', data: this.dashboardAppContext };
|
const eventData = { event: 'appContext', data: this.dashboardAppContext };
|
||||||
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
|
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
|
||||||
|
this.iframeLoading = false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -73,4 +93,11 @@ export default {
|
||||||
.dashboard-app--list iframe {
|
.dashboard-app--list iframe {
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
.dashboard-app_loading-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
v-for="attribute in filterAttributes"
|
v-for="attribute in filterAttributes"
|
||||||
:key="attribute.key"
|
:key="attribute.key"
|
||||||
:value="attribute.key"
|
:value="attribute.key"
|
||||||
|
:disabled="attribute.disabled"
|
||||||
>
|
>
|
||||||
{{ attribute.name }}
|
{{ attribute.name }}
|
||||||
</option>
|
</option>
|
||||||
|
@ -173,6 +174,10 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
customAttributeType: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
attributeKey: {
|
attributeKey: {
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<span>
|
<span>
|
||||||
{{ textToBeDisplayed }}
|
{{ textToBeDisplayed }}
|
||||||
<button class="show-more--button" @click="toggleShowMore">
|
<button
|
||||||
|
v-if="text.length > limit"
|
||||||
|
class="show-more--button"
|
||||||
|
@click="toggleShowMore"
|
||||||
|
>
|
||||||
{{ buttonLabel }}
|
{{ buttonLabel }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
@ -25,7 +29,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
textToBeDisplayed() {
|
textToBeDisplayed() {
|
||||||
if (this.showMore) {
|
if (this.showMore || this.text.length <= this.limit) {
|
||||||
return this.text;
|
return this.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,13 +10,14 @@ describe('Thumbnail.vue', () => {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
hasImageLoaded: true,
|
||||||
imgError: false,
|
imgError: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(wrapper.find('.user-thumbnail').exists()).toBe(true);
|
expect(wrapper.find('.user-thumbnail').exists()).toBe(true);
|
||||||
const avatarComponent = wrapper.findComponent(Avatar);
|
const avatarComponent = wrapper.findComponent(Avatar);
|
||||||
expect(avatarComponent.exists()).toBe(false);
|
expect(avatarComponent.isVisible()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render the avatar component if invalid image is passed', () => {
|
it('should render the avatar component if invalid image is passed', () => {
|
||||||
|
@ -26,13 +27,14 @@ describe('Thumbnail.vue', () => {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
hasImageLoaded: true,
|
||||||
imgError: true,
|
imgError: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(wrapper.find('.avatar-container').exists()).toBe(true);
|
expect(wrapper.find('#image').exists()).toBe(false);
|
||||||
const avatarComponent = wrapper.findComponent(Avatar);
|
const avatarComponent = wrapper.findComponent(Avatar);
|
||||||
expect(avatarComponent.exists()).toBe(true);
|
expect(avatarComponent.isVisible()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should the initial of the name if no image is passed', () => {
|
it('should the initial of the name if no image is passed', () => {
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="thumbnailBoxClass" :style="{ height: size, width: size }">
|
<div
|
||||||
|
:class="thumbnailBoxClass"
|
||||||
|
:style="{ height: size, width: size }"
|
||||||
|
:title="title"
|
||||||
|
>
|
||||||
|
<!-- Using v-show instead of v-if to avoid flickering as v-if removes dom elements. -->
|
||||||
<img
|
<img
|
||||||
v-if="!imgError && src"
|
v-show="shouldShowImage"
|
||||||
:src="src"
|
:src="src"
|
||||||
:class="thumbnailClass"
|
:class="thumbnailClass"
|
||||||
|
@load="onImgLoad"
|
||||||
@error="onImgError"
|
@error="onImgError"
|
||||||
/>
|
/>
|
||||||
<Avatar
|
<Avatar
|
||||||
v-else
|
v-show="!shouldShowImage"
|
||||||
:username="userNameWithoutEmoji"
|
:username="userNameWithoutEmoji"
|
||||||
:class="thumbnailClass"
|
:class="thumbnailClass"
|
||||||
:size="avatarSize"
|
:size="avatarSize"
|
||||||
|
@ -70,6 +76,10 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
variant: {
|
variant: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'circle',
|
default: 'circle',
|
||||||
|
@ -77,6 +87,7 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
hasImageLoaded: false,
|
||||||
imgError: false,
|
imgError: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -124,6 +135,15 @@ export default {
|
||||||
const boxClass = this.variant === 'circle' ? 'is-rounded' : '';
|
const boxClass = this.variant === 'circle' ? 'is-rounded' : '';
|
||||||
return `user-thumbnail-box ${boxClass}`;
|
return `user-thumbnail-box ${boxClass}`;
|
||||||
},
|
},
|
||||||
|
shouldShowImage() {
|
||||||
|
if (!this.src) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.hasImageLoaded) {
|
||||||
|
return !this.imgError;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
src(value, oldValue) {
|
src(value, oldValue) {
|
||||||
|
@ -136,6 +156,9 @@ export default {
|
||||||
onImgError() {
|
onImgError() {
|
||||||
this.imgError = true;
|
this.imgError = true;
|
||||||
},
|
},
|
||||||
|
onImgLoad() {
|
||||||
|
this.hasImageLoaded = true;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -159,6 +182,7 @@ export default {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
vertical-align: initial;
|
||||||
|
|
||||||
&.border {
|
&.border {
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
|
|
|
@ -3,11 +3,13 @@
|
||||||
<thumbnail
|
<thumbnail
|
||||||
v-for="user in usersList"
|
v-for="user in usersList"
|
||||||
:key="user.id"
|
:key="user.id"
|
||||||
|
v-tooltip="user.name"
|
||||||
|
:title="user.name"
|
||||||
:src="user.thumbnail"
|
:src="user.thumbnail"
|
||||||
:username="user.name"
|
:username="user.name"
|
||||||
:has-border="true"
|
:has-border="true"
|
||||||
:size="size"
|
:size="size"
|
||||||
class="overlapping-thumbnail"
|
:class="`overlapping-thumbnail gap-${gap}`"
|
||||||
/>
|
/>
|
||||||
<span v-if="showMoreThumbnailsCount" class="thumbnail-more-text">
|
<span v-if="showMoreThumbnailsCount" class="thumbnail-more-text">
|
||||||
{{ moreThumbnailsText }}
|
{{ moreThumbnailsText }}
|
||||||
|
@ -38,6 +40,14 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
gap: {
|
||||||
|
type: String,
|
||||||
|
default: 'normal',
|
||||||
|
validator(value) {
|
||||||
|
// The value must match one of these strings
|
||||||
|
return ['normal', '', 'tight'].includes(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -52,6 +62,10 @@ export default {
|
||||||
box-shadow: var(--shadow-small);
|
box-shadow: var(--shadow-small);
|
||||||
|
|
||||||
&:not(:first-child) {
|
&:not(:first-child) {
|
||||||
|
margin-left: var(--space-minus-smaller);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gap-tight {
|
||||||
margin-left: var(--space-minus-small);
|
margin-left: var(--space-minus-small);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,11 @@ import 'videojs-record/dist/css/videojs.record.css';
|
||||||
|
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
|
|
||||||
import inboxMixin from '../../../../shared/mixins/inboxMixin';
|
|
||||||
import alertMixin from '../../../../shared/mixins/alertMixin';
|
import alertMixin from '../../../../shared/mixins/alertMixin';
|
||||||
|
|
||||||
import Recorder from 'opus-recorder';
|
import Recorder from 'opus-recorder';
|
||||||
import encoderWorker from 'opus-recorder/dist/encoderWorker.min';
|
import encoderWorker from 'opus-recorder/dist/encoderWorker.min';
|
||||||
|
import waveWorker from 'opus-recorder/dist/waveWorker.min';
|
||||||
|
|
||||||
import WaveSurfer from 'wavesurfer.js';
|
import WaveSurfer from 'wavesurfer.js';
|
||||||
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
|
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
|
||||||
|
@ -23,19 +23,25 @@ import 'videojs-wavesurfer/dist/videojs.wavesurfer.js';
|
||||||
import 'videojs-record/dist/videojs.record.js';
|
import 'videojs-record/dist/videojs.record.js';
|
||||||
import 'videojs-record/dist/plugins/videojs.record.opus-recorder.js';
|
import 'videojs-record/dist/plugins/videojs.record.opus-recorder.js';
|
||||||
import { format, addSeconds } from 'date-fns';
|
import { format, addSeconds } from 'date-fns';
|
||||||
|
import { AUDIO_FORMATS } from 'shared/constants/messages';
|
||||||
|
|
||||||
WaveSurfer.microphone = MicrophonePlugin;
|
WaveSurfer.microphone = MicrophonePlugin;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'WootAudioRecorder',
|
name: 'WootAudioRecorder',
|
||||||
mixins: [inboxMixin, alertMixin],
|
mixins: [alertMixin],
|
||||||
|
props: {
|
||||||
|
audioRecordFormat: {
|
||||||
|
type: String,
|
||||||
|
default: AUDIO_FORMATS.WEBM,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
player: false,
|
player: false,
|
||||||
recordingDateStarted: new Date(0),
|
recordingDateStarted: new Date(0),
|
||||||
initialTimeDuration: '00:00',
|
initialTimeDuration: '00:00',
|
||||||
recorderOptions: {
|
recorderOptions: {
|
||||||
debug: true,
|
|
||||||
controls: true,
|
controls: true,
|
||||||
bigPlayButton: false,
|
bigPlayButton: false,
|
||||||
fluid: false,
|
fluid: false,
|
||||||
|
@ -70,13 +76,28 @@ export default {
|
||||||
record: {
|
record: {
|
||||||
audio: true,
|
audio: true,
|
||||||
video: false,
|
video: false,
|
||||||
|
maxLength: 900,
|
||||||
|
timeSlice: 1000,
|
||||||
|
maxFileSize: 15 * 1024 * 1024,
|
||||||
|
...(this.audioRecordFormat === AUDIO_FORMATS.WEBM && {
|
||||||
|
monitorGain: 0,
|
||||||
|
recordingGain: 1,
|
||||||
|
numberOfChannels: 1,
|
||||||
|
encoderSampleRate: 16000,
|
||||||
|
originalSampleRateOverride: 16000,
|
||||||
|
streamPages: true,
|
||||||
|
maxFramesPerPage: 1,
|
||||||
|
encoderFrameSize: 1,
|
||||||
|
encoderPath: waveWorker,
|
||||||
|
}),
|
||||||
|
...(this.audioRecordFormat === AUDIO_FORMATS.OGG && {
|
||||||
displayMilliseconds: false,
|
displayMilliseconds: false,
|
||||||
maxLength: 300,
|
|
||||||
audioEngine: 'opus-recorder',
|
audioEngine: 'opus-recorder',
|
||||||
audioWorkerURL: encoderWorker,
|
audioWorkerURL: encoderWorker,
|
||||||
audioChannels: 1,
|
audioChannels: 1,
|
||||||
audioSampleRate: 48000,
|
audioSampleRate: 48000,
|
||||||
audioBitRate: 128,
|
audioBitRate: 128,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -39,10 +39,17 @@ const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||||
|
|
||||||
import '@chatwoot/prosemirror-schema/src/woot-editor.css';
|
import '@chatwoot/prosemirror-schema/src/woot-editor.css';
|
||||||
import {
|
import {
|
||||||
|
hasPressedEnterAndNotCmdOrShift,
|
||||||
|
hasPressedCommandAndEnter,
|
||||||
hasPressedAltAndPKey,
|
hasPressedAltAndPKey,
|
||||||
hasPressedAltAndLKey,
|
hasPressedAltAndLKey,
|
||||||
} from 'shared/helpers/KeyboardHelpers';
|
} from 'shared/helpers/KeyboardHelpers';
|
||||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||||
|
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||||
|
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||||
|
import AnalyticsHelper, {
|
||||||
|
ANALYTICS_EVENTS,
|
||||||
|
} from '../../../helper/AnalyticsHelper';
|
||||||
|
|
||||||
const createState = (content, placeholder, plugins = []) => {
|
const createState = (content, placeholder, plugins = []) => {
|
||||||
return EditorState.create({
|
return EditorState.create({
|
||||||
|
@ -58,13 +65,15 @@ const createState = (content, placeholder, plugins = []) => {
|
||||||
export default {
|
export default {
|
||||||
name: 'WootMessageEditor',
|
name: 'WootMessageEditor',
|
||||||
components: { TagAgents, CannedResponse },
|
components: { TagAgents, CannedResponse },
|
||||||
mixins: [eventListenerMixins],
|
mixins: [eventListenerMixins, uiSettingsMixin],
|
||||||
props: {
|
props: {
|
||||||
value: { type: String, default: '' },
|
value: { type: String, default: '' },
|
||||||
editorId: { type: String, default: '' },
|
editorId: { type: String, default: '' },
|
||||||
placeholder: { type: String, default: '' },
|
placeholder: { type: String, default: '' },
|
||||||
isPrivate: { type: Boolean, default: false },
|
isPrivate: { type: Boolean, default: false },
|
||||||
enableSuggestions: { type: Boolean, default: true },
|
enableSuggestions: { type: Boolean, default: true },
|
||||||
|
overrideLineBreaks: { type: Boolean, default: false },
|
||||||
|
updateSelectionWith: { type: String, default: '' },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -162,6 +171,25 @@ export default {
|
||||||
isPrivate() {
|
isPrivate() {
|
||||||
this.reloadState();
|
this.reloadState();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateSelectionWith(newValue, oldValue) {
|
||||||
|
if (!this.editorView) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (newValue !== oldValue) {
|
||||||
|
if (this.updateSelectionWith !== '') {
|
||||||
|
const node = this.editorView.state.schema.text(
|
||||||
|
this.updateSelectionWith
|
||||||
|
);
|
||||||
|
const tr = this.editorView.state.tr.replaceSelectionWith(node);
|
||||||
|
this.editorView.focus();
|
||||||
|
this.state = this.editorView.state.apply(tr);
|
||||||
|
this.emitOnChange();
|
||||||
|
this.$emit('clear-selection');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.state = createState(this.value, this.placeholder, this.plugins);
|
this.state = createState(this.value, this.placeholder, this.plugins);
|
||||||
|
@ -188,6 +216,9 @@ export default {
|
||||||
keyup: () => {
|
keyup: () => {
|
||||||
this.onKeyup();
|
this.onKeyup();
|
||||||
},
|
},
|
||||||
|
keydown: (view, event) => {
|
||||||
|
this.onKeydown(event);
|
||||||
|
},
|
||||||
focus: () => {
|
focus: () => {
|
||||||
this.onFocus();
|
this.onFocus();
|
||||||
},
|
},
|
||||||
|
@ -203,6 +234,12 @@ export default {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
isEnterToSendEnabled() {
|
||||||
|
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
|
||||||
|
},
|
||||||
|
isCmdPlusEnterToSendEnabled() {
|
||||||
|
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter');
|
||||||
|
},
|
||||||
handleKeyEvents(e) {
|
handleKeyEvents(e) {
|
||||||
if (hasPressedAltAndPKey(e)) {
|
if (hasPressedAltAndPKey(e)) {
|
||||||
this.focusEditorInputField();
|
this.focusEditorInputField();
|
||||||
|
@ -233,7 +270,10 @@ export default {
|
||||||
node
|
node
|
||||||
);
|
);
|
||||||
this.state = this.editorView.state.apply(tr);
|
this.state = this.editorView.state.apply(tr);
|
||||||
return this.emitOnChange();
|
this.emitOnChange();
|
||||||
|
AnalyticsHelper.track(ANALYTICS_EVENTS.USED_MENTIONS);
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
insertCannedResponse(cannedItem) {
|
insertCannedResponse(cannedItem) {
|
||||||
|
@ -241,22 +281,27 @@ export default {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tr = this.editorView.state.tr.insertText(
|
let from = this.range.from - 1;
|
||||||
cannedItem,
|
let node = addMentionsToMarkdownParser(defaultMarkdownParser).parse(
|
||||||
this.range.from,
|
cannedItem
|
||||||
this.range.to
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (node.childCount === 1) {
|
||||||
|
node = this.editorView.state.schema.text(cannedItem);
|
||||||
|
from = this.range.from;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tr = this.editorView.state.tr.replaceWith(
|
||||||
|
from,
|
||||||
|
this.range.to,
|
||||||
|
node
|
||||||
|
);
|
||||||
|
|
||||||
this.state = this.editorView.state.apply(tr);
|
this.state = this.editorView.state.apply(tr);
|
||||||
this.emitOnChange();
|
this.emitOnChange();
|
||||||
|
|
||||||
// Hacky fix for #5501
|
tr.scrollIntoView();
|
||||||
this.state = createState(
|
AnalyticsHelper.track(ANALYTICS_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
||||||
this.contentFromEditor,
|
|
||||||
this.placeholder,
|
|
||||||
this.plugins
|
|
||||||
);
|
|
||||||
this.editorView.updateState(this.state);
|
|
||||||
this.focusEditorInputField();
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -278,6 +323,24 @@ export default {
|
||||||
clearTimeout(this.idleTimer);
|
clearTimeout(this.idleTimer);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
handleLineBreakWhenEnterToSendEnabled(event) {
|
||||||
|
if (
|
||||||
|
hasPressedEnterAndNotCmdOrShift(event) &&
|
||||||
|
this.isEnterToSendEnabled() &&
|
||||||
|
!this.overrideLineBreaks
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleLineBreakWhenCmdAndEnterToSendEnabled(event) {
|
||||||
|
if (
|
||||||
|
hasPressedCommandAndEnter(event) &&
|
||||||
|
this.isCmdPlusEnterToSendEnabled() &&
|
||||||
|
!this.overrideLineBreaks
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
onKeyup() {
|
onKeyup() {
|
||||||
if (!this.idleTimer) {
|
if (!this.idleTimer) {
|
||||||
this.$emit('typing-on');
|
this.$emit('typing-on');
|
||||||
|
@ -288,6 +351,14 @@ export default {
|
||||||
TYPING_INDICATOR_IDLE_TIME
|
TYPING_INDICATOR_IDLE_TIME
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
onKeydown(event) {
|
||||||
|
if (this.isEnterToSendEnabled()) {
|
||||||
|
this.handleLineBreakWhenEnterToSendEnabled(event);
|
||||||
|
}
|
||||||
|
if (this.isCmdPlusEnterToSendEnabled()) {
|
||||||
|
this.handleLineBreakWhenCmdAndEnterToSendEnabled(event);
|
||||||
|
}
|
||||||
|
},
|
||||||
onBlur() {
|
onBlur() {
|
||||||
this.turnOffIdleTimer();
|
this.turnOffIdleTimer();
|
||||||
this.resetTyping();
|
this.resetTyping();
|
||||||
|
|
|
@ -232,11 +232,18 @@ export default {
|
||||||
return this.showFileUpload || this.isNote;
|
return this.showFileUpload || this.isNote;
|
||||||
},
|
},
|
||||||
showAudioRecorderButton() {
|
showAudioRecorderButton() {
|
||||||
|
// Disable audio recorder for safari browser as recording is not supported
|
||||||
|
const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(
|
||||||
|
navigator.userAgent
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.isFeatureEnabledonAccount(
|
this.isFeatureEnabledonAccount(
|
||||||
this.accountId,
|
this.accountId,
|
||||||
FEATURE_FLAGS.VOICE_RECORDER
|
FEATURE_FLAGS.VOICE_RECORDER
|
||||||
) && this.showAudioRecorder
|
) &&
|
||||||
|
this.showAudioRecorder &&
|
||||||
|
!isSafari
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
showAudioPlayStopButton() {
|
showAudioPlayStopButton() {
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
</div>
|
</div>
|
||||||
<dashboard-app-frame
|
<dashboard-app-frame
|
||||||
v-else
|
v-else
|
||||||
:key="currentChat.id"
|
:key="currentChat.id + '-' + activeIndex"
|
||||||
:config="dashboardApps[activeIndex - 1].content"
|
:config="dashboardApps[activeIndex - 1].content"
|
||||||
:current-chat="currentChat"
|
:current-chat="currentChat"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -91,6 +91,7 @@
|
||||||
</span>
|
</span>
|
||||||
<span class="unread">{{ unreadCount > 9 ? '9+' : unreadCount }}</span>
|
<span class="unread">{{ unreadCount > 9 ? '9+' : unreadCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<card-labels :conversation-id="chat.id" />
|
||||||
</div>
|
</div>
|
||||||
<woot-context-menu
|
<woot-context-menu
|
||||||
v-if="showContextMenu"
|
v-if="showContextMenu"
|
||||||
|
@ -102,10 +103,12 @@
|
||||||
<conversation-context-menu
|
<conversation-context-menu
|
||||||
:status="chat.status"
|
:status="chat.status"
|
||||||
:inbox-id="inbox.id"
|
:inbox-id="inbox.id"
|
||||||
|
:has-unread-messages="hasUnread"
|
||||||
@update-conversation="onUpdateConversation"
|
@update-conversation="onUpdateConversation"
|
||||||
@assign-agent="onAssignAgent"
|
@assign-agent="onAssignAgent"
|
||||||
@assign-label="onAssignLabel"
|
@assign-label="onAssignLabel"
|
||||||
@assign-team="onAssignTeam"
|
@assign-team="onAssignTeam"
|
||||||
|
@mark-as-unread="markAsUnread"
|
||||||
/>
|
/>
|
||||||
</woot-context-menu>
|
</woot-context-menu>
|
||||||
</div>
|
</div>
|
||||||
|
@ -123,8 +126,8 @@ import InboxName from '../InboxName';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
import ConversationContextMenu from './contextMenu/Index.vue';
|
import ConversationContextMenu from './contextMenu/Index.vue';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
import timeAgo from 'dashboard/components/ui/TimeAgo';
|
import TimeAgo from 'dashboard/components/ui/TimeAgo';
|
||||||
|
import CardLabels from './conversationCardComponents/CardLabels.vue';
|
||||||
const ATTACHMENT_ICONS = {
|
const ATTACHMENT_ICONS = {
|
||||||
image: 'image',
|
image: 'image',
|
||||||
audio: 'headphones-sound-wave',
|
audio: 'headphones-sound-wave',
|
||||||
|
@ -136,10 +139,11 @@ const ATTACHMENT_ICONS = {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
CardLabels,
|
||||||
InboxName,
|
InboxName,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
ConversationContextMenu,
|
ConversationContextMenu,
|
||||||
timeAgo,
|
TimeAgo,
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [
|
mixins: [
|
||||||
|
@ -241,7 +245,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
unreadCount() {
|
unreadCount() {
|
||||||
return this.unreadMessagesCount(this.chat);
|
return this.chat.unread_count;
|
||||||
},
|
},
|
||||||
|
|
||||||
hasUnread() {
|
hasUnread() {
|
||||||
|
@ -359,16 +363,24 @@ export default {
|
||||||
this.$emit('assign-team', team, this.chat.id);
|
this.$emit('assign-team', team, this.chat.id);
|
||||||
this.closeContextMenu();
|
this.closeContextMenu();
|
||||||
},
|
},
|
||||||
|
async markAsUnread() {
|
||||||
|
this.$emit('mark-as-unread', this.chat.id);
|
||||||
|
this.closeContextMenu();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.conversation {
|
.conversation {
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--color-background-light);
|
background: var(--color-background-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&::v-deep .user-thumbnail-box {
|
||||||
|
margin-top: var(--space-normal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-selected {
|
.conversation-selected {
|
||||||
|
@ -377,8 +389,10 @@ export default {
|
||||||
|
|
||||||
.has-inbox-name {
|
.has-inbox-name {
|
||||||
&::v-deep .user-thumbnail-box {
|
&::v-deep .user-thumbnail-box {
|
||||||
margin-top: var(--space-normal);
|
margin-top: var(--space-large);
|
||||||
align-items: flex-start;
|
}
|
||||||
|
.checkbox-wrapper {
|
||||||
|
margin-top: var(--space-large);
|
||||||
}
|
}
|
||||||
.conversation--meta {
|
.conversation--meta {
|
||||||
margin-top: var(--space-normal);
|
margin-top: var(--space-normal);
|
||||||
|
@ -423,6 +437,7 @@ export default {
|
||||||
margin-top: var(--space-minus-micro);
|
margin-top: var(--space-minus-micro);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-wrapper {
|
.checkbox-wrapper {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
width: 40px;
|
width: 40px;
|
||||||
|
@ -432,6 +447,7 @@ export default {
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
margin-top: var(--space-normal);
|
margin-top: var(--space-normal);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--w-100);
|
background-color: var(--w-100);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,11 @@
|
||||||
/>
|
/>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="conversation--header--actions">
|
<div class="conversation--header--actions">
|
||||||
<inbox-name :inbox="inbox" class="margin-right-small" />
|
<inbox-name
|
||||||
|
v-if="hasMultipleInboxes"
|
||||||
|
:inbox="inbox"
|
||||||
|
class="margin-right-small"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="isSnoozed"
|
v-if="isSnoozed"
|
||||||
class="snoozed--display-text margin-right-small"
|
class="snoozed--display-text margin-right-small"
|
||||||
|
@ -145,6 +149,9 @@ export default {
|
||||||
const { inbox_id: inboxId } = this.chat;
|
const { inbox_id: inboxId } = this.chat;
|
||||||
return this.$store.getters['inboxes/getInbox'](inboxId);
|
return this.$store.getters['inboxes/getInbox'](inboxId);
|
||||||
},
|
},
|
||||||
|
hasMultipleInboxes() {
|
||||||
|
return this.$store.getters['inboxes/getInboxes'].length > 1;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
v-if="data.content"
|
v-if="data.content"
|
||||||
:message="message"
|
:message="message"
|
||||||
:is-email="isEmailContentType"
|
:is-email="isEmailContentType"
|
||||||
:readable-time="readableTime"
|
|
||||||
:display-quoted-button="displayQuotedButton"
|
:display-quoted-button="displayQuotedButton"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
|
@ -29,7 +28,6 @@
|
||||||
<bubble-image
|
<bubble-image
|
||||||
v-if="attachment.file_type === 'image' && !hasImageError"
|
v-if="attachment.file_type === 'image' && !hasImageError"
|
||||||
:url="attachment.data_url"
|
:url="attachment.data_url"
|
||||||
:readable-time="readableTime"
|
|
||||||
@error="onImageLoadError"
|
@error="onImageLoadError"
|
||||||
/>
|
/>
|
||||||
<audio v-else-if="attachment.file_type === 'audio'" controls>
|
<audio v-else-if="attachment.file_type === 'audio'" controls>
|
||||||
|
@ -38,13 +36,14 @@
|
||||||
<bubble-video
|
<bubble-video
|
||||||
v-else-if="attachment.file_type === 'video'"
|
v-else-if="attachment.file_type === 'video'"
|
||||||
:url="attachment.data_url"
|
:url="attachment.data_url"
|
||||||
:readable-time="readableTime"
|
|
||||||
/>
|
/>
|
||||||
<bubble-file
|
<bubble-location
|
||||||
v-else
|
v-else-if="attachment.file_type === 'location'"
|
||||||
:url="attachment.data_url"
|
:latitude="attachment.coordinates_lat"
|
||||||
:readable-time="readableTime"
|
:longitude="attachment.coordinates_long"
|
||||||
|
:name="attachment.fallback_title"
|
||||||
/>
|
/>
|
||||||
|
<bubble-file v-else :url="attachment.data_url" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<bubble-actions
|
<bubble-actions
|
||||||
|
@ -53,14 +52,15 @@
|
||||||
:story-sender="storySender"
|
:story-sender="storySender"
|
||||||
:story-id="storyId"
|
:story-id="storyId"
|
||||||
:is-a-tweet="isATweet"
|
:is-a-tweet="isATweet"
|
||||||
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
:has-instagram-story="hasInstagramStory"
|
||||||
:is-email="isEmailContentType"
|
:is-email="isEmailContentType"
|
||||||
:is-private="data.private"
|
:is-private="data.private"
|
||||||
:message-type="data.message_type"
|
:message-type="data.message_type"
|
||||||
:readable-time="readableTime"
|
:message-status="status"
|
||||||
:source-id="data.source_id"
|
:source-id="data.source_id"
|
||||||
:inbox-id="data.inbox_id"
|
:inbox-id="data.inbox_id"
|
||||||
:message-read="showReadTicks"
|
:created-at="createdAt"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<spinner v-if="isPending" size="tiny" />
|
<spinner v-if="isPending" size="tiny" />
|
||||||
|
@ -111,14 +111,13 @@
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
import timeMixin from '../../../mixins/time';
|
|
||||||
|
|
||||||
import BubbleMailHead from './bubble/MailHead';
|
import BubbleMailHead from './bubble/MailHead';
|
||||||
import BubbleText from './bubble/Text';
|
import BubbleText from './bubble/Text';
|
||||||
import BubbleImage from './bubble/Image';
|
import BubbleImage from './bubble/Image';
|
||||||
import BubbleFile from './bubble/File';
|
import BubbleFile from './bubble/File';
|
||||||
import BubbleVideo from './bubble/Video.vue';
|
import BubbleVideo from './bubble/Video.vue';
|
||||||
import BubbleActions from './bubble/Actions';
|
import BubbleActions from './bubble/Actions';
|
||||||
|
import BubbleLocation from './bubble/Location';
|
||||||
|
|
||||||
import Spinner from 'shared/components/Spinner';
|
import Spinner from 'shared/components/Spinner';
|
||||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
|
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
|
||||||
|
@ -136,10 +135,11 @@ export default {
|
||||||
BubbleFile,
|
BubbleFile,
|
||||||
BubbleVideo,
|
BubbleVideo,
|
||||||
BubbleMailHead,
|
BubbleMailHead,
|
||||||
|
BubbleLocation,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
Spinner,
|
Spinner,
|
||||||
},
|
},
|
||||||
mixins: [alertMixin, timeMixin, messageFormatterMixin, contentTypeMixin],
|
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
|
||||||
props: {
|
props: {
|
||||||
data: {
|
data: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -149,11 +149,11 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
hasInstagramStory: {
|
isAWhatsAppChannel: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
hasUserReadMessage: {
|
hasInstagramStory: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
@ -223,6 +223,9 @@ export default {
|
||||||
sender() {
|
sender() {
|
||||||
return this.data.sender || {};
|
return this.data.sender || {};
|
||||||
},
|
},
|
||||||
|
status() {
|
||||||
|
return this.data.status;
|
||||||
|
},
|
||||||
storySender() {
|
storySender() {
|
||||||
return this.contentAttributes.story_sender || null;
|
return this.contentAttributes.story_sender || null;
|
||||||
},
|
},
|
||||||
|
@ -256,11 +259,8 @@ export default {
|
||||||
'has-tweet-menu': this.isATweet,
|
'has-tweet-menu': this.isATweet,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
readableTime() {
|
createdAt() {
|
||||||
return this.messageStamp(
|
return this.contentAttributes.external_created_at || this.data.created_at;
|
||||||
this.contentAttributes.external_created_at || this.data.created_at,
|
|
||||||
'LLL d, h:mm a'
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
isBubble() {
|
isBubble() {
|
||||||
return [0, 1, 3].includes(this.data.message_type);
|
return [0, 1, 3].includes(this.data.message_type);
|
||||||
|
@ -271,14 +271,6 @@ export default {
|
||||||
isOutgoing() {
|
isOutgoing() {
|
||||||
return this.data.message_type === MESSAGE_TYPE.OUTGOING;
|
return this.data.message_type === MESSAGE_TYPE.OUTGOING;
|
||||||
},
|
},
|
||||||
showReadTicks() {
|
|
||||||
return (
|
|
||||||
(this.isOutgoing || this.isTemplate) &&
|
|
||||||
this.hasUserReadMessage &&
|
|
||||||
this.isWebWidgetInbox &&
|
|
||||||
!this.data.private
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isTemplate() {
|
isTemplate() {
|
||||||
return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
|
return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
|
||||||
},
|
},
|
||||||
|
|
|
@ -35,20 +35,18 @@
|
||||||
<message
|
<message
|
||||||
v-for="message in getReadMessages"
|
v-for="message in getReadMessages"
|
||||||
:key="message.id"
|
:key="message.id"
|
||||||
class="message--read"
|
class="message--read ph-no-capture"
|
||||||
:data="message"
|
:data="message"
|
||||||
:is-a-tweet="isATweet"
|
:is-a-tweet="isATweet"
|
||||||
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
:has-instagram-story="hasInstagramStory"
|
||||||
:has-user-read-message="
|
|
||||||
hasUserReadMessage(message.created_at, getLastSeenAt)
|
|
||||||
"
|
|
||||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||||
/>
|
/>
|
||||||
<li v-show="getUnreadCount != 0" class="unread--toast">
|
<li v-show="unreadMessageCount != 0" class="unread--toast">
|
||||||
<span class="text-uppercase">
|
<span class="text-uppercase">
|
||||||
{{ getUnreadCount }}
|
{{ unreadMessageCount }}
|
||||||
{{
|
{{
|
||||||
getUnreadCount > 1
|
unreadMessageCount > 1
|
||||||
? $t('CONVERSATION.UNREAD_MESSAGES')
|
? $t('CONVERSATION.UNREAD_MESSAGES')
|
||||||
: $t('CONVERSATION.UNREAD_MESSAGE')
|
: $t('CONVERSATION.UNREAD_MESSAGE')
|
||||||
}}
|
}}
|
||||||
|
@ -57,13 +55,11 @@
|
||||||
<message
|
<message
|
||||||
v-for="message in getUnReadMessages"
|
v-for="message in getUnReadMessages"
|
||||||
:key="message.id"
|
:key="message.id"
|
||||||
class="message--unread"
|
class="message--unread ph-no-capture"
|
||||||
:data="message"
|
:data="message"
|
||||||
:is-a-tweet="isATweet"
|
:is-a-tweet="isATweet"
|
||||||
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
:has-instagram-story="hasInstagramStory"
|
||||||
:has-user-read-message="
|
|
||||||
hasUserReadMessage(message.created_at, getLastSeenAt)
|
|
||||||
"
|
|
||||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -137,7 +133,6 @@ export default {
|
||||||
allConversations: 'getAllConversations',
|
allConversations: 'getAllConversations',
|
||||||
inboxesList: 'inboxes/getInboxes',
|
inboxesList: 'inboxes/getInboxes',
|
||||||
listLoadingStatus: 'getAllMessagesLoaded',
|
listLoadingStatus: 'getAllMessagesLoaded',
|
||||||
getUnreadCount: 'getUnreadCount',
|
|
||||||
loadingChatList: 'getChatListLoadingStatus',
|
loadingChatList: 'getChatListLoadingStatus',
|
||||||
}),
|
}),
|
||||||
inboxId() {
|
inboxId() {
|
||||||
|
@ -271,6 +266,9 @@ export default {
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
|
unreadMessageCount() {
|
||||||
|
return this.currentChat.unread_count;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -331,7 +329,7 @@ export default {
|
||||||
},
|
},
|
||||||
scrollToBottom() {
|
scrollToBottom() {
|
||||||
let relevantMessages = [];
|
let relevantMessages = [];
|
||||||
if (this.getUnreadCount > 0) {
|
if (this.unreadMessageCount > 0) {
|
||||||
// capturing only the unread messages
|
// capturing only the unread messages
|
||||||
relevantMessages = this.conversationPanel.querySelectorAll(
|
relevantMessages = this.conversationPanel.querySelectorAll(
|
||||||
'.message--unread'
|
'.message--unread'
|
||||||
|
@ -429,12 +427,7 @@ export default {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: unset;
|
left: unset;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
bottom: var(--space-smaller);
|
||||||
&::before {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
left: var(--space-smaller);
|
|
||||||
bottom: var(--space-minus-slab);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
<woot-audio-recorder
|
<woot-audio-recorder
|
||||||
v-if="showAudioRecorderEditor"
|
v-if="showAudioRecorderEditor"
|
||||||
ref="audioRecorderInput"
|
ref="audioRecorderInput"
|
||||||
|
:audio-record-format="audioRecordFormat"
|
||||||
@state-recorder-progress-changed="onStateProgressRecorderChanged"
|
@state-recorder-progress-changed="onStateProgressRecorderChanged"
|
||||||
@state-recorder-changed="onStateRecorderChanged"
|
@state-recorder-changed="onStateRecorderChanged"
|
||||||
@finish-record="onFinishRecorder"
|
@finish-record="onFinishRecorder"
|
||||||
|
@ -60,6 +61,7 @@
|
||||||
class="input"
|
class="input"
|
||||||
:is-private="isOnPrivateNote"
|
:is-private="isOnPrivateNote"
|
||||||
:placeholder="messagePlaceHolder"
|
:placeholder="messagePlaceHolder"
|
||||||
|
:update-selection-with="updateEditorSelectionWith"
|
||||||
:min-height="4"
|
:min-height="4"
|
||||||
@typing-off="onTypingOff"
|
@typing-off="onTypingOff"
|
||||||
@typing-on="onTypingOn"
|
@typing-on="onTypingOn"
|
||||||
|
@ -67,6 +69,7 @@
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
@toggle-user-mention="toggleUserMention"
|
@toggle-user-mention="toggleUserMention"
|
||||||
@toggle-canned-menu="toggleCannedMenu"
|
@toggle-canned-menu="toggleCannedMenu"
|
||||||
|
@clear-selection="clearEditorSelection"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste">
|
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste">
|
||||||
|
@ -130,7 +133,6 @@ import { mapGetters } from 'vuex';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import alertMixin from 'shared/mixins/alertMixin';
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
import EmojiInput from 'shared/components/emoji/EmojiInput';
|
|
||||||
import CannedResponse from './CannedResponse';
|
import CannedResponse from './CannedResponse';
|
||||||
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
import ResizableTextArea from 'shared/components/ResizableTextArea';
|
||||||
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
|
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
|
||||||
|
@ -146,6 +148,7 @@ import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
import {
|
import {
|
||||||
MAXIMUM_FILE_UPLOAD_SIZE,
|
MAXIMUM_FILE_UPLOAD_SIZE,
|
||||||
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL,
|
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL,
|
||||||
|
AUDIO_FORMATS,
|
||||||
} from 'shared/constants/messages';
|
} from 'shared/constants/messages';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
|
|
||||||
|
@ -160,6 +163,11 @@ import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../../helper/localStorage';
|
||||||
import { trimContent, debounce } from '@chatwoot/utils';
|
import { trimContent, debounce } from '@chatwoot/utils';
|
||||||
import wootConstants from 'dashboard/constants';
|
import wootConstants from 'dashboard/constants';
|
||||||
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
|
||||||
|
import AnalyticsHelper, {
|
||||||
|
ANALYTICS_EVENTS,
|
||||||
|
} from '../../../helper/AnalyticsHelper';
|
||||||
|
|
||||||
|
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -215,6 +223,7 @@ export default {
|
||||||
ccEmails: '',
|
ccEmails: '',
|
||||||
doAutoSaveDraft: () => {},
|
doAutoSaveDraft: () => {},
|
||||||
showWhatsAppTemplatesModal: false,
|
showWhatsAppTemplatesModal: false,
|
||||||
|
updateEditorSelectionWith: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -398,7 +407,7 @@ export default {
|
||||||
return conversationDisplayType !== CONDENSED;
|
return conversationDisplayType !== CONDENSED;
|
||||||
},
|
},
|
||||||
emojiDialogClassOnExpanedLayout() {
|
emojiDialogClassOnExpanedLayout() {
|
||||||
return this.isOnExpandedLayout && !this.popoutReplyBox
|
return this.isOnExpandedLayout || this.popoutReplyBox
|
||||||
? 'emoji-dialog--expanded'
|
? 'emoji-dialog--expanded'
|
||||||
: '';
|
: '';
|
||||||
},
|
},
|
||||||
|
@ -450,12 +459,17 @@ export default {
|
||||||
return this.currentChat.id;
|
return this.currentChat.id;
|
||||||
},
|
},
|
||||||
conversationIdByRoute() {
|
conversationIdByRoute() {
|
||||||
const { conversation_id: conversationId } = this.$route.params;
|
return this.conversationId;
|
||||||
return conversationId;
|
|
||||||
},
|
},
|
||||||
editorStateId() {
|
editorStateId() {
|
||||||
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
|
||||||
},
|
},
|
||||||
|
audioRecordFormat() {
|
||||||
|
if (this.isAWebWidgetInbox) {
|
||||||
|
return AUDIO_FORMATS.WEBM;
|
||||||
|
}
|
||||||
|
return AUDIO_FORMATS.OGG;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentChat(conversation) {
|
currentChat(conversation) {
|
||||||
|
@ -587,6 +601,7 @@ export default {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (keyCode === 'enter' && this.isAValidEvent('enter')) {
|
} else if (keyCode === 'enter' && this.isAValidEvent('enter')) {
|
||||||
this.onSendReply();
|
this.onSendReply();
|
||||||
|
e.preventDefault();
|
||||||
} else if (
|
} else if (
|
||||||
['meta+enter', 'ctrl+enter'].includes(keyCode) &&
|
['meta+enter', 'ctrl+enter'].includes(keyCode) &&
|
||||||
this.isAValidEvent('cmd_enter')
|
this.isAValidEvent('cmd_enter')
|
||||||
|
@ -694,6 +709,7 @@ export default {
|
||||||
},
|
},
|
||||||
replaceText(message) {
|
replaceText(message) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
AnalyticsHelper.track(ANALYTICS_EVENTS.INSERTED_A_CANNED_RESPONSE);
|
||||||
this.message = message;
|
this.message = message;
|
||||||
}, 100);
|
}, 100);
|
||||||
},
|
},
|
||||||
|
@ -708,8 +724,26 @@ export default {
|
||||||
}
|
}
|
||||||
this.$nextTick(() => this.$refs.messageInput.focus());
|
this.$nextTick(() => this.$refs.messageInput.focus());
|
||||||
},
|
},
|
||||||
|
clearEditorSelection() {
|
||||||
|
this.updateEditorSelectionWith = '';
|
||||||
|
},
|
||||||
|
insertEmoji(emoji, selectionStart, selectionEnd) {
|
||||||
|
const { message } = this;
|
||||||
|
const newMessage =
|
||||||
|
message.slice(0, selectionStart) +
|
||||||
|
emoji +
|
||||||
|
message.slice(selectionEnd, message.length);
|
||||||
|
this.message = newMessage;
|
||||||
|
},
|
||||||
emojiOnClick(emoji) {
|
emojiOnClick(emoji) {
|
||||||
this.message = `${this.message}${emoji} `;
|
if (this.showRichContentEditor) {
|
||||||
|
this.updateEditorSelectionWith = emoji;
|
||||||
|
this.onFocus();
|
||||||
|
}
|
||||||
|
if (!this.showRichContentEditor) {
|
||||||
|
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
|
||||||
|
this.insertEmoji(emoji, selectionStart, selectionEnd);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clearMessage() {
|
clearMessage() {
|
||||||
this.message = '';
|
this.message = '';
|
||||||
|
@ -964,13 +998,13 @@ export default {
|
||||||
|
|
||||||
.emoji-dialog {
|
.emoji-dialog {
|
||||||
top: unset;
|
top: unset;
|
||||||
bottom: 12px;
|
bottom: var(--space-normal);
|
||||||
left: -320px;
|
left: -320px;
|
||||||
right: unset;
|
right: unset;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
right: -16px;
|
right: var(--space-minus-normal);
|
||||||
bottom: 10px;
|
bottom: var(--space-small);
|
||||||
transform: rotate(270deg);
|
transform: rotate(270deg);
|
||||||
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
||||||
}
|
}
|
||||||
|
@ -984,7 +1018,7 @@ export default {
|
||||||
&::before {
|
&::before {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
left: var(--space-smaller);
|
left: var(--space-smaller);
|
||||||
bottom: var(--space-minus-slab);
|
bottom: var(--space-minus-small);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.message-signature {
|
.message-signature {
|
||||||
|
|
|
@ -1,22 +1,38 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="message-text--metadata">
|
<div class="message-text--metadata">
|
||||||
<span class="time" :class="{ delivered: messageRead }">{{
|
<span
|
||||||
readableTime
|
class="time"
|
||||||
}}</span>
|
:class="{
|
||||||
<span v-if="showSentIndicator" class="time">
|
'has-status-icon':
|
||||||
|
showSentIndicator || showDeliveredIndicator || showReadIndicator,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ readableTime }}
|
||||||
|
</span>
|
||||||
|
<span v-if="showReadIndicator" class="read-indicator-wrap">
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
|
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
|
||||||
icon="checkmark"
|
icon="checkmark-double"
|
||||||
|
class="action--icon read-tick read-indicator"
|
||||||
size="14"
|
size="14"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
<span v-else-if="showDeliveredIndicator" class="read-indicator-wrap">
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-if="messageRead"
|
v-tooltip.top-start="$t('CHAT_LIST.DELIVERED')"
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
|
|
||||||
icon="checkmark-double"
|
icon="checkmark-double"
|
||||||
class="action--icon read-tick"
|
class="action--icon read-tick"
|
||||||
size="12"
|
size="14"
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="showSentIndicator" class="read-indicator-wrap">
|
||||||
|
<fluent-icon
|
||||||
|
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
|
||||||
|
icon="checkmark"
|
||||||
|
class="action--icon read-tick"
|
||||||
|
size="14"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-if="isEmail"
|
v-if="isEmail"
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
|
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
|
||||||
|
@ -44,19 +60,6 @@
|
||||||
size="16"
|
size="16"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<a
|
|
||||||
v-if="hasInstagramStory && (isIncoming || isOutgoing) && linkToStory"
|
|
||||||
:href="linkToStory"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer nofollow"
|
|
||||||
>
|
|
||||||
<fluent-icon
|
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.LINK_TO_STORY')"
|
|
||||||
icon="open"
|
|
||||||
class="action--icon cursor-pointer"
|
|
||||||
size="16"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a
|
<a
|
||||||
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
||||||
:href="linkToTweet"
|
:href="linkToTweet"
|
||||||
|
@ -74,20 +77,22 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import timeMixin from '../../../../mixins/time';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [inboxMixin],
|
mixins: [inboxMixin, timeMixin],
|
||||||
props: {
|
props: {
|
||||||
sender: {
|
sender: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
readableTime: {
|
createdAt: {
|
||||||
type: String,
|
type: Number,
|
||||||
default: '',
|
default: 0,
|
||||||
},
|
},
|
||||||
storySender: {
|
storySender: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -117,6 +122,10 @@ export default {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1,
|
default: 1,
|
||||||
},
|
},
|
||||||
|
messageStatus: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
sourceId: {
|
sourceId: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
|
@ -129,12 +138,9 @@ export default {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
messageRead: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters({ currentChat: 'getSelectedChat' }),
|
||||||
inbox() {
|
inbox() {
|
||||||
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
return this.$store.getters['inboxes/getInbox'](this.inboxId);
|
||||||
},
|
},
|
||||||
|
@ -144,6 +150,21 @@ export default {
|
||||||
isOutgoing() {
|
isOutgoing() {
|
||||||
return MESSAGE_TYPE.OUTGOING === this.messageType;
|
return MESSAGE_TYPE.OUTGOING === this.messageType;
|
||||||
},
|
},
|
||||||
|
isTemplate() {
|
||||||
|
return MESSAGE_TYPE.TEMPLATE === this.messageType;
|
||||||
|
},
|
||||||
|
isDelivered() {
|
||||||
|
return MESSAGE_STATUS.DELIVERED === this.messageStatus;
|
||||||
|
},
|
||||||
|
isRead() {
|
||||||
|
return MESSAGE_STATUS.READ === this.messageStatus;
|
||||||
|
},
|
||||||
|
isSent() {
|
||||||
|
return MESSAGE_STATUS.SENT === this.messageStatus;
|
||||||
|
},
|
||||||
|
readableTime() {
|
||||||
|
return this.messageStamp(this.createdAt, 'LLL d, h:mm a');
|
||||||
|
},
|
||||||
screenName() {
|
screenName() {
|
||||||
const { additional_attributes: additionalAttributes = {} } =
|
const { additional_attributes: additionalAttributes = {} } =
|
||||||
this.sender || {};
|
this.sender || {};
|
||||||
|
@ -164,12 +185,52 @@ export default {
|
||||||
const { storySender, storyId } = this;
|
const { storySender, storyId } = this;
|
||||||
return `https://www.instagram.com/stories/${storySender}/${storyId}`;
|
return `https://www.instagram.com/stories/${storySender}/${storyId}`;
|
||||||
},
|
},
|
||||||
|
showStatusIndicators() {
|
||||||
|
if ((this.isOutgoing || this.isTemplate) && !this.private) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
showSentIndicator() {
|
showSentIndicator() {
|
||||||
return (
|
if (!this.showStatusIndicators) {
|
||||||
this.isOutgoing &&
|
return false;
|
||||||
this.sourceId &&
|
}
|
||||||
(this.isAnEmailChannel || this.isAWhatsAppChannel)
|
|
||||||
);
|
if (this.isAnEmailChannel) {
|
||||||
|
return !!this.sourceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAWhatsAppChannel) {
|
||||||
|
return this.sourceId && this.isSent;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
showDeliveredIndicator() {
|
||||||
|
if (!this.showStatusIndicators) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAWhatsAppChannel) {
|
||||||
|
return this.sourceId && this.isDelivered;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
showReadIndicator() {
|
||||||
|
if (!this.showStatusIndicators) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAWebWidgetInbox) {
|
||||||
|
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
|
||||||
|
return contactLastSeenAt >= this.createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isAWhatsAppChannel) {
|
||||||
|
return this.sourceId && this.isRead;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -185,16 +246,21 @@ export default {
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
.message-text--metadata {
|
.message-text--metadata {
|
||||||
|
align-items: center;
|
||||||
.time {
|
.time {
|
||||||
color: var(--w-100);
|
color: var(--w-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action--icon {
|
.action--icon {
|
||||||
|
color: var(--white);
|
||||||
|
|
||||||
&.read-tick {
|
&.read-tick {
|
||||||
color: var(--v-100);
|
color: var(--v-100);
|
||||||
margin-top: calc(var(--space-micro) + var(--space-micro) / 2);
|
|
||||||
}
|
}
|
||||||
color: var(--white);
|
|
||||||
|
&.read-indicator {
|
||||||
|
color: var(--g-200);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lock--icon--private {
|
.lock--icon--private {
|
||||||
|
@ -258,8 +324,9 @@ export default {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: var(--space-small);
|
right: var(--space-small);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
&.delivered {
|
|
||||||
right: var(--space-medium);
|
&.has-status-icon {
|
||||||
|
right: var(--space-large);
|
||||||
line-height: 2;
|
line-height: 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -296,4 +363,10 @@ export default {
|
||||||
.delivered-icon {
|
.delivered-icon {
|
||||||
margin-left: -var(--space-normal);
|
margin-left: -var(--space-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.read-indicator-wrap {
|
||||||
|
line-height: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<template>
|
||||||
|
<div class="location message-text__wrap">
|
||||||
|
<div class="icon-wrap">
|
||||||
|
<fluent-icon icon="location" class="file--icon" size="32" />
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<h5 class="text-block-title text-truncate">
|
||||||
|
{{ name }}
|
||||||
|
</h5>
|
||||||
|
<div class="link-wrap">
|
||||||
|
<a
|
||||||
|
class="download clear link button small"
|
||||||
|
rel="noreferrer noopener nofollow"
|
||||||
|
target="_blank"
|
||||||
|
:href="mapUrl"
|
||||||
|
>
|
||||||
|
{{ $t('COMPONENTS.LOCATION_BUBBLE.SEE_ON_MAP') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
latitude: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
longitude: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
mapUrl() {
|
||||||
|
return `https://maps.google.com/?q=${this.latitude},${this.longitude}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.location {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: var(--space-smaller) 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.icon-wrap {
|
||||||
|
color: var(--s-600);
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0 var(--space-smaller);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-block-title {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--s-800);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding-right: var(--space-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-wrap {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -35,10 +35,6 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
readableTime: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
isEmail: {
|
isEmail: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="menu-container">
|
<div class="menu-container">
|
||||||
|
<menu-item
|
||||||
|
v-if="!hasUnreadMessages"
|
||||||
|
:option="unreadOption"
|
||||||
|
variant="icon"
|
||||||
|
@click="$emit('mark-as-unread')"
|
||||||
|
/>
|
||||||
<template v-for="option in statusMenuConfig">
|
<template v-for="option in statusMenuConfig">
|
||||||
<menu-item
|
<menu-item
|
||||||
v-if="show(option.key)"
|
v-if="show(option.key)"
|
||||||
|
@ -79,6 +85,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
hasUnreadMessages: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
inboxId: {
|
inboxId: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -87,6 +97,10 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
STATUS_TYPE: wootConstants.STATUS_TYPE,
|
||||||
|
unreadOption: {
|
||||||
|
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.MARK_AS_UNREAD'),
|
||||||
|
icon: 'mail',
|
||||||
|
},
|
||||||
statusMenuConfig: [
|
statusMenuConfig: [
|
||||||
{
|
{
|
||||||
key: wootConstants.STATUS_TYPE.RESOLVED,
|
key: wootConstants.STATUS_TYPE.RESOLVED,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="bulk-action__agents">
|
<div class="bulk-action__agents">
|
||||||
<div class="triangle">
|
<div class="triangle" :style="cssVars">
|
||||||
<svg height="12" viewBox="0 0 24 12" width="24">
|
<svg height="12" viewBox="0 0 24 12" width="24">
|
||||||
<path
|
<path
|
||||||
d="M20 12l-8-8-12 12"
|
d="M20 12l-8-8-12 12"
|
||||||
|
@ -105,13 +105,14 @@ import { mapGetters } from 'vuex';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import Spinner from 'shared/components/Spinner';
|
import Spinner from 'shared/components/Spinner';
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
Spinner,
|
Spinner,
|
||||||
},
|
},
|
||||||
mixins: [clickaway],
|
mixins: [clickaway, bulkActionsMixin],
|
||||||
props: {
|
props: {
|
||||||
selectedInboxes: {
|
selectedInboxes: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -233,7 +234,7 @@ export default {
|
||||||
z-index: var(--z-index-one);
|
z-index: var(--z-index-one);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(var(--space-slab) * -1);
|
top: calc(var(--space-slab) * -1);
|
||||||
right: var(--space-micro);
|
right: var(--triangle-position);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,25 +43,26 @@
|
||||||
variant="smooth"
|
variant="smooth"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
icon="person-assign"
|
icon="person-assign"
|
||||||
|
class="margin-right-smaller"
|
||||||
@click="toggleAgentList"
|
@click="toggleAgentList"
|
||||||
/>
|
/>
|
||||||
|
<woot-button
|
||||||
|
v-tooltip="$t('BULK_ACTION.ASSIGN_TEAM_TOOLTIP')"
|
||||||
|
size="tiny"
|
||||||
|
variant="smooth"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="people-team-add"
|
||||||
|
@click="toggleTeamsList"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<transition name="popover-animation">
|
<transition name="popover-animation">
|
||||||
<label-actions
|
<label-actions
|
||||||
v-if="showLabelActions"
|
v-if="showLabelActions"
|
||||||
|
triangle-position="8.5"
|
||||||
@assign="assignLabels"
|
@assign="assignLabels"
|
||||||
@close="showLabelActions = false"
|
@close="showLabelActions = false"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="popover-animation">
|
|
||||||
<agent-selector
|
|
||||||
v-if="showAgentsList"
|
|
||||||
:selected-inboxes="selectedInboxes"
|
|
||||||
:conversation-count="conversations.length"
|
|
||||||
@select="submit"
|
|
||||||
@close="showAgentsList = false"
|
|
||||||
/>
|
|
||||||
</transition>
|
|
||||||
<transition name="popover-animation">
|
<transition name="popover-animation">
|
||||||
<update-actions
|
<update-actions
|
||||||
v-if="showUpdateActions"
|
v-if="showUpdateActions"
|
||||||
|
@ -70,10 +71,29 @@
|
||||||
:show-resolve="!showResolvedAction"
|
:show-resolve="!showResolvedAction"
|
||||||
:show-reopen="!showOpenAction"
|
:show-reopen="!showOpenAction"
|
||||||
:show-snooze="!showSnoozedAction"
|
:show-snooze="!showSnoozedAction"
|
||||||
|
triangle-position="5.6"
|
||||||
@update="updateConversations"
|
@update="updateConversations"
|
||||||
@close="showUpdateActions = false"
|
@close="showUpdateActions = false"
|
||||||
/>
|
/>
|
||||||
</transition>
|
</transition>
|
||||||
|
<transition name="popover-animation">
|
||||||
|
<agent-selector
|
||||||
|
v-if="showAgentsList"
|
||||||
|
:selected-inboxes="selectedInboxes"
|
||||||
|
:conversation-count="conversations.length"
|
||||||
|
triangle-position="2.8"
|
||||||
|
@select="submit"
|
||||||
|
@close="showAgentsList = false"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
<transition name="popover-animation">
|
||||||
|
<team-actions
|
||||||
|
v-if="showTeamsList"
|
||||||
|
triangle-position="0.2"
|
||||||
|
@assign-team="assignTeam"
|
||||||
|
@close="showTeamsList = false"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="allConversationsSelected" class="bulk-action__alert">
|
<div v-if="allConversationsSelected" class="bulk-action__alert">
|
||||||
{{ $t('BULK_ACTION.ALL_CONVERSATIONS_SELECTED_ALERT') }}
|
{{ $t('BULK_ACTION.ALL_CONVERSATIONS_SELECTED_ALERT') }}
|
||||||
|
@ -85,11 +105,13 @@
|
||||||
import AgentSelector from './AgentSelector.vue';
|
import AgentSelector from './AgentSelector.vue';
|
||||||
import UpdateActions from './UpdateActions.vue';
|
import UpdateActions from './UpdateActions.vue';
|
||||||
import LabelActions from './LabelActions.vue';
|
import LabelActions from './LabelActions.vue';
|
||||||
|
import TeamActions from './TeamActions.vue';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
AgentSelector,
|
AgentSelector,
|
||||||
UpdateActions,
|
UpdateActions,
|
||||||
LabelActions,
|
LabelActions,
|
||||||
|
TeamActions,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
conversations: {
|
conversations: {
|
||||||
|
@ -122,6 +144,8 @@ export default {
|
||||||
showAgentsList: false,
|
showAgentsList: false,
|
||||||
showUpdateActions: false,
|
showUpdateActions: false,
|
||||||
showLabelActions: false,
|
showLabelActions: false,
|
||||||
|
showTeamsList: false,
|
||||||
|
popoverPositions: {},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -137,6 +161,9 @@ export default {
|
||||||
assignLabels(labels) {
|
assignLabels(labels) {
|
||||||
this.$emit('assign-labels', labels);
|
this.$emit('assign-labels', labels);
|
||||||
},
|
},
|
||||||
|
assignTeam(team) {
|
||||||
|
this.$emit('assign-team', team);
|
||||||
|
},
|
||||||
resolveConversations() {
|
resolveConversations() {
|
||||||
this.$emit('resolve-conversations');
|
this.$emit('resolve-conversations');
|
||||||
},
|
},
|
||||||
|
@ -149,6 +176,9 @@ export default {
|
||||||
toggleAgentList() {
|
toggleAgentList() {
|
||||||
this.showAgentsList = !this.showAgentsList;
|
this.showAgentsList = !this.showAgentsList;
|
||||||
},
|
},
|
||||||
|
toggleTeamsList() {
|
||||||
|
this.showTeamsList = !this.showTeamsList;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-on-clickaway="onClose" class="labels-container">
|
<div v-on-clickaway="onClose" class="labels-container">
|
||||||
<div class="triangle">
|
<div class="triangle" :style="cssVars">
|
||||||
<svg height="12" viewBox="0 0 24 12" width="24">
|
<svg height="12" viewBox="0 0 24 12" width="24">
|
||||||
<path
|
<path
|
||||||
d="M20 12l-8-8-12 12"
|
d="M20 12l-8-8-12 12"
|
||||||
|
@ -75,9 +75,10 @@
|
||||||
<script>
|
<script>
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [clickaway],
|
mixins: [clickaway, bulkActionsMixin],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
query: '',
|
query: '',
|
||||||
|
@ -160,7 +161,7 @@ export default {
|
||||||
max-width: var(--space-giga);
|
max-width: var(--space-giga);
|
||||||
min-width: var(--space-giga);
|
min-width: var(--space-giga);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 4.5rem;
|
right: var(--space-small);
|
||||||
top: var(--space-larger);
|
top: var(--space-larger);
|
||||||
transform-origin: top right;
|
transform-origin: top right;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
@ -204,7 +205,7 @@ export default {
|
||||||
.triangle {
|
.triangle {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: var(--space-two);
|
right: var(--triangle-position);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
top: calc(var(--space-slab) * -1);
|
top: calc(var(--space-slab) * -1);
|
||||||
z-index: var(--z-index-one);
|
z-index: var(--z-index-one);
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
<template>
|
||||||
|
<div v-on-clickaway="onClose" class="bulk-action__teams">
|
||||||
|
<div class="triangle" :style="cssVars">
|
||||||
|
<svg height="12" viewBox="0 0 24 12" width="24">
|
||||||
|
<path
|
||||||
|
d="M20 12l-8-8-12 12"
|
||||||
|
fill="var(--white)"
|
||||||
|
fill-rule="evenodd"
|
||||||
|
stroke="var(--s-50)"
|
||||||
|
stroke-width="1px"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="header flex-between">
|
||||||
|
<span>{{ $t('BULK_ACTION.TEAMS.TEAM_SELECT_LABEL') }}</span>
|
||||||
|
<woot-button
|
||||||
|
size="tiny"
|
||||||
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="dismiss"
|
||||||
|
@click="onClose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div class="team__list-container">
|
||||||
|
<ul>
|
||||||
|
<li class="search-container">
|
||||||
|
<div class="agent-list-search flex-between">
|
||||||
|
<fluent-icon icon="search" class="search-icon" size="16" />
|
||||||
|
<input
|
||||||
|
ref="search"
|
||||||
|
v-model="query"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search"
|
||||||
|
class="agent--search_input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<template v-if="filteredTeams.length">
|
||||||
|
<li v-for="team in filteredTeams" :key="team.id">
|
||||||
|
<div class="team__list-item" @click="assignTeam(team)">
|
||||||
|
<span class="reports-option__title">{{ team.name }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<li v-else>
|
||||||
|
<div class="team__list-item">
|
||||||
|
<span class="reports-option__title">{{
|
||||||
|
$t('BULK_ACTION.TEAMS.NO_TEAMS_AVAILABLE')
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
|
||||||
|
export default {
|
||||||
|
mixins: [clickaway, bulkActionsMixin],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
query: '',
|
||||||
|
selectedteams: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ teams: 'teams/getTeams' }),
|
||||||
|
filteredTeams() {
|
||||||
|
return [
|
||||||
|
{ name: 'None', id: 0 },
|
||||||
|
...this.teams.filter(team =>
|
||||||
|
team.name.toLowerCase().includes(this.query.toLowerCase())
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
assignTeam(key) {
|
||||||
|
this.$emit('assign-team', key);
|
||||||
|
},
|
||||||
|
onClose() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.bulk-action__teams {
|
||||||
|
background-color: var(--white);
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
border: 1px solid var(--s-50);
|
||||||
|
box-shadow: var(--shadow-dropdown-pane);
|
||||||
|
max-width: 75%;
|
||||||
|
position: absolute;
|
||||||
|
right: var(--space-small);
|
||||||
|
top: var(--space-larger);
|
||||||
|
transform-origin: top right;
|
||||||
|
width: auto;
|
||||||
|
z-index: var(--z-index-twenty);
|
||||||
|
min-width: var(--space-giga);
|
||||||
|
.header {
|
||||||
|
padding: var(--space-one);
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-height: var(--space-giga);
|
||||||
|
overflow-y: auto;
|
||||||
|
.team__list-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.agent-list-search {
|
||||||
|
padding: 0 var(--space-one);
|
||||||
|
border: 1px solid var(--s-100);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
background-color: var(--s-50);
|
||||||
|
.search-icon {
|
||||||
|
color: var(--s-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent--search_input {
|
||||||
|
border: 0;
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
margin: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.triangle {
|
||||||
|
display: block;
|
||||||
|
z-index: var(--z-index-one);
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--space-slab) * -1);
|
||||||
|
right: var(--triangle-position);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team__list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--space-one);
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--s-50);
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
padding: 0 var(--space-one);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: var(--z-index-twenty);
|
||||||
|
background-color: var(--white);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-on-clickaway="onClose" class="actions-container">
|
<div v-on-clickaway="onClose" class="actions-container">
|
||||||
<div class="triangle">
|
<div class="triangle" :style="cssVars">
|
||||||
<svg height="12" viewBox="0 0 24 12" width="24">
|
<svg height="12" viewBox="0 0 24 12" width="24">
|
||||||
<path
|
<path
|
||||||
d="M20 12l-8-8-12 12"
|
d="M20 12l-8-8-12 12"
|
||||||
|
@ -45,12 +45,14 @@
|
||||||
import { mixin as clickaway } from 'vue-clickaway';
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||||
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||||
|
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
WootDropdownItem,
|
WootDropdownItem,
|
||||||
WootDropdownMenu,
|
WootDropdownMenu,
|
||||||
},
|
},
|
||||||
mixins: [clickaway],
|
mixins: [clickaway, bulkActionsMixin],
|
||||||
props: {
|
props: {
|
||||||
selectedInboxes: {
|
selectedInboxes: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
@ -131,7 +133,7 @@ export default {
|
||||||
box-shadow: var(--shadow-dropdown-pane);
|
box-shadow: var(--shadow-dropdown-pane);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: var(--space-small);
|
right: var(--space-small);
|
||||||
top: 48px;
|
top: var(--space-larger);
|
||||||
transform-origin: top right;
|
transform-origin: top right;
|
||||||
width: auto;
|
width: auto;
|
||||||
z-index: var(--z-index-twenty);
|
z-index: var(--z-index-twenty);
|
||||||
|
@ -152,7 +154,7 @@ export default {
|
||||||
.triangle {
|
.triangle {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 2.8rem;
|
right: var(--triangle-position);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
top: calc(var(--space-slab) * -1);
|
top: calc(var(--space-slab) * -1);
|
||||||
z-index: var(--z-index-one);
|
z-index: var(--z-index-one);
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-show="activeLabels.length"
|
||||||
|
ref="labelContainer"
|
||||||
|
class="label-container"
|
||||||
|
>
|
||||||
|
<div class="labels-wrap" :class="{ expand: showAllLabels }">
|
||||||
|
<woot-label
|
||||||
|
v-for="(label, index) in activeLabels"
|
||||||
|
:key="label.id"
|
||||||
|
:title="label.title"
|
||||||
|
:description="label.description"
|
||||||
|
:color="label.color"
|
||||||
|
variant="smooth"
|
||||||
|
small
|
||||||
|
:class="{ hidden: !showAllLabels && index > labelPosition }"
|
||||||
|
/>
|
||||||
|
<woot-button
|
||||||
|
v-if="showExpandLabelButton"
|
||||||
|
:title="
|
||||||
|
showAllLabels
|
||||||
|
? $t('CONVERSATION.CARD.HIDE_LABELS')
|
||||||
|
: $t('CONVERSATION.CARD.SHOW_LABELS')
|
||||||
|
"
|
||||||
|
class="show-more--button"
|
||||||
|
color-scheme="secondary"
|
||||||
|
variant="hollow"
|
||||||
|
:icon="showAllLabels ? 'chevron-left' : 'chevron-right'"
|
||||||
|
size="tiny"
|
||||||
|
@click="onShowLabels"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
|
||||||
|
export default {
|
||||||
|
mixins: [conversationLabelMixin],
|
||||||
|
props: {
|
||||||
|
conversationId: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showAllLabels: false,
|
||||||
|
showExpandLabelButton: false,
|
||||||
|
labelPosition: -1,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
activeLabels() {
|
||||||
|
this.$nextTick(() => this.computeVisibleLabelPosition());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.computeVisibleLabelPosition();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onShowLabels(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.showAllLabels = !this.showAllLabels;
|
||||||
|
},
|
||||||
|
computeVisibleLabelPosition() {
|
||||||
|
const labelContainer = this.$refs.labelContainer;
|
||||||
|
const labels = this.$refs.labelContainer.querySelectorAll('.label');
|
||||||
|
let labelOffset = 0;
|
||||||
|
this.showExpandLabelButton = false;
|
||||||
|
|
||||||
|
Array.from(labels).forEach((label, index) => {
|
||||||
|
labelOffset += label.offsetWidth + 8;
|
||||||
|
|
||||||
|
if (labelOffset < labelContainer.clientWidth - 16) {
|
||||||
|
this.labelPosition = index;
|
||||||
|
} else {
|
||||||
|
this.showExpandLabelButton = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.show-more--button {
|
||||||
|
height: var(--space-two);
|
||||||
|
position: sticky;
|
||||||
|
flex-shrink: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-right: var(--space-medium);
|
||||||
|
|
||||||
|
&.secondary:focus {
|
||||||
|
color: var(--s-700);
|
||||||
|
border-color: var(--s-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-container {
|
||||||
|
margin-top: var(--space-micro);
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
&.expand {
|
||||||
|
height: auto;
|
||||||
|
overflow: visible;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-bottom: var(--space-smaller);
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-more--button {
|
||||||
|
margin-bottom: var(--space-smaller);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
border: 1px solid var(--s-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,34 @@
|
||||||
|
import LocationBubble from '../bubble/Location.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/Help Center',
|
||||||
|
component: LocationBubble,
|
||||||
|
argTypes: {
|
||||||
|
latitude: {
|
||||||
|
defaultValue: 1,
|
||||||
|
control: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
longitude: {
|
||||||
|
defaultValue: 1,
|
||||||
|
control: {
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
defaultValue: '420, Dope street',
|
||||||
|
control: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: { LocationBubble },
|
||||||
|
template: '<location-bubble v-bind="$props" />',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LocationBubbleView = Template.bind({});
|
|
@ -22,5 +22,6 @@ export default {
|
||||||
EXPANDED: 'expanded',
|
EXPANDED: 'expanded',
|
||||||
},
|
},
|
||||||
DOCS_URL: '//www.chatwoot.com/docs/product/',
|
DOCS_URL: '//www.chatwoot.com/docs/product/',
|
||||||
|
TESTIMONIAL_URL: 'https://testimonials.cdn.chatwoot.com/content.json',
|
||||||
};
|
};
|
||||||
export const DEFAULT_REDIRECT_URL = '/app/';
|
export const DEFAULT_REDIRECT_URL = '/app/';
|
||||||
|
|
|
@ -1,13 +1,18 @@
|
||||||
export const FEATURE_FLAGS = {
|
export const FEATURE_FLAGS = {
|
||||||
AGENT_BOTS: 'agent_bots',
|
AGENT_BOTS: 'agent_bots',
|
||||||
AGENT_MANAGEMENT: 'agent_management',
|
AGENT_MANAGEMENT: 'agent_management',
|
||||||
|
AUTO_RESOLVE_CONVERSATIONS: 'auto_resolve_conversations',
|
||||||
AUTOMATIONS: 'automations',
|
AUTOMATIONS: 'automations',
|
||||||
|
CAMPAIGNS: 'campaigns',
|
||||||
CANNED_RESPONSES: 'canned_responses',
|
CANNED_RESPONSES: 'canned_responses',
|
||||||
|
CRM: 'crm',
|
||||||
CUSTOM_ATTRIBUTES: 'custom_attributes',
|
CUSTOM_ATTRIBUTES: 'custom_attributes',
|
||||||
INBOX_MANAGEMENT: 'inbox_management',
|
INBOX_MANAGEMENT: 'inbox_management',
|
||||||
INTEGRATIONS: 'integrations',
|
INTEGRATIONS: 'integrations',
|
||||||
LABELS: 'labels',
|
LABELS: 'labels',
|
||||||
MACROS: 'macros',
|
MACROS: 'macros',
|
||||||
|
HELP_CENTER: 'help_center',
|
||||||
|
REPORTS: 'reports',
|
||||||
TEAM_MANAGEMENT: 'team_management',
|
TEAM_MANAGEMENT: 'team_management',
|
||||||
VOICE_RECORDER: 'voice_recorder',
|
VOICE_RECORDER: 'voice_recorder',
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
export const EXECUTED_A_MACRO = 'Executed a macro';
|
||||||
|
export const SENT_MESSAGE = 'Sent a message';
|
||||||
|
export const SENT_PRIVATE_NOTE = 'Sent a private note';
|
||||||
|
export const INSERTED_A_CANNED_RESPONSE = 'Inserted a canned response';
|
||||||
|
export const USED_MENTIONS = 'Used mentions';
|
||||||
|
export const MERGED_CONTACTS = 'Used merge contact option';
|
||||||
|
export const ADDED_TO_CANNED_RESPONSE = 'Used added to canned response option';
|
||||||
|
export const ADDED_A_CUSTOM_ATTRIBUTE = 'Added a custom attribute';
|
||||||
|
export const ADDED_AN_INBOX = 'Added an inbox';
|
67
app/javascript/dashboard/helper/AnalyticsHelper/index.js
Normal file
67
app/javascript/dashboard/helper/AnalyticsHelper/index.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { AnalyticsBrowser } from '@june-so/analytics-next';
|
||||||
|
|
||||||
|
class AnalyticsHelper {
|
||||||
|
constructor({ token: analyticsToken } = {}) {
|
||||||
|
this.analyticsToken = analyticsToken;
|
||||||
|
this.analytics = null;
|
||||||
|
this.user = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (!this.analyticsToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let [analytics] = await AnalyticsBrowser.load({
|
||||||
|
writeKey: this.analyticsToken,
|
||||||
|
});
|
||||||
|
this.analytics = analytics;
|
||||||
|
}
|
||||||
|
|
||||||
|
identify(user) {
|
||||||
|
if (!this.analytics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.user = user;
|
||||||
|
this.analytics.identify(this.user.email, {
|
||||||
|
userId: this.user.id,
|
||||||
|
email: this.user.email,
|
||||||
|
name: this.user.name,
|
||||||
|
avatar: this.user.avatar_url,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { accounts, account_id: accountId } = this.user;
|
||||||
|
const [currentAccount] = accounts.filter(
|
||||||
|
account => account.id === accountId
|
||||||
|
);
|
||||||
|
if (currentAccount) {
|
||||||
|
this.analytics.group(currentAccount.id, this.user.id, {
|
||||||
|
name: currentAccount.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
track(eventName, properties = {}) {
|
||||||
|
if (!this.analytics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.analytics.track({
|
||||||
|
userId: this.user.id,
|
||||||
|
event: eventName,
|
||||||
|
properties,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
page(params) {
|
||||||
|
if (!this.analytics) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.analytics.page(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export * as ANALYTICS_EVENTS from './events';
|
||||||
|
|
||||||
|
export default new AnalyticsHelper(window.analyticsConfig);
|
|
@ -56,6 +56,8 @@ export const conversationUrl = ({
|
||||||
url = `accounts/${accountId}/custom_view/${foldersId}/conversations/${id}`;
|
url = `accounts/${accountId}/custom_view/${foldersId}/conversations/${id}`;
|
||||||
} else if (conversationType === 'mention') {
|
} else if (conversationType === 'mention') {
|
||||||
url = `accounts/${accountId}/mentions/conversations/${id}`;
|
url = `accounts/${accountId}/mentions/conversations/${id}`;
|
||||||
|
} else if (conversationType === 'unattended') {
|
||||||
|
url = `accounts/${accountId}/unattended/conversations/${id}`;
|
||||||
}
|
}
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
@ -66,16 +68,23 @@ export const conversationListPageURL = ({
|
||||||
inboxId,
|
inboxId,
|
||||||
label,
|
label,
|
||||||
teamId,
|
teamId,
|
||||||
|
customViewId,
|
||||||
}) => {
|
}) => {
|
||||||
let url = `accounts/${accountId}/dashboard`;
|
let url = `accounts/${accountId}/dashboard`;
|
||||||
if (label) {
|
if (label) {
|
||||||
url = `accounts/${accountId}/label/${label}`;
|
url = `accounts/${accountId}/label/${label}`;
|
||||||
} else if (teamId) {
|
} else if (teamId) {
|
||||||
url = `accounts/${accountId}/team/${teamId}`;
|
url = `accounts/${accountId}/team/${teamId}`;
|
||||||
} else if (conversationType === 'mention') {
|
|
||||||
url = `accounts/${accountId}/mentions/conversations`;
|
|
||||||
} else if (inboxId) {
|
} else if (inboxId) {
|
||||||
url = `accounts/${accountId}/inbox/${inboxId}`;
|
url = `accounts/${accountId}/inbox/${inboxId}`;
|
||||||
|
} else if (customViewId) {
|
||||||
|
url = `accounts/${accountId}/custom_view/${customViewId}`;
|
||||||
|
} else if (conversationType) {
|
||||||
|
const urlMap = {
|
||||||
|
mention: 'mentions/conversations',
|
||||||
|
unattended: 'unattended/conversations',
|
||||||
|
};
|
||||||
|
url = `accounts/${accountId}/${urlMap[conversationType]}`;
|
||||||
}
|
}
|
||||||
return frontendURL(url);
|
return frontendURL(url);
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,13 +17,22 @@ const formatArray = params => {
|
||||||
return params;
|
return params;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generatePayloadForObject = item => {
|
||||||
|
if (item.action_params.id) {
|
||||||
|
item.action_params = [item.action_params.id];
|
||||||
|
} else {
|
||||||
|
item.action_params = [item.action_params];
|
||||||
|
}
|
||||||
|
return item.action_params;
|
||||||
|
};
|
||||||
|
|
||||||
const generatePayload = data => {
|
const generatePayload = data => {
|
||||||
const actions = JSON.parse(JSON.stringify(data));
|
const actions = JSON.parse(JSON.stringify(data));
|
||||||
let payload = actions.map(item => {
|
let payload = actions.map(item => {
|
||||||
if (Array.isArray(item.action_params)) {
|
if (Array.isArray(item.action_params)) {
|
||||||
item.action_params = formatArray(item.action_params);
|
item.action_params = formatArray(item.action_params);
|
||||||
} else if (typeof item.values === 'object') {
|
} else if (typeof item.action_params === 'object') {
|
||||||
item.action_params = [item.action_params.id];
|
item.action_params = generatePayloadForObject(item);
|
||||||
} else if (!item.action_params) {
|
} else if (!item.action_params) {
|
||||||
item.action_params = [];
|
item.action_params = [];
|
||||||
} else {
|
} else {
|
||||||
|
|
242
app/javascript/dashboard/helper/automationHelper.js
Normal file
242
app/javascript/dashboard/helper/automationHelper.js
Normal file
|
@ -0,0 +1,242 @@
|
||||||
|
import {
|
||||||
|
OPERATOR_TYPES_1,
|
||||||
|
OPERATOR_TYPES_3,
|
||||||
|
OPERATOR_TYPES_4,
|
||||||
|
} from 'dashboard/routes/dashboard/settings/automation/operators';
|
||||||
|
import filterQueryGenerator from './filterQueryGenerator';
|
||||||
|
import actionQueryGenerator from './actionQueryGenerator';
|
||||||
|
const MESSAGE_CONDITION_VALUES = [
|
||||||
|
{
|
||||||
|
id: 'incoming',
|
||||||
|
name: 'Incoming Message',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'outgoing',
|
||||||
|
name: 'Outgoing Message',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getCustomAttributeInputType = key => {
|
||||||
|
const customAttributeMap = {
|
||||||
|
date: 'date',
|
||||||
|
text: 'plain_text',
|
||||||
|
list: 'search_select',
|
||||||
|
checkbox: 'search_select',
|
||||||
|
};
|
||||||
|
|
||||||
|
return customAttributeMap[key] || 'plain_text';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isACustomAttribute = (customAttributes, key) => {
|
||||||
|
return customAttributes.find(attr => {
|
||||||
|
return attr.attribute_key === key;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCustomAttributeListDropdownValues = (
|
||||||
|
customAttributes,
|
||||||
|
type
|
||||||
|
) => {
|
||||||
|
return customAttributes
|
||||||
|
.find(attr => attr.attribute_key === type)
|
||||||
|
.attribute_values.map(item => {
|
||||||
|
return {
|
||||||
|
id: item,
|
||||||
|
name: item,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isCustomAttributeCheckbox = (customAttributes, key) => {
|
||||||
|
return customAttributes.find(attr => {
|
||||||
|
return (
|
||||||
|
attr.attribute_key === key && attr.attribute_display_type === 'checkbox'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isCustomAttributeList = (customAttributes, type) => {
|
||||||
|
return customAttributes.find(attr => {
|
||||||
|
return (
|
||||||
|
attr.attribute_key === type && attr.attribute_display_type === 'list'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getOperatorTypes = key => {
|
||||||
|
const operatorMap = {
|
||||||
|
list: OPERATOR_TYPES_1,
|
||||||
|
text: OPERATOR_TYPES_3,
|
||||||
|
number: OPERATOR_TYPES_1,
|
||||||
|
link: OPERATOR_TYPES_1,
|
||||||
|
date: OPERATOR_TYPES_4,
|
||||||
|
checkbox: OPERATOR_TYPES_1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return operatorMap[key] || OPERATOR_TYPES_1;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateCustomAttributeTypes = (customAttributes, type) => {
|
||||||
|
return customAttributes.map(attr => {
|
||||||
|
return {
|
||||||
|
key: attr.attribute_key,
|
||||||
|
name: attr.attribute_display_name,
|
||||||
|
inputType: getCustomAttributeInputType(attr.attribute_display_type),
|
||||||
|
filterOperators: getOperatorTypes(attr.attribute_display_type),
|
||||||
|
customAttributeType: type,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateConditionOptions = (options, key = 'id') => {
|
||||||
|
return options.map(i => {
|
||||||
|
return {
|
||||||
|
id: i[key],
|
||||||
|
name: i.title,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getActionOptions = ({ teams, labels, type }) => {
|
||||||
|
const actionsMap = {
|
||||||
|
assign_team: teams,
|
||||||
|
send_email_to_team: teams,
|
||||||
|
add_label: generateConditionOptions(labels, 'title'),
|
||||||
|
};
|
||||||
|
return actionsMap[type];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getConditionOptions = ({
|
||||||
|
agents,
|
||||||
|
booleanFilterOptions,
|
||||||
|
campaigns,
|
||||||
|
contacts,
|
||||||
|
countries,
|
||||||
|
customAttributes,
|
||||||
|
inboxes,
|
||||||
|
languages,
|
||||||
|
statusFilterOptions,
|
||||||
|
teams,
|
||||||
|
type,
|
||||||
|
}) => {
|
||||||
|
if (isCustomAttributeCheckbox(customAttributes, type)) {
|
||||||
|
return booleanFilterOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCustomAttributeList(customAttributes, type)) {
|
||||||
|
return getCustomAttributeListDropdownValues(customAttributes, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const conditionFilterMaps = {
|
||||||
|
status: statusFilterOptions,
|
||||||
|
assignee_id: agents,
|
||||||
|
contact: contacts,
|
||||||
|
inbox_id: inboxes,
|
||||||
|
team_id: teams,
|
||||||
|
campaigns: generateConditionOptions(campaigns),
|
||||||
|
browser_language: languages,
|
||||||
|
country_code: countries,
|
||||||
|
message_type: MESSAGE_CONDITION_VALUES,
|
||||||
|
};
|
||||||
|
|
||||||
|
return conditionFilterMaps[type];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFileName = (action, files = []) => {
|
||||||
|
const blobId = action.action_params[0];
|
||||||
|
if (!blobId) return '';
|
||||||
|
if (action.action_name === 'send_attachment') {
|
||||||
|
const file = files.find(item => item.blob_id === blobId);
|
||||||
|
if (file) return file.filename.toString();
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDefaultConditions = eventName => {
|
||||||
|
if (eventName === 'message_created') {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
attribute_key: 'message_type',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: '',
|
||||||
|
query_operator: 'and',
|
||||||
|
custom_attribute_type: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
attribute_key: 'status',
|
||||||
|
filter_operator: 'equal_to',
|
||||||
|
values: '',
|
||||||
|
query_operator: 'and',
|
||||||
|
custom_attribute_type: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDefaultActions = () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
action_name: 'assign_team',
|
||||||
|
action_params: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterCustomAttributes = customAttributes => {
|
||||||
|
return customAttributes.map(attr => {
|
||||||
|
return {
|
||||||
|
key: attr.attribute_key,
|
||||||
|
name: attr.attribute_display_name,
|
||||||
|
type: attr.attribute_display_type,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStandardAttributeInputType = (automationTypes, event, key) => {
|
||||||
|
return automationTypes[event].conditions.find(item => item.key === key)
|
||||||
|
.inputType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateAutomationPayload = payload => {
|
||||||
|
const automation = JSON.parse(JSON.stringify(payload));
|
||||||
|
automation.conditions[automation.conditions.length - 1].query_operator = null;
|
||||||
|
automation.conditions = filterQueryGenerator(automation.conditions).payload;
|
||||||
|
automation.actions = actionQueryGenerator(automation.actions);
|
||||||
|
return automation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isCustomAttribute = (attrs, key) => {
|
||||||
|
return attrs.find(attr => attr.key === key);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateCustomAttributes = (
|
||||||
|
conversationAttributes = [],
|
||||||
|
contactAttribtues = [],
|
||||||
|
conversationlabel,
|
||||||
|
contactlabel
|
||||||
|
) => {
|
||||||
|
const customAttributes = [];
|
||||||
|
if (conversationAttributes.length) {
|
||||||
|
customAttributes.push(
|
||||||
|
{
|
||||||
|
key: `conversation_custom_attribute`,
|
||||||
|
name: conversationlabel,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
...conversationAttributes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (contactAttribtues.length) {
|
||||||
|
customAttributes.push(
|
||||||
|
{
|
||||||
|
key: `contact_custom_attribute`,
|
||||||
|
name: contactlabel,
|
||||||
|
disabled: true,
|
||||||
|
},
|
||||||
|
...contactAttribtues
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return customAttributes;
|
||||||
|
};
|
|
@ -1,15 +1,3 @@
|
||||||
const lowerCaseValues = (operator, values) => {
|
|
||||||
if (operator === 'equal_to' || operator === 'not_equal_to') {
|
|
||||||
values = values.map(val => {
|
|
||||||
if (typeof val === 'string') {
|
|
||||||
return val.toLowerCase();
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
};
|
|
||||||
|
|
||||||
const generatePayload = data => {
|
const generatePayload = data => {
|
||||||
// Make a copy of data to avoid vue data reactivity issues
|
// Make a copy of data to avoid vue data reactivity issues
|
||||||
const filters = JSON.parse(JSON.stringify(data));
|
const filters = JSON.parse(JSON.stringify(data));
|
||||||
|
@ -23,8 +11,6 @@ const generatePayload = data => {
|
||||||
} else {
|
} else {
|
||||||
item.values = [item.values];
|
item.values = [item.values];
|
||||||
}
|
}
|
||||||
// Convert all values to lowerCase if operator_type is 'equal_to' or 'not_equal_to'
|
|
||||||
item.values = lowerCaseValues(item.filter_operator, item.values);
|
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
// For every query added, the query_operator is set default to and so the
|
// For every query added, the query_operator is set default to and so the
|
||||||
|
|
|
@ -60,15 +60,11 @@ export const getFormattedPreChatFields = ({ preChatFields }) => {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
label: getLabel({
|
label: getLabel({
|
||||||
key: standardFieldKeys[item.name]
|
key: item.name,
|
||||||
? standardFieldKeys[item.name].key
|
|
||||||
: item.name,
|
|
||||||
label: item.label ? item.label : item.name,
|
label: item.label ? item.label : item.name,
|
||||||
}),
|
}),
|
||||||
placeholder: getPlaceHolder({
|
placeholder: getPlaceHolder({
|
||||||
key: standardFieldKeys[item.name]
|
key: item.name,
|
||||||
? standardFieldKeys[item.name].key
|
|
||||||
: item.name,
|
|
||||||
placeholder: item.placeholder ? item.placeholder : item.name,
|
placeholder: item.placeholder ? item.placeholder : item.name,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import posthog from 'posthog-js';
|
import AnalyticsHelper from './AnalyticsHelper';
|
||||||
|
|
||||||
export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER';
|
export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER';
|
||||||
export const CHATWOOT_RESET = 'CHATWOOT_RESET';
|
export const CHATWOOT_RESET = 'CHATWOOT_RESET';
|
||||||
|
@ -8,16 +8,9 @@ export const ANALYTICS_RESET = 'ANALYTICS_RESET';
|
||||||
|
|
||||||
export const initializeAnalyticsEvents = () => {
|
export const initializeAnalyticsEvents = () => {
|
||||||
window.bus.$on(ANALYTICS_IDENTITY, ({ user }) => {
|
window.bus.$on(ANALYTICS_IDENTITY, ({ user }) => {
|
||||||
if (window.analyticsConfig) {
|
AnalyticsHelper.identify(user);
|
||||||
posthog.identify(user.id, { name: user.name, email: user.email });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.bus.$on(ANALYTICS_RESET, () => {
|
|
||||||
if (window.analyticsConfig) {
|
|
||||||
posthog.reset();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
window.bus.$on(ANALYTICS_RESET, () => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initializeChatwootEvents = () => {
|
export const initializeChatwootEvents = () => {
|
||||||
|
|
|
@ -29,6 +29,12 @@ describe('#URL Helpers', () => {
|
||||||
'/app/accounts/1/team/1'
|
'/app/accounts/1/team/1'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return url to custom view', () => {
|
||||||
|
expect(conversationListPageURL({ accountId: 1, customViewId: 1 })).toBe(
|
||||||
|
'/app/accounts/1/custom_view/1'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
describe('conversationUrl', () => {
|
describe('conversationUrl', () => {
|
||||||
it('should return direct conversation URL if activeInbox is nil', () => {
|
it('should return direct conversation URL if activeInbox is nil', () => {
|
||||||
|
|
|
@ -5,7 +5,7 @@ const testData = [
|
||||||
attribute_key: 'status',
|
attribute_key: 'status',
|
||||||
filter_operator: 'equal_to',
|
filter_operator: 'equal_to',
|
||||||
values: [
|
values: [
|
||||||
{ id: 'PENDING', name: 'Pending' },
|
{ id: 'pending', name: 'Pending' },
|
||||||
{ id: 'resolved', name: 'Resolved' },
|
{ id: 'resolved', name: 'Resolved' },
|
||||||
],
|
],
|
||||||
query_operator: 'and',
|
query_operator: 'and',
|
||||||
|
@ -18,7 +18,7 @@ const testData = [
|
||||||
account_id: 1,
|
account_id: 1,
|
||||||
auto_offline: true,
|
auto_offline: true,
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
email: 'fayazara@gmail.com',
|
email: 'fayaz@test.com',
|
||||||
available_name: 'Fayaz',
|
available_name: 'Fayaz',
|
||||||
name: 'Fayaz',
|
name: 'Fayaz',
|
||||||
role: 'agent',
|
role: 'agent',
|
||||||
|
@ -52,7 +52,7 @@ const finalResult = {
|
||||||
{
|
{
|
||||||
attribute_key: 'id',
|
attribute_key: 'id',
|
||||||
filter_operator: 'equal_to',
|
filter_operator: 'equal_to',
|
||||||
values: ['this is a test'],
|
values: ['This is a test'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -70,6 +70,34 @@ export const labels = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const agents = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
account_id: 1,
|
||||||
|
availability_status: 'offline',
|
||||||
|
auto_offline: true,
|
||||||
|
confirmed: true,
|
||||||
|
email: 'john@doe.com',
|
||||||
|
available_name: 'John Doe',
|
||||||
|
name: 'John Doe',
|
||||||
|
role: 'agent',
|
||||||
|
thumbnail:
|
||||||
|
'http://localhost:3000/rails/active_storage/representations/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBUZz09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--746506837470c1a3dd063e90211ba2386963d52f/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2QzNKbGMybDZaVWtpRERJMU1IZ3lOVEFHT3daVSIsImV4cCI6bnVsbCwicHVyIjoidmFyaWF0aW9uIn19--e0e35266e8ed66e90c51be02408be8a022aca545/batman_90804.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
account_id: 1,
|
||||||
|
availability_status: 'offline',
|
||||||
|
auto_offline: true,
|
||||||
|
confirmed: true,
|
||||||
|
email: 'clark@kent.com',
|
||||||
|
available_name: 'Clark Kent',
|
||||||
|
name: 'Clark Kent',
|
||||||
|
role: 'agent',
|
||||||
|
thumbnail: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const files = [
|
export const files = [
|
||||||
{
|
{
|
||||||
id: 76,
|
id: 76,
|
||||||
|
|
|
@ -4,9 +4,10 @@ import {
|
||||||
resolveLabels,
|
resolveLabels,
|
||||||
resolveTeamIds,
|
resolveTeamIds,
|
||||||
getFileName,
|
getFileName,
|
||||||
|
resolveAgents,
|
||||||
} from '../../routes/dashboard/settings/macros/macroHelper';
|
} from '../../routes/dashboard/settings/macros/macroHelper';
|
||||||
import { MACRO_ACTION_TYPES } from '../../routes/dashboard/settings/macros/constants';
|
import { MACRO_ACTION_TYPES } from '../../routes/dashboard/settings/macros/constants';
|
||||||
import { teams, labels, files } from './macrosFixtures';
|
import { teams, labels, files, agents } from './macrosFixtures';
|
||||||
|
|
||||||
describe('#emptyMacro', () => {
|
describe('#emptyMacro', () => {
|
||||||
const defaultMacro = {
|
const defaultMacro = {
|
||||||
|
@ -52,6 +53,13 @@ describe('#resolveLabels', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#resolveAgents', () => {
|
||||||
|
it('resolves agents names from ids and returns a joined string', () => {
|
||||||
|
const resolvedAgents = 'John Doe';
|
||||||
|
expect(resolveAgents(agents, [1])).toEqual(resolvedAgents);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#getFileName', () => {
|
describe('#getFileName', () => {
|
||||||
it('returns the correct file name from the list of files', () => {
|
it('returns the correct file name from the list of files', () => {
|
||||||
expect(getFileName(files[0].blob_id, 'send_attachment', files)).toEqual(
|
expect(getFileName(files[0].blob_id, 'send_attachment', files)).toEqual(
|
||||||
|
|
|
@ -15,6 +15,7 @@ import id from './locale/id';
|
||||||
import it from './locale/it';
|
import it from './locale/it';
|
||||||
import ja from './locale/ja';
|
import ja from './locale/ja';
|
||||||
import ko from './locale/ko';
|
import ko from './locale/ko';
|
||||||
|
import lv from './locale/lv';
|
||||||
import ml from './locale/ml';
|
import ml from './locale/ml';
|
||||||
import nl from './locale/nl';
|
import nl from './locale/nl';
|
||||||
import no from './locale/no';
|
import no from './locale/no';
|
||||||
|
@ -52,6 +53,7 @@ export default {
|
||||||
ja,
|
ja,
|
||||||
ko,
|
ko,
|
||||||
ml,
|
ml,
|
||||||
|
lv,
|
||||||
nl,
|
nl,
|
||||||
no,
|
no,
|
||||||
pl,
|
pl,
|
||||||
|
|
|
@ -1,5 +1,70 @@
|
||||||
{
|
{
|
||||||
"AGENT_BOTS": {
|
"AGENT_BOTS": {
|
||||||
"HEADER": "Bots"
|
"HEADER": "Bots",
|
||||||
|
"LOADING_EDITOR": "Loading Editor...",
|
||||||
|
"HEADER_BTN_TXT": "Add Bot Configuration",
|
||||||
|
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>",
|
||||||
|
"CSML_BOT_EDITOR": {
|
||||||
|
"NAME": {
|
||||||
|
"LABEL": "Bot Name",
|
||||||
|
"PLACEHOLDER": "Give your bot a name",
|
||||||
|
"ERROR": "Bot name is required"
|
||||||
|
},
|
||||||
|
"DESCRIPTION": {
|
||||||
|
"LABEL": "Bot Description",
|
||||||
|
"PLACEHOLDER": "What does this bot do?"
|
||||||
|
},
|
||||||
|
"BOT_CONFIG": {
|
||||||
|
"ERROR": "Please enter your CSML bot configuration above",
|
||||||
|
"API_ERROR": "Your CSML configuration is invalid, please fix it and try again."
|
||||||
|
},
|
||||||
|
"SUBMIT": "Validate and save"
|
||||||
|
},
|
||||||
|
"BOT_CONFIGURATION": {
|
||||||
|
"TITLE": "Select an agent bot",
|
||||||
|
"DESC": "You can set an agent bot from the list to this inbox. The bot can initially handle the conversation and transfer it to an agent when needed.",
|
||||||
|
"SUBMIT": "تحديث",
|
||||||
|
"SUCCESS_MESSAGE": "Successfully updated the agent bot",
|
||||||
|
"ERROR_MESSAGE": "Could not update the agent bot, please try again later",
|
||||||
|
"SELECT_PLACEHOLDER": "Select Bot"
|
||||||
|
},
|
||||||
|
"ADD": {
|
||||||
|
"TITLE": "Configure new bot",
|
||||||
|
"CANCEL_BUTTON_TEXT": "إلغاء",
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Bot added successfully",
|
||||||
|
"ERROR_MESSAGE": "Could not add bot, Please try again later"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"404": "No Bots found, you can create a bot by clicking the 'Configure new bot' Button ↗",
|
||||||
|
"LOADING": "Fetching Bots...",
|
||||||
|
"TYPE": "Bot Type"
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"BUTTON_TEXT": "حذف",
|
||||||
|
"TITLE": "Delete Bot",
|
||||||
|
"SUBMIT": "حذف",
|
||||||
|
"CANCEL_BUTTON_TEXT": "إلغاء",
|
||||||
|
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible",
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Bot deleted successfully",
|
||||||
|
"ERROR_MESSAGE": "Could not able to delete bot, Please try again later"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EDIT": {
|
||||||
|
"BUTTON_TEXT": "تعديل",
|
||||||
|
"LOADING": "Fetching Bots...",
|
||||||
|
"TITLE": "Edit Bot",
|
||||||
|
"CANCEL_BUTTON_TEXT": "إلغاء",
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Bot updated successfully",
|
||||||
|
"ERROR_MESSAGE": "Could not update bot, Please try again later"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TYPES": {
|
||||||
|
"WEBHOOK": "Webhook Bot",
|
||||||
|
"CSML": "CSML Bot"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,9 @@
|
||||||
"RESET_MESSAGE": "تغيير نوع الحدث سوف يعيد تعيين الشروط والأحداث التي أضفتها أدناه"
|
"RESET_MESSAGE": "تغيير نوع الحدث سوف يعيد تعيين الشروط والأحداث التي أضفتها أدناه"
|
||||||
},
|
},
|
||||||
"CONDITION": {
|
"CONDITION": {
|
||||||
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ"
|
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ",
|
||||||
|
"CONTACT_CUSTOM_ATTR_LABEL": "Contact Custom Attributes",
|
||||||
|
"CONVERSATION_CUSTOM_ATTR_LABEL": "Conversation Custom Attributes"
|
||||||
},
|
},
|
||||||
"ACTION": {
|
"ACTION": {
|
||||||
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ",
|
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ",
|
||||||
|
@ -109,7 +111,7 @@
|
||||||
"UPLOAD_ERROR": "تعذر تحميل المرفق، الرجاء المحاولة مرة أخرى",
|
"UPLOAD_ERROR": "تعذر تحميل المرفق، الرجاء المحاولة مرة أخرى",
|
||||||
"LABEL_IDLE": "ارفع المرفق",
|
"LABEL_IDLE": "ارفع المرفق",
|
||||||
"LABEL_UPLOADING": "جاري الرفع...",
|
"LABEL_UPLOADING": "جاري الرفع...",
|
||||||
"LABEL_UPLOADED": "تم الرفع بنجاح",
|
"LABEL_UPLOADED": "Successfully Uploaded",
|
||||||
"LABEL_UPLOAD_FAILED": "فشل الرفع"
|
"LABEL_UPLOAD_FAILED": "فشل الرفع"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"ASSIGN_LABEL": "تكليف",
|
"ASSIGN_LABEL": "تكليف",
|
||||||
"YES": "نعم",
|
"YES": "نعم",
|
||||||
"ASSIGN_AGENT_TOOLTIP": "إسناد وكيل",
|
"ASSIGN_AGENT_TOOLTIP": "إسناد وكيل",
|
||||||
|
"ASSIGN_TEAM_TOOLTIP": "تعيين فريق",
|
||||||
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح",
|
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح",
|
||||||
"ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى",
|
"ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى",
|
||||||
"RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح",
|
"RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح",
|
||||||
|
@ -26,6 +27,14 @@
|
||||||
"ASSIGN_SELECTED_LABELS": "تعيين التسميات المحددة",
|
"ASSIGN_SELECTED_LABELS": "تعيين التسميات المحددة",
|
||||||
"ASSIGN_SUCCESFUL": "تم تعيين التسميات بنجاح",
|
"ASSIGN_SUCCESFUL": "تم تعيين التسميات بنجاح",
|
||||||
"ASSIGN_FAILED": "فشل في تعيين التسميات ، الرجاء المحاولة مرة أخرى"
|
"ASSIGN_FAILED": "فشل في تعيين التسميات ، الرجاء المحاولة مرة أخرى"
|
||||||
|
},
|
||||||
|
"TEAMS": {
|
||||||
|
"TEAM_SELECT_LABEL": "اختيار فريق",
|
||||||
|
"NONE": "لا شيء",
|
||||||
|
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
|
||||||
|
"ASSIGN_SELECTED_TEAMS": "Assign selected team",
|
||||||
|
"ASSIGN_SUCCESFUL": "Teams assiged successfully",
|
||||||
|
"ASSIGN_FAILED": "Failed to assign team, please try again"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
},
|
},
|
||||||
"TAB_HEADING": "المحادثات",
|
"TAB_HEADING": "المحادثات",
|
||||||
"MENTION_HEADING": "الإشارات",
|
"MENTION_HEADING": "الإشارات",
|
||||||
|
"UNATTENDED_HEADING": "بدون حضور",
|
||||||
"SEARCH": {
|
"SEARCH": {
|
||||||
"INPUT": "البحث عن جهات الاتصال، المحادثات، قوالب الردود الجاهزة .."
|
"INPUT": "البحث عن جهات الاتصال، المحادثات، قوالب الردود الجاهزة .."
|
||||||
},
|
},
|
||||||
|
@ -56,6 +57,8 @@
|
||||||
"REPLY_TO_TWEET": "الرد على هذه التغريدة",
|
"REPLY_TO_TWEET": "الرد على هذه التغريدة",
|
||||||
"LINK_TO_STORY": "الذهاب إلى قصة الإنستقرام",
|
"LINK_TO_STORY": "الذهاب إلى قصة الإنستقرام",
|
||||||
"SENT": "Sent successfully",
|
"SENT": "Sent successfully",
|
||||||
|
"READ": "Read successfully",
|
||||||
|
"DELIVERED": "Delivered successfully",
|
||||||
"NO_MESSAGES": "لا توجد رسائل",
|
"NO_MESSAGES": "لا توجد رسائل",
|
||||||
"NO_CONTENT": "لم يتم العثور على محتوى",
|
"NO_CONTENT": "لم يتم العثور على محتوى",
|
||||||
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
||||||
|
|
|
@ -41,6 +41,10 @@
|
||||||
"NO_RESPONSE": "لا توجد استجابة",
|
"NO_RESPONSE": "لا توجد استجابة",
|
||||||
"RATING_TITLE": "التقييم",
|
"RATING_TITLE": "التقييم",
|
||||||
"FEEDBACK_TITLE": "الملاحظات",
|
"FEEDBACK_TITLE": "الملاحظات",
|
||||||
|
"CARD": {
|
||||||
|
"SHOW_LABELS": "Show labels",
|
||||||
|
"HIDE_LABELS": "Hide labels"
|
||||||
|
},
|
||||||
"HEADER": {
|
"HEADER": {
|
||||||
"RESOLVE_ACTION": "إغلاق المحادثة",
|
"RESOLVE_ACTION": "إغلاق المحادثة",
|
||||||
"REOPEN_ACTION": "إعادة فتح",
|
"REOPEN_ACTION": "إعادة فتح",
|
||||||
|
@ -64,6 +68,7 @@
|
||||||
"CARD_CONTEXT_MENU": {
|
"CARD_CONTEXT_MENU": {
|
||||||
"PENDING": "تحديد كمعلق",
|
"PENDING": "تحديد كمعلق",
|
||||||
"RESOLVED": "تحديد كمحلولة",
|
"RESOLVED": "تحديد كمحلولة",
|
||||||
|
"MARK_AS_UNREAD": "Mark as unread",
|
||||||
"REOPEN": "إعادة فتح المحادثة",
|
"REOPEN": "إعادة فتح المحادثة",
|
||||||
"SNOOZE": {
|
"SNOOZE": {
|
||||||
"TITLE": "غفوة",
|
"TITLE": "غفوة",
|
||||||
|
@ -208,7 +213,8 @@
|
||||||
"CONVERSATION_LABELS": "وسوم المحادثة",
|
"CONVERSATION_LABELS": "وسوم المحادثة",
|
||||||
"CONVERSATION_INFO": "معلومات المحادثة",
|
"CONVERSATION_INFO": "معلومات المحادثة",
|
||||||
"CONTACT_ATTRIBUTES": "سمات جهة الاتصال",
|
"CONTACT_ATTRIBUTES": "سمات جهة الاتصال",
|
||||||
"PREVIOUS_CONVERSATION": "المحادثات السابقة"
|
"PREVIOUS_CONVERSATION": "المحادثات السابقة",
|
||||||
|
"MACROS": "Macros"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
"CONVERSATION_CUSTOM_ATTRIBUTES": {
|
||||||
|
|
6
app/javascript/dashboard/i18n/locale/ar/emoji.json
Normal file
6
app/javascript/dashboard/i18n/locale/ar/emoji.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"EMOJI": {
|
||||||
|
"PLACEHOLDER": "Search emojis",
|
||||||
|
"NOT_FOUND": "No emoji match your search"
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,7 @@
|
||||||
"ERROR": "الرجاء إدخال اسم حساب صحيح"
|
"ERROR": "الرجاء إدخال اسم حساب صحيح"
|
||||||
},
|
},
|
||||||
"LANGUAGE": {
|
"LANGUAGE": {
|
||||||
"LABEL": "لغة الموقع (تجريبي)",
|
"LABEL": "Site language",
|
||||||
"PLACEHOLDER": "اسم الحساب الخاص بك",
|
"PLACEHOLDER": "اسم الحساب الخاص بك",
|
||||||
"ERROR": ""
|
"ERROR": ""
|
||||||
},
|
},
|
||||||
|
@ -54,7 +54,8 @@
|
||||||
"MULTISELECT": {
|
"MULTISELECT": {
|
||||||
"ENTER_TO_SELECT": "اضغط على زر الإدخال للاختيار",
|
"ENTER_TO_SELECT": "اضغط على زر الإدخال للاختيار",
|
||||||
"ENTER_TO_REMOVE": "اضغط على زر الإدخال للحذف",
|
"ENTER_TO_REMOVE": "اضغط على زر الإدخال للحذف",
|
||||||
"SELECT_ONE": "اختر واحدا"
|
"SELECT_ONE": "اختر واحدا",
|
||||||
|
"SELECT": "Select"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"NOTIFICATIONS_PAGE": {
|
"NOTIFICATIONS_PAGE": {
|
||||||
|
@ -136,5 +137,8 @@
|
||||||
"UNTIL_NEXT_WEEK": "حتى الأسبوع القادم",
|
"UNTIL_NEXT_WEEK": "حتى الأسبوع القادم",
|
||||||
"UNTIL_TOMORROW": "حتى الغد"
|
"UNTIL_TOMORROW": "حتى الغد"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"DASHBOARD_APPS": {
|
||||||
|
"LOADING_MESSAGE": "Loading Dashboard App..."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -217,14 +217,14 @@
|
||||||
"DOMAIN": {
|
"DOMAIN": {
|
||||||
"LABEL": "نطاق مخصص",
|
"LABEL": "نطاق مخصص",
|
||||||
"PLACEHOLDER": "نطاق البوابة المخصص",
|
"PLACEHOLDER": "نطاق البوابة المخصص",
|
||||||
"HELP_TEXT": "أضف فقط إذا كنت ترغب في استخدام نطاق مخصص للبوابات الخاصة بك.",
|
"HELP_TEXT": "Add only If you want to use a custom domain for your portals. Eg: https://example.com",
|
||||||
"ERROR": "النطاق المخصص مطلوب"
|
"ERROR": "Enter a valid domain URL"
|
||||||
},
|
},
|
||||||
"HOME_PAGE_LINK": {
|
"HOME_PAGE_LINK": {
|
||||||
"LABEL": "رابط الصفحة الرئيسية",
|
"LABEL": "رابط الصفحة الرئيسية",
|
||||||
"PLACEHOLDER": "رابط الصفحة الرئيسية للبوابة",
|
"PLACEHOLDER": "رابط الصفحة الرئيسية للبوابة",
|
||||||
"HELP_TEXT": "الرابط المستخدم للعودة من البوابة إلى الصفحة الرئيسية.",
|
"HELP_TEXT": "The link used to return from the portal to the home page. Eg: https://example.com",
|
||||||
"ERROR": "رابط الصفحة الرئيسية مطلوب"
|
"ERROR": "Enter a valid home page URL"
|
||||||
},
|
},
|
||||||
"THEME_COLOR": {
|
"THEME_COLOR": {
|
||||||
"LABEL": "لون قالب البوابة",
|
"LABEL": "لون قالب البوابة",
|
||||||
|
|
|
@ -134,7 +134,7 @@
|
||||||
"PHONE_NUMBER": {
|
"PHONE_NUMBER": {
|
||||||
"LABEL": "رقم الهاتف",
|
"LABEL": "رقم الهاتف",
|
||||||
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
|
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
|
||||||
"ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
|
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
|
||||||
},
|
},
|
||||||
"API_CALLBACK": {
|
"API_CALLBACK": {
|
||||||
"TITLE": "عنوان Callback URL",
|
"TITLE": "عنوان Callback URL",
|
||||||
|
@ -185,7 +185,7 @@
|
||||||
"PHONE_NUMBER": {
|
"PHONE_NUMBER": {
|
||||||
"LABEL": "رقم الهاتف",
|
"LABEL": "رقم الهاتف",
|
||||||
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
|
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
|
||||||
"ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
|
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
|
||||||
},
|
},
|
||||||
"SUBMIT_BUTTON": "إنشاء قناة عرض التردد",
|
"SUBMIT_BUTTON": "إنشاء قناة عرض التردد",
|
||||||
"API": {
|
"API": {
|
||||||
|
@ -214,7 +214,7 @@
|
||||||
"PHONE_NUMBER": {
|
"PHONE_NUMBER": {
|
||||||
"LABEL": "رقم الهاتف",
|
"LABEL": "رقم الهاتف",
|
||||||
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
|
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
|
||||||
"ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
|
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces."
|
||||||
},
|
},
|
||||||
"PHONE_NUMBER_ID": {
|
"PHONE_NUMBER_ID": {
|
||||||
"LABEL": "رقم الهاتف",
|
"LABEL": "رقم الهاتف",
|
||||||
|
@ -388,6 +388,10 @@
|
||||||
"ENABLED": "مفعل",
|
"ENABLED": "مفعل",
|
||||||
"DISABLED": "معطّل"
|
"DISABLED": "معطّل"
|
||||||
},
|
},
|
||||||
|
"LOCK_TO_SINGLE_CONVERSATION": {
|
||||||
|
"ENABLED": "مفعل",
|
||||||
|
"DISABLED": "معطّل"
|
||||||
|
},
|
||||||
"ENABLE_HMAC": {
|
"ENABLE_HMAC": {
|
||||||
"LABEL": "تمكين"
|
"LABEL": "تمكين"
|
||||||
}
|
}
|
||||||
|
@ -416,7 +420,8 @@
|
||||||
"CAMPAIGN": "الحملات",
|
"CAMPAIGN": "الحملات",
|
||||||
"PRE_CHAT_FORM": "نموذج ما قبل الدردشة",
|
"PRE_CHAT_FORM": "نموذج ما قبل الدردشة",
|
||||||
"BUSINESS_HOURS": "ساعات العمل",
|
"BUSINESS_HOURS": "ساعات العمل",
|
||||||
"WIDGET_BUILDER": "منشئ اللايف شات"
|
"WIDGET_BUILDER": "منشئ اللايف شات",
|
||||||
|
"BOT_CONFIGURATION": "Bot Configuration"
|
||||||
},
|
},
|
||||||
"SETTINGS": "الإعدادات",
|
"SETTINGS": "الإعدادات",
|
||||||
"FEATURES": {
|
"FEATURES": {
|
||||||
|
@ -440,6 +445,8 @@
|
||||||
"ENABLE_CSAT_SUB_TEXT": "تمكين/تعطيل تقييم خدمة العملاء بعد إنتهاء المحادثة",
|
"ENABLE_CSAT_SUB_TEXT": "تمكين/تعطيل تقييم خدمة العملاء بعد إنتهاء المحادثة",
|
||||||
"ENABLE_CONTINUITY_VIA_EMAIL": "تمكين استمرارية المحادثة عبر البريد الإلكتروني",
|
"ENABLE_CONTINUITY_VIA_EMAIL": "تمكين استمرارية المحادثة عبر البريد الإلكتروني",
|
||||||
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "المحادثات ستستمر عبر البريد الإلكتروني إذا كان عنوان البريد الإلكتروني لجهة الاتصال متاحاً.",
|
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "المحادثات ستستمر عبر البريد الإلكتروني إذا كان عنوان البريد الإلكتروني لجهة الاتصال متاحاً.",
|
||||||
|
"LOCK_TO_SINGLE_CONVERSATION": "Lock to single conversation",
|
||||||
|
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "Enable or disable multiple conversations for the same contact in this inbox",
|
||||||
"INBOX_UPDATE_TITLE": "إعدادات قناة التواصل",
|
"INBOX_UPDATE_TITLE": "إعدادات قناة التواصل",
|
||||||
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
|
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
|
||||||
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
|
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
|
||||||
|
|
|
@ -1,5 +1,78 @@
|
||||||
{
|
{
|
||||||
"MACROS": {
|
"MACROS": {
|
||||||
"HEADER": "Macros"
|
"HEADER": "Macros",
|
||||||
|
"HEADER_BTN_TXT": "Add a new macro",
|
||||||
|
"HEADER_BTN_TXT_SAVE": "Save macro",
|
||||||
|
"LOADING": "Fetching macros",
|
||||||
|
"SIDEBAR_TXT": "<p><b>Macros</b><p>A macro is a set of saved actions that help customer service agents easily complete tasks. The agents can define a set of actions like tagging a conversation with a label, sending an email transcript, updating a custom attribute, etc., and they can run these actions in a single click. When the agents run the macro, the actions would be performed sequentially in the order they are defined. Macros improve productivity and increase consistency in actions. </p><p>A macro can be helpful in 2 ways. </p><p><b>As an agent assist:</b> If an agent performs a set of actions multiple times, they can save it as a macro and execute all the actions together using a single click.</p><p><b>As an option to onboard a team member:</b> Every agent has to perform many different checks/actions during each conversation. Onboarding a new support team member will be easy if pre-defined macros are available on the account. Instead of describing each step in detail, the manager/team lead can point to the macros used in different scenarios.</p>",
|
||||||
|
"ERROR": "Something went wrong. Please try again",
|
||||||
|
"ORDER_INFO": "Macros will run in the order you add your actions. You can rearrange them by dragging them by the handle beside each node.",
|
||||||
|
"ADD": {
|
||||||
|
"FORM": {
|
||||||
|
"NAME": {
|
||||||
|
"LABEL": "Macro name",
|
||||||
|
"PLACEHOLDER": "Enter a name for your macro",
|
||||||
|
"ERROR": "Name is required for creating a macro"
|
||||||
|
},
|
||||||
|
"ACTIONS": {
|
||||||
|
"LABEL": "الإجراءات"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Macro added successfully",
|
||||||
|
"ERROR_MESSAGE": "Unable to create macro, Please try again later"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"LIST": {
|
||||||
|
"TABLE_HEADER": [
|
||||||
|
"الاسم",
|
||||||
|
"Created by",
|
||||||
|
"Last updated by",
|
||||||
|
"Visibility"
|
||||||
|
],
|
||||||
|
"404": "No macros found"
|
||||||
|
},
|
||||||
|
"DELETE": {
|
||||||
|
"TOOLTIP": "Delete macro",
|
||||||
|
"CONFIRM": {
|
||||||
|
"MESSAGE": "هل أنت متأكد من الحذف ",
|
||||||
|
"YES": "نعم، احذف",
|
||||||
|
"NO": "لا"
|
||||||
|
},
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Macro deleted successfully",
|
||||||
|
"ERROR_MESSAGE": "There was an error deleting the macro. Please try again later"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EDIT": {
|
||||||
|
"TOOLTIP": "Edit macro",
|
||||||
|
"API": {
|
||||||
|
"SUCCESS_MESSAGE": "Macro updated successfully",
|
||||||
|
"ERROR_MESSAGE": "Could not update Macro, Please try again later"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EDITOR": {
|
||||||
|
"START_FLOW": "Start Flow",
|
||||||
|
"END_FLOW": "End Flow",
|
||||||
|
"LOADING": "Fetching macro",
|
||||||
|
"ADD_BTN_TOOLTIP": "Add new action",
|
||||||
|
"DELETE_BTN_TOOLTIP": "Delete Action",
|
||||||
|
"VISIBILITY": {
|
||||||
|
"LABEL": "Macro Visibility",
|
||||||
|
"GLOBAL": {
|
||||||
|
"LABEL": "Public",
|
||||||
|
"DESCRIPTION": "This macro is available publicly for all agents in this account."
|
||||||
|
},
|
||||||
|
"PERSONAL": {
|
||||||
|
"LABEL": "Private",
|
||||||
|
"DESCRIPTION": "This macro will be private to you and not be available to others."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EXECUTE": {
|
||||||
|
"BUTTON_TOOLTIP": "Execute",
|
||||||
|
"PREVIEW": "Preview Macro",
|
||||||
|
"EXECUTED_SUCCESSFULLY": "Macro executed successfully"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -103,7 +103,9 @@
|
||||||
"متصل",
|
"متصل",
|
||||||
"مشغول",
|
"مشغول",
|
||||||
"غير متصل"
|
"غير متصل"
|
||||||
]
|
],
|
||||||
|
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
|
||||||
|
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again"
|
||||||
},
|
},
|
||||||
"EMAIL": {
|
"EMAIL": {
|
||||||
"LABEL": "عنوان البريد الإلكتروني الخاص بك",
|
"LABEL": "عنوان البريد الإلكتروني الخاص بك",
|
||||||
|
@ -134,6 +136,7 @@
|
||||||
"SELECTOR_SUBTITLE": "اختر حساباً من القائمة التالية",
|
"SELECTOR_SUBTITLE": "اختر حساباً من القائمة التالية",
|
||||||
"PROFILE_SETTINGS": "إعدادات الملف الشخصي",
|
"PROFILE_SETTINGS": "إعدادات الملف الشخصي",
|
||||||
"KEYBOARD_SHORTCUTS": "اختصارات لوحة المفاتيح",
|
"KEYBOARD_SHORTCUTS": "اختصارات لوحة المفاتيح",
|
||||||
|
"SUPER_ADMIN_CONSOLE": "Super Admin Console",
|
||||||
"LOGOUT": "تسجيل الخروج"
|
"LOGOUT": "تسجيل الخروج"
|
||||||
},
|
},
|
||||||
"APP_GLOBAL": {
|
"APP_GLOBAL": {
|
||||||
|
@ -158,6 +161,9 @@
|
||||||
"DOWNLOAD": "تنزيل",
|
"DOWNLOAD": "تنزيل",
|
||||||
"UPLOADING": "جاري الرفع..."
|
"UPLOADING": "جاري الرفع..."
|
||||||
},
|
},
|
||||||
|
"LOCATION_BUBBLE": {
|
||||||
|
"SEE_ON_MAP": "See on map"
|
||||||
|
},
|
||||||
"FORM_BUBBLE": {
|
"FORM_BUBBLE": {
|
||||||
"SUBMIT": "إرسال"
|
"SUBMIT": "إرسال"
|
||||||
}
|
}
|
||||||
|
@ -174,6 +180,7 @@
|
||||||
"CONVERSATIONS": "المحادثات",
|
"CONVERSATIONS": "المحادثات",
|
||||||
"ALL_CONVERSATIONS": "كل المحادثات",
|
"ALL_CONVERSATIONS": "كل المحادثات",
|
||||||
"MENTIONED_CONVERSATIONS": "الإشارات",
|
"MENTIONED_CONVERSATIONS": "الإشارات",
|
||||||
|
"UNATTENDED_CONVERSATIONS": "بدون حضور",
|
||||||
"REPORTS": "التقارير",
|
"REPORTS": "التقارير",
|
||||||
"SETTINGS": "الإعدادات",
|
"SETTINGS": "الإعدادات",
|
||||||
"CONTACTS": "جهات الاتصال",
|
"CONTACTS": "جهات الاتصال",
|
||||||
|
@ -222,6 +229,10 @@
|
||||||
"CATEGORY": "الفئة",
|
"CATEGORY": "الفئة",
|
||||||
"CATEGORY_EMPTY_MESSAGE": "لم يتم العثور على فئات"
|
"CATEGORY_EMPTY_MESSAGE": "لم يتم العثور على فئات"
|
||||||
},
|
},
|
||||||
|
"SET_AUTO_OFFLINE": {
|
||||||
|
"TEXT": "Mark offline automatically",
|
||||||
|
"INFO_TEXT": "Let the system automatically mark you offline when you aren't using the app or dashboard."
|
||||||
|
},
|
||||||
"DOCS": "قراءة المستندات"
|
"DOCS": "قراءة المستندات"
|
||||||
},
|
},
|
||||||
"BILLING_SETTINGS": {
|
"BILLING_SETTINGS": {
|
||||||
|
@ -253,7 +264,7 @@
|
||||||
},
|
},
|
||||||
"FORM": {
|
"FORM": {
|
||||||
"NAME": {
|
"NAME": {
|
||||||
"LABEL": "اسم الحساب",
|
"LABEL": "اسم الشركة",
|
||||||
"PLACEHOLDER": "مؤسسة Wayne"
|
"PLACEHOLDER": "مؤسسة Wayne"
|
||||||
},
|
},
|
||||||
"SUBMIT": "إرسال"
|
"SUBMIT": "إرسال"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue