Merge branch 'release/2.6.0'
This commit is contained in:
commit
374b367115
715 changed files with 10996 additions and 3230 deletions
|
@ -7,7 +7,7 @@ defaults: &defaults
|
||||||
working_directory: ~/build
|
working_directory: ~/build
|
||||||
docker:
|
docker:
|
||||||
# specify the version you desire here
|
# specify the version you desire here
|
||||||
- image: cimg/ruby:3.0.2-browsers
|
- image: cimg/ruby:3.0.4-browsers
|
||||||
|
|
||||||
# Specify service dependencies here if necessary
|
# Specify service dependencies here if necessary
|
||||||
# CircleCI maintains a library of pre-built images
|
# CircleCI maintains a library of pre-built images
|
||||||
|
@ -40,14 +40,13 @@ jobs:
|
||||||
|
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
keys:
|
keys:
|
||||||
- chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-{{ checksum "Gemfile.lock" }}
|
- chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
||||||
- chatwoot-bundle
|
|
||||||
|
|
||||||
- run: bundle install --frozen --path ~/.bundle
|
- run: bundle install --frozen --path ~/.bundle
|
||||||
- save_cache:
|
- save_cache:
|
||||||
paths:
|
paths:
|
||||||
- ~/.bundle
|
- ~/.bundle
|
||||||
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-{{ checksum "Gemfile.lock" }}
|
key: chatwoot-bundle-{{ .Environment.CACHE_VERSION }}-v20220524-{{ checksum "Gemfile.lock" }}
|
||||||
|
|
||||||
|
|
||||||
# Only necessary if app uses webpacker or yarn in some other way
|
# Only necessary if app uses webpacker or yarn in some other way
|
||||||
|
|
36
.eslintrc.js
36
.eslintrc.js
|
@ -19,18 +19,32 @@ module.exports = {
|
||||||
'jsx-a11y/label-has-for': 'off',
|
'jsx-a11y/label-has-for': 'off',
|
||||||
'jsx-a11y/anchor-is-valid': 'off',
|
'jsx-a11y/anchor-is-valid': 'off',
|
||||||
'import/no-unresolved': 'off',
|
'import/no-unresolved': 'off',
|
||||||
'vue/max-attributes-per-line': ['error', {
|
'vue/max-attributes-per-line': [
|
||||||
'singleline': 20,
|
'error',
|
||||||
'multiline': {
|
{
|
||||||
'max': 1,
|
singleline: 20,
|
||||||
'allowFirstLine': false
|
multiline: {
|
||||||
|
max: 1,
|
||||||
|
allowFirstLine: false,
|
||||||
},
|
},
|
||||||
}],
|
},
|
||||||
'vue/html-self-closing': 'off',
|
],
|
||||||
"vue/no-v-html": 'off',
|
'vue/html-self-closing': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
html: {
|
||||||
|
void: 'always',
|
||||||
|
normal: 'always',
|
||||||
|
component: 'always',
|
||||||
|
},
|
||||||
|
svg: 'always',
|
||||||
|
math: 'always',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'vue/no-v-html': 'off',
|
||||||
'vue/singleline-html-element-content-newline': 'off',
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
'import/extensions': ['off'],
|
'import/extensions': ['off'],
|
||||||
'no-console': 'error'
|
'no-console': 'error',
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
'import/resolver': {
|
'import/resolver': {
|
||||||
|
@ -41,12 +55,10 @@ module.exports = {
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
|
||||||
jest: true,
|
jest: true,
|
||||||
jasmine: true
|
node: true,
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
__WEBPACK_ENV__: true,
|
|
||||||
bus: true,
|
bus: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
2
.github/workflows/run_foss_spec.yml
vendored
2
.github/workflows/run_foss_spec.yml
vendored
|
@ -47,7 +47,7 @@ jobs:
|
||||||
|
|
||||||
- uses: ruby/setup-ruby@v1
|
- uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.0.2 # Not needed with a .ruby-version file
|
ruby-version: 3.0.4 # Not needed with a .ruby-version file
|
||||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||||
|
|
||||||
- name: yarn
|
- name: yarn
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
3.0.2
|
3.0.4
|
||||||
|
|
10
Gemfile
10
Gemfile
|
@ -1,6 +1,6 @@
|
||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
ruby '3.0.2'
|
ruby '3.0.4'
|
||||||
|
|
||||||
##-- base gems for rails --##
|
##-- base gems for rails --##
|
||||||
gem 'rack-cors', require: 'rack/cors'
|
gem 'rack-cors', require: 'rack/cors'
|
||||||
|
@ -128,6 +128,9 @@ gem 'html2text'
|
||||||
# to calculate working hours
|
# to calculate working hours
|
||||||
gem 'working_hours'
|
gem 'working_hours'
|
||||||
|
|
||||||
|
# full text search for articles
|
||||||
|
gem 'pg_search'
|
||||||
|
|
||||||
group :production, :staging do
|
group :production, :staging do
|
||||||
# we dont want request timing out in development while using byebug
|
# we dont want request timing out in development while using byebug
|
||||||
gem 'rack-timeout'
|
gem 'rack-timeout'
|
||||||
|
@ -156,11 +159,6 @@ group :test do
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
# TODO: is this needed ?
|
|
||||||
# errors thrown by devise password gem
|
|
||||||
gem 'flay'
|
|
||||||
gem 'rspec'
|
|
||||||
# for error thrown by devise password gem
|
|
||||||
gem 'active_record_query_trace'
|
gem 'active_record_query_trace'
|
||||||
gem 'bundle-audit', require: false
|
gem 'bundle-audit', require: false
|
||||||
gem 'byebug', platform: :mri
|
gem 'byebug', platform: :mri
|
||||||
|
|
35
Gemfile.lock
35
Gemfile.lock
|
@ -1,6 +1,6 @@
|
||||||
GIT
|
GIT
|
||||||
remote: https://github.com/chatwoot/devise-secure_password
|
remote: https://github.com/chatwoot/devise-secure_password
|
||||||
revision: de11e8765654b8242d42101ee9c8ffc8126f7975
|
revision: d777b04f12652d576b1272b8f39857e3e0b3fc26
|
||||||
specs:
|
specs:
|
||||||
devise-secure_password (2.0.1)
|
devise-secure_password (2.0.1)
|
||||||
devise (>= 4.0.0, < 5.0.0)
|
devise (>= 4.0.0, < 5.0.0)
|
||||||
|
@ -182,7 +182,6 @@ GEM
|
||||||
regexp_parser (~> 2.2)
|
regexp_parser (~> 2.2)
|
||||||
email_reply_trimmer (0.1.13)
|
email_reply_trimmer (0.1.13)
|
||||||
erubi (1.10.0)
|
erubi (1.10.0)
|
||||||
erubis (2.7.0)
|
|
||||||
et-orbi (1.2.7)
|
et-orbi (1.2.7)
|
||||||
tzinfo
|
tzinfo
|
||||||
execjs (2.8.1)
|
execjs (2.8.1)
|
||||||
|
@ -204,11 +203,6 @@ GEM
|
||||||
faraday (~> 1)
|
faraday (~> 1)
|
||||||
ffi (1.15.5)
|
ffi (1.15.5)
|
||||||
flag_shih_tzu (0.3.23)
|
flag_shih_tzu (0.3.23)
|
||||||
flay (2.12.1)
|
|
||||||
erubis (~> 2.7.0)
|
|
||||||
path_expander (~> 1.0)
|
|
||||||
ruby_parser (~> 3.0)
|
|
||||||
sexp_processor (~> 4.0)
|
|
||||||
foreman (0.87.2)
|
foreman (0.87.2)
|
||||||
fugit (1.5.3)
|
fugit (1.5.3)
|
||||||
et-orbi (~> 1, >= 1.2.7)
|
et-orbi (~> 1, >= 1.2.7)
|
||||||
|
@ -309,7 +303,7 @@ GEM
|
||||||
jbuilder (2.11.5)
|
jbuilder (2.11.5)
|
||||||
actionview (>= 5.0.0)
|
actionview (>= 5.0.0)
|
||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
jmespath (1.6.0)
|
jmespath (1.6.1)
|
||||||
jquery-rails (4.4.0)
|
jquery-rails (4.4.0)
|
||||||
rails-dom-testing (>= 1, < 3)
|
rails-dom-testing (>= 1, < 3)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
|
@ -378,14 +372,14 @@ GEM
|
||||||
netrc (0.11.0)
|
netrc (0.11.0)
|
||||||
newrelic_rpm (8.7.0)
|
newrelic_rpm (8.7.0)
|
||||||
nio4r (2.5.8)
|
nio4r (2.5.8)
|
||||||
nokogiri (1.13.5)
|
nokogiri (1.13.6)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.5-arm64-darwin)
|
nokogiri (1.13.6-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.5-x86_64-darwin)
|
nokogiri (1.13.6-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.13.5-x86_64-linux)
|
nokogiri (1.13.6-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth (0.5.8)
|
oauth (0.5.8)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
|
@ -393,8 +387,10 @@ GEM
|
||||||
parallel (1.21.0)
|
parallel (1.21.0)
|
||||||
parser (3.1.1.0)
|
parser (3.1.1.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
path_expander (1.1.0)
|
|
||||||
pg (1.3.2)
|
pg (1.3.2)
|
||||||
|
pg_search (2.3.6)
|
||||||
|
activerecord (>= 5.2)
|
||||||
|
activesupport (>= 5.2)
|
||||||
procore-sift (0.16.0)
|
procore-sift (0.16.0)
|
||||||
rails (> 4.2.0)
|
rails (> 4.2.0)
|
||||||
pry (0.14.1)
|
pry (0.14.1)
|
||||||
|
@ -409,7 +405,7 @@ GEM
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.6.0)
|
racc (1.6.0)
|
||||||
rack (2.2.3)
|
rack (2.2.3.1)
|
||||||
rack-attack (6.6.0)
|
rack-attack (6.6.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rack-cors (1.1.1)
|
rack-cors (1.1.1)
|
||||||
|
@ -468,10 +464,6 @@ GEM
|
||||||
netrc (~> 0.8)
|
netrc (~> 0.8)
|
||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.5)
|
rexml (3.2.5)
|
||||||
rspec (3.11.0)
|
|
||||||
rspec-core (~> 3.11.0)
|
|
||||||
rspec-expectations (~> 3.11.0)
|
|
||||||
rspec-mocks (~> 3.11.0)
|
|
||||||
rspec-core (3.11.0)
|
rspec-core (3.11.0)
|
||||||
rspec-support (~> 3.11.0)
|
rspec-support (~> 3.11.0)
|
||||||
rspec-expectations (3.11.0)
|
rspec-expectations (3.11.0)
|
||||||
|
@ -681,7 +673,6 @@ DEPENDENCIES
|
||||||
faker
|
faker
|
||||||
fcm
|
fcm
|
||||||
flag_shih_tzu
|
flag_shih_tzu
|
||||||
flay
|
|
||||||
foreman
|
foreman
|
||||||
geocoder
|
geocoder
|
||||||
google-cloud-dialogflow
|
google-cloud-dialogflow
|
||||||
|
@ -706,6 +697,7 @@ DEPENDENCIES
|
||||||
mock_redis
|
mock_redis
|
||||||
newrelic_rpm
|
newrelic_rpm
|
||||||
pg
|
pg
|
||||||
|
pg_search
|
||||||
procore-sift
|
procore-sift
|
||||||
pry-rails
|
pry-rails
|
||||||
puma
|
puma
|
||||||
|
@ -718,7 +710,6 @@ DEPENDENCIES
|
||||||
redis-namespace
|
redis-namespace
|
||||||
responders
|
responders
|
||||||
rest-client
|
rest-client
|
||||||
rspec
|
|
||||||
rspec-rails (~> 5.0.0)
|
rspec-rails (~> 5.0.0)
|
||||||
rubocop
|
rubocop
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
|
@ -752,7 +743,7 @@ DEPENDENCIES
|
||||||
working_hours
|
working_hours
|
||||||
|
|
||||||
RUBY VERSION
|
RUBY VERSION
|
||||||
ruby 3.0.2p107
|
ruby 3.0.4p208
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.3.10
|
2.3.15
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
|
||||||
end
|
end
|
||||||
ensure_contact_avatar
|
ensure_contact_avatar
|
||||||
rescue Koala::Facebook::AuthenticationError
|
rescue Koala::Facebook::AuthenticationError
|
||||||
Rails.logger.error "Facebook Authorization expired for Inbox #{@inbox.id}"
|
@inbox.channel.authorization_error!
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
|
||||||
true
|
true
|
||||||
|
|
|
@ -73,6 +73,10 @@ class Messages::MessageBuilder
|
||||||
@params[:campaign_id].present? ? { additional_attributes: { campaign_id: @params[:campaign_id] } } : {}
|
@params[:campaign_id].present? ? { additional_attributes: { campaign_id: @params[:campaign_id] } } : {}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def template_params
|
||||||
|
@params[:template_params].present? ? { additional_attributes: { template_params: JSON.parse(@params[:template_params].to_json) } } : {}
|
||||||
|
end
|
||||||
|
|
||||||
def message_sender
|
def message_sender
|
||||||
return if @params[:sender_type] != 'AgentBot'
|
return if @params[:sender_type] != 'AgentBot'
|
||||||
|
|
||||||
|
@ -91,6 +95,6 @@ class Messages::MessageBuilder
|
||||||
items: @items,
|
items: @items,
|
||||||
in_reply_to: @in_reply_to,
|
in_reply_to: @in_reply_to,
|
||||||
echo_id: @params[:echo_id]
|
echo_id: @params[:echo_id]
|
||||||
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id)
|
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
48
app/controllers/api/v1/accounts/articles_controller.rb
Normal file
48
app/controllers/api/v1/accounts/articles_controller.rb
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :portal
|
||||||
|
before_action :fetch_article, except: [:index, :create]
|
||||||
|
|
||||||
|
def index
|
||||||
|
@articles = @portal.articles
|
||||||
|
@articles.search(list_params) if params[:payload].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@article = @portal.articles.create!(article_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def edit; end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@article.update!(article_params)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@article.destroy!
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_article
|
||||||
|
@article = @portal.articles.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def portal
|
||||||
|
@portal ||= Current.account.portals.find_by(slug: params[:portal_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def article_params
|
||||||
|
params.require(:article).permit(
|
||||||
|
:title, :content, :description, :position, :category_id, :author_id
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def list_params
|
||||||
|
params.require(:payload).permit(
|
||||||
|
:category_slug, :locale, :query
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,24 @@
|
||||||
|
class Api::V1::Accounts::AssignableAgentsController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :fetch_inboxes
|
||||||
|
|
||||||
|
def index
|
||||||
|
agent_ids = @inboxes.map do |inbox|
|
||||||
|
authorize inbox, :show?
|
||||||
|
member_ids = inbox.members.pluck(:user_id)
|
||||||
|
member_ids
|
||||||
|
end
|
||||||
|
agent_ids = agent_ids.inject(:&)
|
||||||
|
agents = Current.account.users.where(id: agent_ids)
|
||||||
|
@assignable_agents = (agents + Current.account.administrators).uniq
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_inboxes
|
||||||
|
@inboxes = Current.account.inboxes.find(permitted_params[:inbox_ids])
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(inbox_ids: [])
|
||||||
|
end
|
||||||
|
end
|
|
@ -9,6 +9,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||||
def create
|
def create
|
||||||
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
|
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
|
||||||
@automation_rule.actions = params[:actions]
|
@automation_rule.actions = params[:actions]
|
||||||
|
@automation_rule.conditions = params[:conditions]
|
||||||
|
|
||||||
render json: { error: @automation_rule.errors.messages }, status: :unprocessable_entity and return unless @automation_rule.valid?
|
render json: { error: @automation_rule.errors.messages }, status: :unprocessable_entity and return unless @automation_rule.valid?
|
||||||
|
|
||||||
|
@ -31,9 +32,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||||
|
|
||||||
def update
|
def update
|
||||||
ActiveRecord::Base.transaction do
|
ActiveRecord::Base.transaction do
|
||||||
@automation_rule.update!(automation_rules_permit)
|
automation_rule_update
|
||||||
@automation_rule.actions = params[:actions] if params[:actions]
|
|
||||||
@automation_rule.save!
|
|
||||||
process_attachments
|
process_attachments
|
||||||
|
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
|
@ -67,10 +66,17 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def automation_rule_update
|
||||||
|
@automation_rule.update!(automation_rules_permit)
|
||||||
|
@automation_rule.actions = params[:actions] if params[:actions]
|
||||||
|
@automation_rule.conditions = params[:conditions] if params[:conditions]
|
||||||
|
@automation_rule.save!
|
||||||
|
end
|
||||||
|
|
||||||
def automation_rules_permit
|
def automation_rules_permit
|
||||||
params.permit(
|
params.permit(
|
||||||
:name, :description, :event_name, :account_id, :active,
|
:name, :description, :event_name, :account_id, :active,
|
||||||
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
|
conditions: [:attribute_key, :filter_operator, :query_operator, :custom_attribute_type, { values: [] }],
|
||||||
actions: [:action_name, { action_params: [] }]
|
actions: [:action_name, { action_params: [] }]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase::BaseController
|
class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :portal
|
||||||
before_action :fetch_category, except: [:index, :create]
|
before_action :fetch_category, except: [:index, :create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@categories = @portal.categories
|
@categories = @portal.categories.search(params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
|
@ -24,9 +25,13 @@ class Api::V1::Accounts::Kbase::CategoriesController < Api::V1::Accounts::Kbase:
|
||||||
@category = @portal.categories.find(params[:id])
|
@category = @portal.categories.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def portal
|
||||||
|
@portal ||= Current.account.portals.find_by(slug: params[:portal_id])
|
||||||
|
end
|
||||||
|
|
||||||
def category_params
|
def category_params
|
||||||
params.require(:category).permit(
|
params.require(:category).permit(
|
||||||
:name, :description, :position
|
:name, :description, :position, :slug, :locale
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -5,7 +5,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
||||||
RESULTS_PER_PAGE = 25
|
RESULTS_PER_PAGE = 25
|
||||||
|
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :set_csat_survey_responses, only: [:index, :metrics]
|
before_action :set_csat_survey_responses, only: [:index, :metrics, :download]
|
||||||
before_action :set_current_page, only: [:index]
|
before_action :set_current_page, only: [:index]
|
||||||
before_action :set_current_page_surveys, only: [:index]
|
before_action :set_current_page_surveys, only: [:index]
|
||||||
before_action :set_total_sent_messages_count, only: [:metrics]
|
before_action :set_total_sent_messages_count, only: [:metrics]
|
||||||
|
@ -19,6 +19,12 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
|
||||||
@ratings_count = @csat_survey_responses.group(:rating).count
|
@ratings_count = @csat_survey_responses.group(:rating).count
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def download
|
||||||
|
response.headers['Content-Type'] = 'text/csv'
|
||||||
|
response.headers['Content-Disposition'] = 'attachment; filename=csat_report.csv'
|
||||||
|
render layout: false, template: 'api/v1/accounts/csat_survey_responses/download.csv.erb', format: 'csv'
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_total_sent_messages_count
|
def set_total_sent_messages_count
|
||||||
|
|
44
app/controllers/api/v1/accounts/dashboard_apps_controller.rb
Normal file
44
app/controllers/api/v1/accounts/dashboard_apps_controller.rb
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
class Api::V1::Accounts::DashboardAppsController < Api::V1::Accounts::BaseController
|
||||||
|
before_action :fetch_dashboard_apps, except: [:create]
|
||||||
|
before_action :fetch_dashboard_app, only: [:show, :update, :destroy]
|
||||||
|
|
||||||
|
def index; end
|
||||||
|
|
||||||
|
def show; end
|
||||||
|
|
||||||
|
def create
|
||||||
|
@dashboard_app = Current.account.dashboard_apps.create!(
|
||||||
|
permitted_payload.merge(user_id: Current.user.id)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
@dashboard_app.update!(permitted_payload)
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@dashboard_app.destroy!
|
||||||
|
head :no_content
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def fetch_dashboard_apps
|
||||||
|
@dashboard_apps = Current.account.dashboard_apps
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_dashboard_app
|
||||||
|
@dashboard_app = @dashboard_apps.find(permitted_params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_payload
|
||||||
|
params.require(:dashboard_app).permit(
|
||||||
|
:title,
|
||||||
|
content: [:url, :type]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:id)
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,6 +12,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
|
# Deprecated: This API will be removed in 2.7.0
|
||||||
def assignable_agents
|
def assignable_agents
|
||||||
@assignable_agents = (Current.account.users.where(id: @inbox.members.select(:user_id)) + Current.account.administrators).uniq
|
@assignable_agents = (Current.account.users.where(id: @inbox.members.select(:user_id)) + Current.account.administrators).uniq
|
||||||
end
|
end
|
||||||
|
@ -41,15 +42,19 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@inbox.update(permitted_params.except(:channel))
|
@inbox.update!(permitted_params.except(:channel))
|
||||||
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
update_inbox_working_hours
|
||||||
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
channel_attributes = get_channel_attributes(@inbox.channel_type)
|
||||||
|
|
||||||
# Inbox update doesn't necessarily need channel attributes
|
# Inbox update doesn't necessarily need channel attributes
|
||||||
return if permitted_params(channel_attributes)[:channel].blank?
|
return if permitted_params(channel_attributes)[:channel].blank?
|
||||||
|
|
||||||
if @inbox.inbox_type == 'Email'
|
if @inbox.inbox_type == 'Email'
|
||||||
|
begin
|
||||||
validate_email_channel(channel_attributes)
|
validate_email_channel(channel_attributes)
|
||||||
|
rescue StandardError => e
|
||||||
|
render json: { message: e }, status: :unprocessable_entity and return
|
||||||
|
end
|
||||||
@inbox.channel.reauthorized!
|
@inbox.channel.reauthorized!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -57,6 +62,10 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
update_channel_feature_flags
|
update_channel_feature_flags
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_inbox_working_hours
|
||||||
|
@inbox.update_working_hours(params.permit(working_hours: Inbox::OFFISABLE_ATTRS)[:working_hours]) if params[:working_hours]
|
||||||
|
end
|
||||||
|
|
||||||
def agent_bot
|
def agent_bot
|
||||||
@agent_bot = @inbox.agent_bot
|
@agent_bot = @inbox.agent_bot
|
||||||
end
|
end
|
||||||
|
@ -88,12 +97,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
|
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
|
||||||
end
|
end
|
||||||
|
|
||||||
def inbox_name(channel)
|
|
||||||
return channel.try(:bot_name) if channel.is_a?(Channel::Telegram)
|
|
||||||
|
|
||||||
permitted_params[:name]
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_channel
|
def create_channel
|
||||||
return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type])
|
return unless %w[web_widget api email line telegram whatsapp sms].include?(permitted_params[:channel][:type])
|
||||||
|
|
||||||
|
@ -108,10 +111,14 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
@inbox.channel.save!
|
@inbox.channel.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def inbox_attributes
|
||||||
|
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
||||||
|
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved]
|
||||||
|
end
|
||||||
|
|
||||||
def permitted_params(channel_attributes = [])
|
def permitted_params(channel_attributes = [])
|
||||||
params.permit(
|
params.permit(
|
||||||
:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
|
*inbox_attributes,
|
||||||
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved,
|
|
||||||
channel: [:type, *channel_attributes]
|
channel: [:type, *channel_attributes]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -128,18 +135,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
}[permitted_params[:channel][:type]]
|
}[permitted_params[:channel][:type]]
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_channels_method
|
|
||||||
{
|
|
||||||
'web_widget' => Current.account.web_widgets,
|
|
||||||
'api' => Current.account.api_channels,
|
|
||||||
'email' => Current.account.email_channels,
|
|
||||||
'line' => Current.account.line_channels,
|
|
||||||
'telegram' => Current.account.telegram_channels,
|
|
||||||
'whatsapp' => Current.account.whatsapp_channels,
|
|
||||||
'sms' => Current.account.sms_channels
|
|
||||||
}[permitted_params[:channel][:type]]
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_channel_attributes(channel_type)
|
def get_channel_attributes(channel_type)
|
||||||
if channel_type.constantize.const_defined?(:EDITABLE_ATTRS)
|
if channel_type.constantize.const_defined?(:EDITABLE_ATTRS)
|
||||||
channel_type.constantize::EDITABLE_ATTRS.presence
|
channel_type.constantize::EDITABLE_ATTRS.presence
|
||||||
|
@ -147,10 +142,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_limit
|
|
||||||
return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes]
|
|
||||||
|
|
||||||
render_payment_required('Account limit exceeded. Upgrade to a higher plan')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Api::V1::Accounts::InboxesController.prepend_mod_with('Api::V1::Accounts::InboxesController')
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
class Api::V1::Accounts::Kbase::BaseController < Api::V1::Accounts::BaseController
|
|
||||||
before_action :portal
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def portal
|
|
||||||
@portal ||= Current.account.kbase_portals.find_by(slug: params[:portal_id])
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,14 +1,14 @@
|
||||||
class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::BaseController
|
class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||||
before_action :fetch_portal, except: [:index, :create]
|
before_action :fetch_portal, except: [:index, :create]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@portals = Current.account.kbase_portals
|
@portals = Current.account.portals
|
||||||
end
|
end
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
@portal = Current.account.kbase_portals.create!(portal_params)
|
@portal = Current.account.portals.create!(portal_params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
|
@ -23,7 +23,7 @@ class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::BaseContr
|
||||||
private
|
private
|
||||||
|
|
||||||
def fetch_portal
|
def fetch_portal
|
||||||
@portal = Current.account.kbase_portals.find_by(slug: permitted_params[:id])
|
@portal = Current.account.portals.find_by(slug: permitted_params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
|
@ -32,7 +32,7 @@ class Api::V1::Accounts::Kbase::PortalsController < Api::V1::Accounts::BaseContr
|
||||||
|
|
||||||
def portal_params
|
def portal_params
|
||||||
params.require(:portal).permit(
|
params.require(:portal).permit(
|
||||||
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug
|
:account_id, :color, :custom_domain, :header_text, :homepage_link, :name, :page_title, :slug, :archived
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -54,6 +54,7 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||||
).perform
|
).perform
|
||||||
else
|
else
|
||||||
@contact.update!(email: email)
|
@contact.update!(email: email)
|
||||||
|
update_contact_name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -67,9 +68,14 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||||
).perform
|
).perform
|
||||||
else
|
else
|
||||||
@contact.update!(phone_number: phone_number)
|
@contact.update!(phone_number: phone_number)
|
||||||
|
update_contact_name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_contact_name
|
||||||
|
@contact.update!(name: contact_name) if contact_name.present?
|
||||||
|
end
|
||||||
|
|
||||||
def contact_email
|
def contact_email
|
||||||
permitted_params.dig(:contact, :email)&.downcase
|
permitted_params.dig(:contact, :email)&.downcase
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
def process_update_contact
|
def process_update_contact
|
||||||
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
|
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
|
||||||
update_contact_phone_number(contact_phone_number) if @contact.phone_number.blank? && contact_phone_number.present?
|
update_contact_phone_number(contact_phone_number) if @contact.phone_number.blank? && contact_phone_number.present?
|
||||||
@contact.update!(name: contact_name) if contact_name.present?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_last_seen
|
def update_last_seen
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
|
include Api::V2::Accounts::ReportsHelper
|
||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -12,27 +13,23 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def agents
|
def agents
|
||||||
response.headers['Content-Type'] = 'text/csv'
|
@report_data = generate_agents_report
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename=agents_report.csv'
|
generate_csv('agents_report', 'api/v2/accounts/reports/agents.csv.erb')
|
||||||
render layout: false, template: 'api/v2/accounts/reports/agents.csv.erb', format: 'csv'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def inboxes
|
def inboxes
|
||||||
response.headers['Content-Type'] = 'text/csv'
|
@report_data = generate_inboxes_report
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename=inboxes_report.csv'
|
generate_csv('inboxes_report', 'api/v2/accounts/reports/inboxes.csv.erb')
|
||||||
render layout: false, template: 'api/v2/accounts/reports/inboxes.csv.erb', format: 'csv'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def labels
|
def labels
|
||||||
response.headers['Content-Type'] = 'text/csv'
|
@report_data = generate_labels_report
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename=labels_report.csv'
|
generate_csv('labels_report', 'api/v2/accounts/reports/labels.csv.erb')
|
||||||
render layout: false, template: 'api/v2/accounts/reports/labels.csv.erb', format: 'csv'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def teams
|
def teams
|
||||||
response.headers['Content-Type'] = 'text/csv'
|
@report_data = generate_teams_report
|
||||||
response.headers['Content-Disposition'] = 'attachment; filename=teams_report.csv'
|
generate_csv('teams_report', 'api/v2/accounts/reports/teams.csv.erb')
|
||||||
render layout: false, template: 'api/v2/accounts/reports/teams.csv.erb', format: 'csv'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversations
|
def conversations
|
||||||
|
@ -43,6 +40,12 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def generate_csv(filename, template)
|
||||||
|
response.headers['Content-Type'] = 'text/csv'
|
||||||
|
response.headers['Content-Disposition'] = "attachment; filename=#{filename}.csv"
|
||||||
|
render layout: false, template: template, format: 'csv'
|
||||||
|
end
|
||||||
|
|
||||||
def check_authorization
|
def check_authorization
|
||||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class ApplicationController < ActionController::Base
|
class ApplicationController < ActionController::Base
|
||||||
include DeviseTokenAuth::Concerns::SetUserByToken
|
include DeviseTokenAuth::Concerns::SetUserByToken
|
||||||
include RequestExceptionHandler
|
include RequestExceptionHandler
|
||||||
include Pundit
|
include Pundit::Authorization
|
||||||
include SwitchLocale
|
include SwitchLocale
|
||||||
|
|
||||||
skip_before_action :verify_authenticity_token
|
skip_before_action :verify_authenticity_token
|
||||||
|
|
|
@ -43,7 +43,8 @@ class DashboardController < ActionController::Base
|
||||||
VAPID_PUBLIC_KEY: VapidService.public_key,
|
VAPID_PUBLIC_KEY: VapidService.public_key,
|
||||||
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
||||||
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
||||||
FACEBOOK_API_VERSION: 'v13.0'
|
FACEBOOK_API_VERSION: 'v14.0',
|
||||||
|
IS_ENTERPRISE: ChatwootApp.enterprise?
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -51,6 +51,7 @@ 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
|
||||||
|
@ -90,6 +91,10 @@ class ConversationFinder
|
||||||
@conversations
|
@conversations
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def filter_by_reply_status
|
||||||
|
@conversations = @conversations.where(first_reply_created_at: nil) if params[:reply_status] == 'unattended'
|
||||||
|
end
|
||||||
|
|
||||||
def filter_by_query
|
def filter_by_query
|
||||||
allowed_message_types = [Message.message_types[:incoming], Message.message_types[:outgoing]]
|
allowed_message_types = [Message.message_types[:incoming], Message.message_types[:outgoing]]
|
||||||
@conversations = conversations.joins(:messages).where('messages.content ILIKE :search', search: "%#{params[:q]}%")
|
@conversations = conversations.joins(:messages).where('messages.content ILIKE :search', search: "%#{params[:q]}%")
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
module Api::V1::InboxesHelper
|
module Api::V1::InboxesHelper
|
||||||
|
def inbox_name(channel)
|
||||||
|
return channel.try(:bot_name) if channel.is_a?(Channel::Telegram)
|
||||||
|
|
||||||
|
permitted_params[:name]
|
||||||
|
end
|
||||||
|
|
||||||
def validate_email_channel(attributes)
|
def validate_email_channel(attributes)
|
||||||
channel_data = permitted_params(attributes)[:channel]
|
channel_data = permitted_params(attributes)[:channel]
|
||||||
|
|
||||||
|
@ -19,8 +25,7 @@ module Api::V1::InboxesHelper
|
||||||
enable_ssl: channel_data[:imap_enable_ssl] }
|
enable_ssl: channel_data[:imap_enable_ssl] }
|
||||||
end
|
end
|
||||||
|
|
||||||
Mail.connection do # rubocop:disable:block
|
check_imap_connection(channel_data)
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_smtp(channel_data)
|
def validate_smtp(channel_data)
|
||||||
|
@ -32,6 +37,25 @@ module Api::V1::InboxesHelper
|
||||||
check_smtp_connection(channel_data, smtp)
|
check_smtp_connection(channel_data, smtp)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_imap_connection(channel_data)
|
||||||
|
Mail.connection {} # rubocop:disable:block
|
||||||
|
rescue SocketError => e
|
||||||
|
raise StandardError, I18n.t('errors.inboxes.imap.socket_error')
|
||||||
|
rescue Net::IMAP::NoResponseError => e
|
||||||
|
raise StandardError, I18n.t('errors.inboxes.imap.no_response_error')
|
||||||
|
rescue Errno::EHOSTUNREACH => e
|
||||||
|
raise StandardError, I18n.t('errors.inboxes.imap.host_unreachable_error')
|
||||||
|
rescue Net::OpenTimeout => e
|
||||||
|
raise StandardError,
|
||||||
|
I18n.t('errors.inboxes.imap.connection_timed_out_error', address: channel_data[:imap_address], port: channel_data[:imap_port])
|
||||||
|
rescue Net::IMAP::Error => e
|
||||||
|
raise StandardError, I18n.t('errors.inboxes.imap.connection_closed_error')
|
||||||
|
rescue StandardError => e
|
||||||
|
raise StandardError, e.message
|
||||||
|
ensure
|
||||||
|
Rails.logger.error e if e.present?
|
||||||
|
end
|
||||||
|
|
||||||
def check_smtp_connection(channel_data, smtp)
|
def check_smtp_connection(channel_data, smtp)
|
||||||
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password],
|
smtp.start(channel_data[:smtp_domain], channel_data[:smtp_login], channel_data[:smtp_password],
|
||||||
channel_data[:smtp_authentication]&.to_sym || :login)
|
channel_data[:smtp_authentication]&.to_sym || :login)
|
||||||
|
@ -74,4 +98,22 @@ module Api::V1::InboxesHelper
|
||||||
context.verify_mode = openssl_verify_mode
|
context.verify_mode = openssl_verify_mode
|
||||||
context
|
context
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def account_channels_method
|
||||||
|
{
|
||||||
|
'web_widget' => Current.account.web_widgets,
|
||||||
|
'api' => Current.account.api_channels,
|
||||||
|
'email' => Current.account.email_channels,
|
||||||
|
'line' => Current.account.line_channels,
|
||||||
|
'telegram' => Current.account.telegram_channels,
|
||||||
|
'whatsapp' => Current.account.whatsapp_channels,
|
||||||
|
'sms' => Current.account.sms_channels
|
||||||
|
}[permitted_params[:channel][:type]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_limit
|
||||||
|
return unless Current.account.inboxes.count >= Current.account.usage_limits[:inboxes]
|
||||||
|
|
||||||
|
render_payment_required('Account limit exceeded. Upgrade to a higher plan')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
56
app/helpers/api/v2/accounts/reports_helper.rb
Normal file
56
app/helpers/api/v2/accounts/reports_helper.rb
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
module Api::V2::Accounts::ReportsHelper
|
||||||
|
def generate_agents_report
|
||||||
|
Current.account.users.map do |agent|
|
||||||
|
agent_report = generate_report({ type: :agent, id: agent.id })
|
||||||
|
[agent.name] + generate_readable_report_metrics(agent_report)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_inboxes_report
|
||||||
|
Current.account.inboxes.map do |inbox|
|
||||||
|
inbox_report = generate_report({ type: :inbox, id: inbox.id })
|
||||||
|
[inbox.name, inbox.channel&.name] + generate_readable_report_metrics(inbox_report)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_teams_report
|
||||||
|
Current.account.teams.map do |team|
|
||||||
|
team_report = generate_report({ type: :team, id: team.id })
|
||||||
|
[team.name] + generate_readable_report_metrics(team_report)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_labels_report
|
||||||
|
Current.account.labels.map do |label|
|
||||||
|
label_report = generate_report({ type: :label, id: label.id })
|
||||||
|
[label.title] + generate_readable_report_metrics(label_report)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_report(report_params)
|
||||||
|
V2::ReportBuilder.new(
|
||||||
|
Current.account,
|
||||||
|
report_params.merge(
|
||||||
|
{
|
||||||
|
since: params[:since],
|
||||||
|
until: params[:until],
|
||||||
|
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).summary
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def generate_readable_report_metrics(report_metric)
|
||||||
|
[
|
||||||
|
report_metric[:conversations_count],
|
||||||
|
time_to_minutes(report_metric[:avg_first_response_time]),
|
||||||
|
time_to_minutes(report_metric[:avg_resolution_time])
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def time_to_minutes(time_in_seconds)
|
||||||
|
(time_in_seconds / 60).to_i
|
||||||
|
end
|
||||||
|
end
|
|
@ -2,7 +2,7 @@
|
||||||
<div v-if="!authUIFlags.isFetching" id="app" class="app-wrapper app-root">
|
<div v-if="!authUIFlags.isFetching" id="app" class="app-wrapper app-root">
|
||||||
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<router-view></router-view>
|
<router-view />
|
||||||
</transition>
|
</transition>
|
||||||
<add-account-modal
|
<add-account-modal
|
||||||
:show="showAddAccountModal"
|
:show="showAddAccountModal"
|
||||||
|
|
16
app/javascript/dashboard/api/assignableAgents.js
Normal file
16
app/javascript/dashboard/api/assignableAgents.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/* global axios */
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class AssignableAgents extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('assignable_agents', { accountScoped: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
get(inboxIds) {
|
||||||
|
return axios.get(this.url, {
|
||||||
|
params: { inbox_ids: inboxIds },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new AssignableAgents();
|
9
app/javascript/dashboard/api/bulkActions.js
Normal file
9
app/javascript/dashboard/api/bulkActions.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class BulkActionsAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('bulk_actions', { accountScoped: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new BulkActionsAPI();
|
|
@ -18,6 +18,17 @@ class CSATReportsAPI extends ApiClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
download({ from, to, user_ids } = {}) {
|
||||||
|
return axios.get(`${this.url}/download`, {
|
||||||
|
params: {
|
||||||
|
since: from,
|
||||||
|
until: to,
|
||||||
|
sort: '-created_at',
|
||||||
|
user_ids,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getMetrics({ from, to, user_ids } = {}) {
|
getMetrics({ from, to, user_ids } = {}) {
|
||||||
return axios.get(`${this.url}/metrics`, {
|
return axios.get(`${this.url}/metrics`, {
|
||||||
params: { since: from, until: to, user_ids },
|
params: { since: from, until: to, user_ids },
|
||||||
|
|
9
app/javascript/dashboard/api/dashboardApps.js
Normal file
9
app/javascript/dashboard/api/dashboardApps.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class DashboardAppsAPI extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('dashboard_apps', { accountScoped: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new DashboardAppsAPI();
|
|
@ -10,6 +10,7 @@ export const buildCreatePayload = ({
|
||||||
files,
|
files,
|
||||||
ccEmails = '',
|
ccEmails = '',
|
||||||
bccEmails = '',
|
bccEmails = '',
|
||||||
|
templateParams,
|
||||||
}) => {
|
}) => {
|
||||||
let payload;
|
let payload;
|
||||||
if (files && files.length !== 0) {
|
if (files && files.length !== 0) {
|
||||||
|
@ -32,6 +33,7 @@ export const buildCreatePayload = ({
|
||||||
content_attributes: contentAttributes,
|
content_attributes: contentAttributes,
|
||||||
cc_emails: ccEmails,
|
cc_emails: ccEmails,
|
||||||
bcc_emails: bccEmails,
|
bcc_emails: bccEmails,
|
||||||
|
template_params: templateParams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
|
@ -51,6 +53,7 @@ class MessageApi extends ApiClient {
|
||||||
files,
|
files,
|
||||||
ccEmails = '',
|
ccEmails = '',
|
||||||
bccEmails = '',
|
bccEmails = '',
|
||||||
|
templateParams,
|
||||||
}) {
|
}) {
|
||||||
return axios({
|
return axios({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
|
@ -63,6 +66,7 @@ class MessageApi extends ApiClient {
|
||||||
files,
|
files,
|
||||||
ccEmails,
|
ccEmails,
|
||||||
bccEmails,
|
bccEmails,
|
||||||
|
templateParams,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,6 @@ class Inboxes extends ApiClient {
|
||||||
super('inboxes', { accountScoped: true });
|
super('inboxes', { accountScoped: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
getAssignableAgents(inboxId) {
|
|
||||||
return axios.get(`${this.url}/${inboxId}/assignable_agents`);
|
|
||||||
}
|
|
||||||
|
|
||||||
getCampaigns(inboxId) {
|
getCampaigns(inboxId) {
|
||||||
return axios.get(`${this.url}/${inboxId}/campaigns`);
|
return axios.get(`${this.url}/${inboxId}/campaigns`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,27 +53,27 @@ class ReportsAPI extends ApiClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getAgentReports(since, until) {
|
getAgentReports({ from: since, to: until, businessHours }) {
|
||||||
return axios.get(`${this.url}/agents`, {
|
return axios.get(`${this.url}/agents`, {
|
||||||
params: { since, until },
|
params: { since, until, business_hours: businessHours },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getLabelReports(since, until) {
|
getLabelReports({ from: since, to: until, businessHours }) {
|
||||||
return axios.get(`${this.url}/labels`, {
|
return axios.get(`${this.url}/labels`, {
|
||||||
params: { since, until },
|
params: { since, until, business_hours: businessHours },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getInboxReports(since, until) {
|
getInboxReports({ from: since, to: until, businessHours }) {
|
||||||
return axios.get(`${this.url}/inboxes`, {
|
return axios.get(`${this.url}/inboxes`, {
|
||||||
params: { since, until },
|
params: { since, until, business_hours: businessHours },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTeamReports(since, until) {
|
getTeamReports({ from: since, to: until, businessHours }) {
|
||||||
return axios.get(`${this.url}/teams`, {
|
return axios.get(`${this.url}/teams`, {
|
||||||
params: { since, until },
|
params: { since, until, business_hours: businessHours },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
18
app/javascript/dashboard/api/specs/assignableAgents.spec.js
Normal file
18
app/javascript/dashboard/api/specs/assignableAgents.spec.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import assignableAgentsAPI from '../assignableAgents';
|
||||||
|
import describeWithAPIMock from './apiSpecHelper';
|
||||||
|
|
||||||
|
describe('#AssignableAgentsAPI', () => {
|
||||||
|
describeWithAPIMock('API calls', context => {
|
||||||
|
it('#getAssignableAgents', () => {
|
||||||
|
assignableAgentsAPI.get([1]);
|
||||||
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/assignable_agents',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
inbox_ids: [1],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
9
app/javascript/dashboard/api/specs/bulkAction.spec.js
Normal file
9
app/javascript/dashboard/api/specs/bulkAction.spec.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import bulkActions from '../bulkActions';
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
describe('#BulkActionsAPI', () => {
|
||||||
|
it('creates correct instance', () => {
|
||||||
|
expect(bulkActions).toBeInstanceOf(ApiClient);
|
||||||
|
expect(bulkActions).toHaveProperty('create');
|
||||||
|
});
|
||||||
|
});
|
|
@ -33,5 +33,23 @@ describe('#Reports API', () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('#download', () => {
|
||||||
|
csatReportsAPI.download({
|
||||||
|
from: 1622485800,
|
||||||
|
to: 1623695400,
|
||||||
|
user_ids: 1,
|
||||||
|
});
|
||||||
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/csat_survey_responses/download',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
since: 1622485800,
|
||||||
|
until: 1623695400,
|
||||||
|
user_ids: 1,
|
||||||
|
sort: '-created_at',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
13
app/javascript/dashboard/api/specs/dashboardApps.spec.js
Normal file
13
app/javascript/dashboard/api/specs/dashboardApps.spec.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import dashboardAppsAPI from '../dashboardApps';
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
describe('#dashboardAppsAPI', () => {
|
||||||
|
it('creates correct instance', () => {
|
||||||
|
expect(dashboardAppsAPI).toBeInstanceOf(ApiClient);
|
||||||
|
expect(dashboardAppsAPI).toHaveProperty('get');
|
||||||
|
expect(dashboardAppsAPI).toHaveProperty('show');
|
||||||
|
expect(dashboardAppsAPI).toHaveProperty('create');
|
||||||
|
expect(dashboardAppsAPI).toHaveProperty('update');
|
||||||
|
expect(dashboardAppsAPI).toHaveProperty('delete');
|
||||||
|
});
|
||||||
|
});
|
|
@ -10,17 +10,9 @@ describe('#InboxesAPI', () => {
|
||||||
expect(inboxesAPI).toHaveProperty('create');
|
expect(inboxesAPI).toHaveProperty('create');
|
||||||
expect(inboxesAPI).toHaveProperty('update');
|
expect(inboxesAPI).toHaveProperty('update');
|
||||||
expect(inboxesAPI).toHaveProperty('delete');
|
expect(inboxesAPI).toHaveProperty('delete');
|
||||||
expect(inboxesAPI).toHaveProperty('getAssignableAgents');
|
|
||||||
expect(inboxesAPI).toHaveProperty('getCampaigns');
|
expect(inboxesAPI).toHaveProperty('getCampaigns');
|
||||||
});
|
});
|
||||||
describeWithAPIMock('API calls', context => {
|
describeWithAPIMock('API calls', context => {
|
||||||
it('#getAssignableAgents', () => {
|
|
||||||
inboxesAPI.getAssignableAgents(1);
|
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
|
||||||
'/api/v1/inboxes/1/assignable_agents'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('#getCampaigns', () => {
|
it('#getCampaigns', () => {
|
||||||
inboxesAPI.getCampaigns(2);
|
inboxesAPI.getCampaigns(2);
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
|
|
|
@ -47,20 +47,25 @@ describe('#Reports API', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getAgentReports', () => {
|
it('#getAgentReports', () => {
|
||||||
reportsAPI.getAgentReports(1621103400, 1621621800);
|
reportsAPI.getAgentReports({
|
||||||
|
from: 1621103400,
|
||||||
|
to: 1621621800,
|
||||||
|
businessHours: true,
|
||||||
|
});
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v2/reports/agents',
|
'/api/v2/reports/agents',
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
since: 1621103400,
|
since: 1621103400,
|
||||||
until: 1621621800,
|
until: 1621621800,
|
||||||
|
business_hours: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getLabelReports', () => {
|
it('#getLabelReports', () => {
|
||||||
reportsAPI.getLabelReports(1621103400, 1621621800);
|
reportsAPI.getLabelReports({ from: 1621103400, to: 1621621800 });
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v2/reports/labels',
|
'/api/v2/reports/labels',
|
||||||
{
|
{
|
||||||
|
@ -73,7 +78,7 @@ describe('#Reports API', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getInboxReports', () => {
|
it('#getInboxReports', () => {
|
||||||
reportsAPI.getInboxReports(1621103400, 1621621800);
|
reportsAPI.getInboxReports({ from: 1621103400, to: 1621621800 });
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v2/reports/inboxes',
|
'/api/v2/reports/inboxes',
|
||||||
{
|
{
|
||||||
|
@ -86,7 +91,7 @@ describe('#Reports API', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('#getTeamReports', () => {
|
it('#getTeamReports', () => {
|
||||||
reportsAPI.getTeamReports(1621103400, 1621621800);
|
reportsAPI.getTeamReports({ from: 1621103400, to: 1621621800 });
|
||||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||||
'/api/v2/reports/teams',
|
'/api/v2/reports/teams',
|
||||||
{
|
{
|
||||||
|
|
|
@ -60,3 +60,9 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-between {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="conversations-list-wrap">
|
<div class="conversations-list-wrap">
|
||||||
<slot></slot>
|
<slot />
|
||||||
<div
|
<div
|
||||||
class="chat-list__top"
|
class="chat-list__top"
|
||||||
:class="{ filter__applied: hasAppliedFiltersOrActiveFolders }"
|
:class="{ filter__applied: hasAppliedFiltersOrActiveFolders }"
|
||||||
|
@ -53,8 +53,7 @@
|
||||||
size="small"
|
size="small"
|
||||||
class="btn-filter"
|
class="btn-filter"
|
||||||
@click="onToggleAdvanceFiltersModal"
|
@click="onToggleAdvanceFiltersModal"
|
||||||
>
|
/>
|
||||||
</woot-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -85,7 +84,19 @@
|
||||||
<p v-if="!chatListLoading && !conversationList.length" class="content-box">
|
<p v-if="!chatListLoading && !conversationList.length" class="content-box">
|
||||||
{{ $t('CHAT_LIST.LIST.404') }}
|
{{ $t('CHAT_LIST.LIST.404') }}
|
||||||
</p>
|
</p>
|
||||||
|
<conversation-bulk-actions
|
||||||
|
v-if="selectedConversations.length"
|
||||||
|
:conversations="selectedConversations"
|
||||||
|
:all-conversations-selected="allConversationsSelected"
|
||||||
|
:selected-inboxes="uniqueInboxes"
|
||||||
|
:show-open-action="allSelectedConversationsStatus('open')"
|
||||||
|
:show-resolved-action="allSelectedConversationsStatus('resolved')"
|
||||||
|
:show-snoozed-action="allSelectedConversationsStatus('snoozed')"
|
||||||
|
@select-all-conversations="selectAllConversations"
|
||||||
|
@assign-agent="onAssignAgent"
|
||||||
|
@update-conversations="onUpdateConversations"
|
||||||
|
@assign-labels="onAssignLabels"
|
||||||
|
/>
|
||||||
<div ref="activeConversation" class="conversations-list">
|
<div ref="activeConversation" class="conversations-list">
|
||||||
<conversation-card
|
<conversation-card
|
||||||
v-for="chat in conversationList"
|
v-for="chat in conversationList"
|
||||||
|
@ -96,10 +107,13 @@
|
||||||
:chat="chat"
|
:chat="chat"
|
||||||
:conversation-type="conversationType"
|
:conversation-type="conversationType"
|
||||||
:show-assignee="showAssigneeInConversationCard"
|
:show-assignee="showAssigneeInConversationCard"
|
||||||
|
:selected="isConversationSelected(chat.id)"
|
||||||
|
@select-conversation="selectConversation"
|
||||||
|
@de-select-conversation="deSelectConversation"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div v-if="chatListLoading" class="text-center">
|
<div v-if="chatListLoading" class="text-center">
|
||||||
<span class="spinner"></span>
|
<span class="spinner" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<woot-button
|
<woot-button
|
||||||
|
@ -112,11 +126,7 @@
|
||||||
</woot-button>
|
</woot-button>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="
|
v-if="showEndOfListMessage"
|
||||||
conversationList.length &&
|
|
||||||
hasCurrentPageEndReached &&
|
|
||||||
!chatListLoading
|
|
||||||
"
|
|
||||||
class="text-center text-muted end-of-list-text"
|
class="text-center text-muted end-of-list-text"
|
||||||
>
|
>
|
||||||
{{ $t('CHAT_LIST.EOF') }}
|
{{ $t('CHAT_LIST.EOF') }}
|
||||||
|
@ -152,6 +162,8 @@ import advancedFilterTypes from './widgets/conversation/advancedFilterItems';
|
||||||
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
|
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
|
||||||
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
|
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
|
||||||
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
||||||
|
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
||||||
|
import alertMixin from 'shared/mixins/alertMixin';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
hasPressedAltAndJKey,
|
hasPressedAltAndJKey,
|
||||||
|
@ -166,8 +178,9 @@ export default {
|
||||||
ChatFilter,
|
ChatFilter,
|
||||||
ConversationAdvancedFilter,
|
ConversationAdvancedFilter,
|
||||||
DeleteCustomViews,
|
DeleteCustomViews,
|
||||||
|
ConversationBulkActions,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin, conversationMixin, eventListenerMixins],
|
mixins: [timeMixin, conversationMixin, eventListenerMixins, alertMixin],
|
||||||
props: {
|
props: {
|
||||||
conversationInbox: {
|
conversationInbox: {
|
||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
|
@ -202,6 +215,8 @@ export default {
|
||||||
foldersQuery: {},
|
foldersQuery: {},
|
||||||
showAddFoldersModal: false,
|
showAddFoldersModal: false,
|
||||||
showDeleteFoldersModal: false,
|
showDeleteFoldersModal: false,
|
||||||
|
selectedConversations: [],
|
||||||
|
selectedInboxes: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -217,6 +232,7 @@ export default {
|
||||||
conversationStats: 'conversationStats/getStats',
|
conversationStats: 'conversationStats/getStats',
|
||||||
appliedFilters: 'getAppliedConversationFilters',
|
appliedFilters: 'getAppliedConversationFilters',
|
||||||
folders: 'customViews/getCustomViews',
|
folders: 'customViews/getCustomViews',
|
||||||
|
inboxes: 'inboxes/getInboxes',
|
||||||
}),
|
}),
|
||||||
hasAppliedFilters() {
|
hasAppliedFilters() {
|
||||||
return this.appliedFilters.length !== 0;
|
return this.appliedFilters.length !== 0;
|
||||||
|
@ -234,12 +250,24 @@ export default {
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
showEndOfListMessage() {
|
||||||
|
return (
|
||||||
|
this.conversationList.length &&
|
||||||
|
this.hasCurrentPageEndReached &&
|
||||||
|
!this.chatListLoading
|
||||||
|
);
|
||||||
|
},
|
||||||
assigneeTabItems() {
|
assigneeTabItems() {
|
||||||
return this.$t('CHAT_LIST.ASSIGNEE_TYPE_TABS').map(item => {
|
const ASSIGNEE_TYPE_TAB_KEYS = {
|
||||||
const count = this.conversationStats[item.COUNT_KEY] || 0;
|
me: 'mineCount',
|
||||||
|
unassigned: 'unAssignedCount',
|
||||||
|
all: 'allCount',
|
||||||
|
};
|
||||||
|
return Object.keys(ASSIGNEE_TYPE_TAB_KEYS).map(key => {
|
||||||
|
const count = this.conversationStats[ASSIGNEE_TYPE_TAB_KEYS[key]] || 0;
|
||||||
return {
|
return {
|
||||||
key: item.KEY,
|
key,
|
||||||
name: item.NAME,
|
name: this.$t(`CHAT_LIST.ASSIGNEE_TYPE_TABS.${key}`),
|
||||||
count,
|
count,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -338,6 +366,17 @@ export default {
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
allConversationsSelected() {
|
||||||
|
return (
|
||||||
|
this.conversationList.length === this.selectedConversations.length &&
|
||||||
|
this.conversationList.every(el =>
|
||||||
|
this.selectedConversations.includes(el.id)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
uniqueInboxes() {
|
||||||
|
return [...new Set(this.selectedInboxes)];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
activeTeam() {
|
activeTeam() {
|
||||||
|
@ -371,6 +410,7 @@ export default {
|
||||||
if (this.$route.name !== 'home') {
|
if (this.$route.name !== 'home') {
|
||||||
this.$router.push({ name: 'home' });
|
this.$router.push({ name: 'home' });
|
||||||
}
|
}
|
||||||
|
this.resetBulkActions();
|
||||||
this.foldersQuery = filterQueryGenerator(payload);
|
this.foldersQuery = filterQueryGenerator(payload);
|
||||||
this.$store.dispatch('conversationPage/reset');
|
this.$store.dispatch('conversationPage/reset');
|
||||||
this.$store.dispatch('emptyAllConversations');
|
this.$store.dispatch('emptyAllConversations');
|
||||||
|
@ -436,6 +476,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetAndFetchData() {
|
resetAndFetchData() {
|
||||||
|
this.resetBulkActions();
|
||||||
this.$store.dispatch('conversationPage/reset');
|
this.$store.dispatch('conversationPage/reset');
|
||||||
this.$store.dispatch('emptyAllConversations');
|
this.$store.dispatch('emptyAllConversations');
|
||||||
this.$store.dispatch('clearConversationFilters');
|
this.$store.dispatch('clearConversationFilters');
|
||||||
|
@ -486,6 +527,7 @@ export default {
|
||||||
},
|
},
|
||||||
updateAssigneeTab(selectedTab) {
|
updateAssigneeTab(selectedTab) {
|
||||||
if (this.activeAssigneeTab !== selectedTab) {
|
if (this.activeAssigneeTab !== selectedTab) {
|
||||||
|
this.resetBulkActions();
|
||||||
bus.$emit('clearSearchInput');
|
bus.$emit('clearSearchInput');
|
||||||
this.activeAssigneeTab = selectedTab;
|
this.activeAssigneeTab = selectedTab;
|
||||||
if (!this.currentPage) {
|
if (!this.currentPage) {
|
||||||
|
@ -493,6 +535,10 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
resetBulkActions() {
|
||||||
|
this.selectedConversations = [];
|
||||||
|
this.selectedInboxes = [];
|
||||||
|
},
|
||||||
updateStatusType(index) {
|
updateStatusType(index) {
|
||||||
if (this.activeStatus !== index) {
|
if (this.activeStatus !== index) {
|
||||||
this.activeStatus = index;
|
this.activeStatus = index;
|
||||||
|
@ -515,6 +561,80 @@ export default {
|
||||||
this.fetchConversations();
|
this.fetchConversations();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isConversationSelected(id) {
|
||||||
|
return this.selectedConversations.includes(id);
|
||||||
|
},
|
||||||
|
selectConversation(conversationId, inboxId) {
|
||||||
|
this.selectedConversations.push(conversationId);
|
||||||
|
this.selectedInboxes.push(inboxId);
|
||||||
|
},
|
||||||
|
deSelectConversation(conversationId, inboxId) {
|
||||||
|
this.selectedConversations = this.selectedConversations.filter(
|
||||||
|
item => item !== conversationId
|
||||||
|
);
|
||||||
|
this.selectedInboxes = this.selectedInboxes.filter(
|
||||||
|
item => item !== inboxId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
selectAllConversations(check) {
|
||||||
|
if (check) {
|
||||||
|
this.selectedConversations = this.conversationList.map(item => item.id);
|
||||||
|
this.selectedInboxes = this.conversationList.map(item => item.inbox_id);
|
||||||
|
} else {
|
||||||
|
this.resetBulkActions();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onAssignAgent(agent) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('bulkActions/process', {
|
||||||
|
type: 'Conversation',
|
||||||
|
ids: this.selectedConversations,
|
||||||
|
fields: {
|
||||||
|
assignee_id: agent.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.selectedConversations = [];
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.ASSIGN_SUCCESFUL'));
|
||||||
|
} catch (err) {
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onAssignLabels(labels) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('bulkActions/process', {
|
||||||
|
type: 'Conversation',
|
||||||
|
ids: this.selectedConversations,
|
||||||
|
labels: {
|
||||||
|
add: labels,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.selectedConversations = [];
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_SUCCESFUL'));
|
||||||
|
} catch (err) {
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onUpdateConversations(status) {
|
||||||
|
try {
|
||||||
|
await this.$store.dispatch('bulkActions/process', {
|
||||||
|
type: 'Conversation',
|
||||||
|
ids: this.selectedConversations,
|
||||||
|
fields: {
|
||||||
|
status,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.selectedConversations = [];
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_SUCCESFUL'));
|
||||||
|
} catch (err) {
|
||||||
|
this.showAlert(this.$t('BULK_ACTION.UPDATE.UPDATE_FAILED'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
allSelectedConversationsStatus(status) {
|
||||||
|
if (!this.selectedConversations.length) return false;
|
||||||
|
return this.selectedConversations.every(item => {
|
||||||
|
return this.$store.getters.getConversationById(item).status === status;
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -530,7 +650,7 @@ export default {
|
||||||
.conversations-list-wrap {
|
.conversations-list-wrap {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 34rem;
|
width: 34rem;
|
||||||
|
overflow: hidden;
|
||||||
@include breakpoint(large up) {
|
@include breakpoint(large up) {
|
||||||
width: 36rem;
|
width: 36rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,4 +98,7 @@ export default {
|
||||||
width: 48rem;
|
width: 48rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.modal-big {
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<p v-if="headerContent" class="small-12 column">
|
<p v-if="headerContent" class="small-12 column">
|
||||||
{{ headerContent }}
|
{{ headerContent }}
|
||||||
</p>
|
</p>
|
||||||
<slot></slot>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,7 @@
|
||||||
color-scheme="warning"
|
color-scheme="warning"
|
||||||
icon="dismiss-circle"
|
icon="dismiss-circle"
|
||||||
@click="closeNotification"
|
@click="closeNotification"
|
||||||
>
|
/>
|
||||||
</woot-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="medium-6 small-12">
|
<div class="medium-6 small-12">
|
||||||
<slot></slot>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
:icon="icon"
|
:icon="icon"
|
||||||
/>
|
/>
|
||||||
<spinner v-if="isLoading" />
|
<spinner v-if="isLoading" />
|
||||||
<slot></slot>
|
<slot />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -95,6 +95,7 @@ export default {
|
||||||
),
|
),
|
||||||
type: inbox.channel_type,
|
type: inbox.channel_type,
|
||||||
phoneNumber: inbox.phone_number,
|
phoneNumber: inbox.phone_number,
|
||||||
|
reauthorizationRequired: inbox.reauthorization_required,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) =>
|
.sort((a, b) =>
|
||||||
a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1
|
a.label.toLowerCase() > b.label.toLowerCase() ? 1 : -1
|
||||||
|
|
|
@ -30,6 +30,14 @@
|
||||||
<span v-if="count" class="badge" :class="{ secondary: !isActive }">
|
<span v-if="count" class="badge" :class="{ secondary: !isActive }">
|
||||||
{{ count }}
|
{{ count }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="warningIcon" class="badge--icon">
|
||||||
|
<fluent-icon
|
||||||
|
v-tooltip.top-end="$t('SIDEBAR.FACEBOOK_REAUTHORIZE')"
|
||||||
|
class="inbox-icon"
|
||||||
|
:icon="warningIcon"
|
||||||
|
size="12"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
@ -57,6 +65,10 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
warningIcon: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
count: {
|
count: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
:label-color="child.color"
|
:label-color="child.color"
|
||||||
:should-truncate="child.truncateLabel"
|
:should-truncate="child.truncateLabel"
|
||||||
:icon="computedInboxClass(child)"
|
:icon="computedInboxClass(child)"
|
||||||
|
:warning-icon="computedInboxErrorClass(child)"
|
||||||
/>
|
/>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="showItem(menuItem)"
|
v-if="showItem(menuItem)"
|
||||||
|
@ -63,7 +64,10 @@
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
import adminMixin from '../../../mixins/isAdmin';
|
import adminMixin from '../../../mixins/isAdmin';
|
||||||
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
import {
|
||||||
|
getInboxClassByType,
|
||||||
|
getInboxWarningIconClass,
|
||||||
|
} from 'dashboard/helper/inbox';
|
||||||
|
|
||||||
import SecondaryChildNavItem from './SecondaryChildNavItem';
|
import SecondaryChildNavItem from './SecondaryChildNavItem';
|
||||||
|
|
||||||
|
@ -136,6 +140,15 @@ export default {
|
||||||
const classByType = getInboxClassByType(type, phoneNumber);
|
const classByType = getInboxClassByType(type, phoneNumber);
|
||||||
return classByType;
|
return classByType;
|
||||||
},
|
},
|
||||||
|
computedInboxErrorClass(child) {
|
||||||
|
const { type, reauthorizationRequired } = child;
|
||||||
|
if (!type) return '';
|
||||||
|
const warningClass = getInboxWarningIconClass(
|
||||||
|
type,
|
||||||
|
reauthorizationRequired
|
||||||
|
);
|
||||||
|
return warningClass;
|
||||||
|
},
|
||||||
newLinkClick(e, navigate) {
|
newLinkClick(e, navigate) {
|
||||||
if (this.menuItem.newLinkRouteName) {
|
if (this.menuItem.newLinkRouteName) {
|
||||||
navigate(e);
|
navigate(e);
|
||||||
|
|
|
@ -30,8 +30,7 @@
|
||||||
icon="dismiss-circle"
|
icon="dismiss-circle"
|
||||||
class-names="banner-action__button"
|
class-names="banner-action__button"
|
||||||
@click="onClickClose"
|
@click="onClickClose"
|
||||||
>
|
/>
|
||||||
</woot-button>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
:aria-checked="value.toString()"
|
:aria-checked="value.toString()"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true" :class="{ active: value }"></span>
|
<span aria-hidden="true" :class="{ active: value }" />
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
:icon="icon"
|
:icon="icon"
|
||||||
:icon-size="iconSize"
|
:icon-size="iconSize"
|
||||||
/>
|
/>
|
||||||
<span v-if="$slots.default" class="button__content"><slot></slot></span>
|
<span v-if="$slots.default" class="button__content"><slot /></span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
|
|
@ -77,7 +77,7 @@
|
||||||
rows="4"
|
rows="4"
|
||||||
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
||||||
class="action-message"
|
class="action-message"
|
||||||
></textarea>
|
/>
|
||||||
<p
|
<p
|
||||||
v-if="v.action_params.$dirty && v.action_params.$error"
|
v-if="v.action_params.$dirty && v.action_params.$error"
|
||||||
class="filter-error"
|
class="filter-error"
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
rows="4"
|
rows="4"
|
||||||
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
:placeholder="$t('AUTOMATION.ACTION.TEAM_MESSAGE_INPUT_PLACEHOLDER')"
|
||||||
@input="updateValue"
|
@input="updateValue"
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<div class="dashboard-app--container">
|
||||||
|
<div
|
||||||
|
v-for="(configItem, index) in config"
|
||||||
|
:key="index"
|
||||||
|
class="dashboard-app--list"
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
v-if="configItem.type === 'frame' && configItem.url"
|
||||||
|
:id="`dashboard-app--frame-${index}`"
|
||||||
|
:src="configItem.url"
|
||||||
|
@load="() => onIframeLoad(index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
config: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
currentChat: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dashboardAppContext() {
|
||||||
|
return {
|
||||||
|
conversation: this.currentChat,
|
||||||
|
contact: this.$store.getters['contacts/getContact'](this.contactId),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contactId() {
|
||||||
|
return this.currentChat?.meta?.sender?.id;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onIframeLoad(index) {
|
||||||
|
const frameElement = document.getElementById(
|
||||||
|
`dashboard-app--frame-${index}`
|
||||||
|
);
|
||||||
|
const eventData = { event: 'appContext', data: this.dashboardAppContext };
|
||||||
|
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-app--container,
|
||||||
|
.dashboard-app--list,
|
||||||
|
.dashboard-app--list iframe {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-app--list iframe {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -215,6 +215,32 @@ export default {
|
||||||
this.$emit('input', { ...payload, query_operator: value });
|
this.$emit('input', { ...payload, query_operator: value });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
custom_attribute_type: {
|
||||||
|
get() {
|
||||||
|
if (!this.customAttributeType) return '';
|
||||||
|
return this.customAttributeType;
|
||||||
|
},
|
||||||
|
set() {
|
||||||
|
const payload = this.value || {};
|
||||||
|
this.$emit('input', {
|
||||||
|
...payload,
|
||||||
|
custom_attribute_type: this.customAttributeType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
customAttributeType: {
|
||||||
|
handler(value) {
|
||||||
|
if (
|
||||||
|
value === 'conversation_attribute' ||
|
||||||
|
value === 'contact_attribute'
|
||||||
|
) {
|
||||||
|
this.value.custom_attribute_type = this.customAttributeType;
|
||||||
|
} else this.value.custom_attribute_type = '';
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
removeFilter() {
|
removeFilter() {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<p v-if="headerContent" class="small-12 column">
|
<p v-if="headerContent" class="small-12 column">
|
||||||
{{ headerContent }}
|
{{ headerContent }}
|
||||||
</p>
|
</p>
|
||||||
<slot></slot>
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="audio-wave-wrapper">
|
<div class="audio-wave-wrapper">
|
||||||
<audio id="audio-wave" class="video-js vjs-fill vjs-default-skin"></audio>
|
<audio id="audio-wave" class="video-js vjs-fill vjs-default-skin" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
:search-key="cannedSearchTerm"
|
:search-key="cannedSearchTerm"
|
||||||
@click="insertCannedResponse"
|
@click="insertCannedResponse"
|
||||||
/>
|
/>
|
||||||
<div ref="editor"></div>
|
<div ref="editor" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,16 @@
|
||||||
:title="signatureToggleTooltip"
|
:title="signatureToggleTooltip"
|
||||||
@click="toggleMessageSignature"
|
@click="toggleMessageSignature"
|
||||||
/>
|
/>
|
||||||
|
<woot-button
|
||||||
|
v-if="hasWhatsappTemplates"
|
||||||
|
v-tooltip.top-end="'Whatsapp Templates'"
|
||||||
|
icon="whatsapp"
|
||||||
|
color-scheme="secondary"
|
||||||
|
variant="smooth"
|
||||||
|
size="small"
|
||||||
|
:title="'Whatsapp Templates'"
|
||||||
|
@click="$emit('selectWhatsappTemplate')"
|
||||||
|
/>
|
||||||
<transition name="modal-fade">
|
<transition name="modal-fade">
|
||||||
<div
|
<div
|
||||||
v-show="$refs.upload && $refs.upload.dropActive"
|
v-show="$refs.upload && $refs.upload.dropActive"
|
||||||
|
@ -218,6 +228,10 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
hasWhatsappTemplates: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isNote() {
|
isNote() {
|
||||||
|
|
|
@ -6,7 +6,20 @@
|
||||||
:is-contact-panel-open="isContactPanelOpen"
|
:is-contact-panel-open="isContactPanelOpen"
|
||||||
@contact-panel-toggle="onToggleContactPanel"
|
@contact-panel-toggle="onToggleContactPanel"
|
||||||
/>
|
/>
|
||||||
<div class="messages-and-sidebar">
|
<woot-tabs
|
||||||
|
v-if="dashboardApps.length && currentChat.id"
|
||||||
|
:index="activeIndex"
|
||||||
|
class="dashboard-app--tabs"
|
||||||
|
@change="onDashboardAppTabChange"
|
||||||
|
>
|
||||||
|
<woot-tabs-item
|
||||||
|
v-for="tab in dashboardAppTabs"
|
||||||
|
:key="tab.key"
|
||||||
|
:name="tab.name"
|
||||||
|
:show-badge="false"
|
||||||
|
/>
|
||||||
|
</woot-tabs>
|
||||||
|
<div v-if="!activeIndex" class="messages-and-sidebar">
|
||||||
<messages-view
|
<messages-view
|
||||||
v-if="currentChat.id"
|
v-if="currentChat.id"
|
||||||
:inbox-id="inboxId"
|
:inbox-id="inboxId"
|
||||||
|
@ -14,7 +27,6 @@
|
||||||
@contact-panel-toggle="onToggleContactPanel"
|
@contact-panel-toggle="onToggleContactPanel"
|
||||||
/>
|
/>
|
||||||
<empty-state v-else />
|
<empty-state v-else />
|
||||||
|
|
||||||
<div v-show="showContactPanel" class="conversation-sidebar-wrap">
|
<div v-show="showContactPanel" class="conversation-sidebar-wrap">
|
||||||
<contact-panel
|
<contact-panel
|
||||||
v-if="showContactPanel"
|
v-if="showContactPanel"
|
||||||
|
@ -24,21 +36,29 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<dashboard-app-frame
|
||||||
|
v-else
|
||||||
|
:key="currentChat.id"
|
||||||
|
:config="dashboardApps[activeIndex - 1].content"
|
||||||
|
:current-chat="currentChat"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel';
|
import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel';
|
||||||
import ConversationHeader from './ConversationHeader';
|
import ConversationHeader from './ConversationHeader';
|
||||||
|
import DashboardAppFrame from '../DashboardApp/Frame.vue';
|
||||||
import EmptyState from './EmptyState';
|
import EmptyState from './EmptyState';
|
||||||
import MessagesView from './MessagesView';
|
import MessagesView from './MessagesView';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
EmptyState,
|
|
||||||
MessagesView,
|
|
||||||
ContactPanel,
|
ContactPanel,
|
||||||
ConversationHeader,
|
ConversationHeader,
|
||||||
|
DashboardAppFrame,
|
||||||
|
EmptyState,
|
||||||
|
MessagesView,
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
@ -52,8 +72,26 @@ export default {
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
return { activeIndex: 0 };
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({ currentChat: 'getSelectedChat' }),
|
...mapGetters({
|
||||||
|
currentChat: 'getSelectedChat',
|
||||||
|
dashboardApps: 'dashboardApps/getRecords',
|
||||||
|
}),
|
||||||
|
dashboardAppTabs() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: 'messages',
|
||||||
|
name: this.$t('CONVERSATION.DASHBOARD_APP_TAB_MESSAGES'),
|
||||||
|
},
|
||||||
|
...this.dashboardApps.map(dashboardApp => ({
|
||||||
|
key: `dashboard-${dashboardApp.id}`,
|
||||||
|
name: dashboardApp.title,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
},
|
||||||
showContactPanel() {
|
showContactPanel() {
|
||||||
return this.isContactPanelOpen && this.currentChat.id;
|
return this.isContactPanelOpen && this.currentChat.id;
|
||||||
},
|
},
|
||||||
|
@ -61,7 +99,7 @@ export default {
|
||||||
watch: {
|
watch: {
|
||||||
'currentChat.inbox_id'(inboxId) {
|
'currentChat.inbox_id'(inboxId) {
|
||||||
if (inboxId) {
|
if (inboxId) {
|
||||||
this.$store.dispatch('inboxAssignableAgents/fetch', { inboxId });
|
this.$store.dispatch('inboxAssignableAgents/fetch', [inboxId]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'currentChat.id'() {
|
'currentChat.id'() {
|
||||||
|
@ -70,6 +108,7 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchLabels();
|
this.fetchLabels();
|
||||||
|
this.$store.dispatch('dashboardApps/get');
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchLabels() {
|
fetchLabels() {
|
||||||
|
@ -81,6 +120,9 @@ export default {
|
||||||
onToggleContactPanel() {
|
onToggleContactPanel() {
|
||||||
this.$emit('contact-panel-toggle');
|
this.$emit('contact-panel-toggle');
|
||||||
},
|
},
|
||||||
|
onDashboardAppTabChange(index) {
|
||||||
|
this.activeIndex = index;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -96,6 +138,11 @@ export default {
|
||||||
background: var(--color-background-light);
|
background: var(--color-background-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dashboard-app--tabs {
|
||||||
|
background: var(--white);
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
.messages-and-sidebar {
|
.messages-and-sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: var(--color-background-light);
|
background: var(--color-background-light);
|
||||||
|
|
|
@ -5,11 +5,23 @@
|
||||||
active: isActiveChat,
|
active: isActiveChat,
|
||||||
'unread-chat': hasUnread,
|
'unread-chat': hasUnread,
|
||||||
'has-inbox-name': showInboxName,
|
'has-inbox-name': showInboxName,
|
||||||
|
'conversation-selected': selected,
|
||||||
}"
|
}"
|
||||||
|
@mouseenter="onCardHover"
|
||||||
|
@mouseleave="onCardLeave"
|
||||||
@click="cardClick(chat)"
|
@click="cardClick(chat)"
|
||||||
>
|
>
|
||||||
|
<label v-if="hovered || selected" class="checkbox-wrapper" @click.stop>
|
||||||
|
<input
|
||||||
|
:value="selected"
|
||||||
|
:checked="selected"
|
||||||
|
class="checkbox"
|
||||||
|
type="checkbox"
|
||||||
|
@change="onSelectConversation($event.target.checked)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<thumbnail
|
<thumbnail
|
||||||
v-if="!hideThumbnail"
|
v-if="bulkActionCheck"
|
||||||
:src="currentContact.thumbnail"
|
:src="currentContact.thumbnail"
|
||||||
:badge="inboxBadge"
|
:badge="inboxBadge"
|
||||||
class="columns"
|
class="columns"
|
||||||
|
@ -142,8 +154,16 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
|
selected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
hovered: false,
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentChat: 'getSelectedChat',
|
currentChat: 'getSelectedChat',
|
||||||
|
@ -152,7 +172,9 @@ export default {
|
||||||
currentUser: 'getCurrentUser',
|
currentUser: 'getCurrentUser',
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
}),
|
}),
|
||||||
|
bulkActionCheck() {
|
||||||
|
return !this.hideThumbnail && !this.hovered && !this.selected;
|
||||||
|
},
|
||||||
chatMetadata() {
|
chatMetadata() {
|
||||||
return this.chat.meta || {};
|
return this.chat.meta || {};
|
||||||
},
|
},
|
||||||
|
@ -260,6 +282,16 @@ export default {
|
||||||
}
|
}
|
||||||
router.push({ path: frontendURL(path) });
|
router.push({ path: frontendURL(path) });
|
||||||
},
|
},
|
||||||
|
onCardHover() {
|
||||||
|
this.hovered = !this.hideThumbnail;
|
||||||
|
},
|
||||||
|
onCardLeave() {
|
||||||
|
this.hovered = false;
|
||||||
|
},
|
||||||
|
onSelectConversation(checked) {
|
||||||
|
const action = checked ? 'select-conversation' : 'de-select-conversation';
|
||||||
|
this.$emit(action, this.chat.id, this.inbox.id);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -272,6 +304,10 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.conversation-selected {
|
||||||
|
background: var(--color-background-light);
|
||||||
|
}
|
||||||
|
|
||||||
.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-normal);
|
||||||
|
@ -320,4 +356,22 @@ export default {
|
||||||
margin-top: var(--space-minus-micro);
|
margin-top: var(--space-minus-micro);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
.checkbox-wrapper {
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 100%;
|
||||||
|
margin-top: var(--space-normal);
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--w-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
margin: var(--space-zero);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,19 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="view-box fill-height">
|
<div class="view-box fill-height">
|
||||||
<banner
|
<banner
|
||||||
v-if="!currentChat.can_reply && !isAWhatsappChannel"
|
v-if="!currentChat.can_reply"
|
||||||
color-scheme="alert"
|
color-scheme="alert"
|
||||||
:banner-message="$t('CONVERSATION.CANNOT_REPLY')"
|
:banner-message="replyWindowBannerMessage"
|
||||||
:href-link="facebookReplyPolicy"
|
:href-link="replyWindowLink"
|
||||||
:href-link-text="$t('CONVERSATION.24_HOURS_WINDOW')"
|
:href-link-text="replyWindowLinkText"
|
||||||
/>
|
|
||||||
|
|
||||||
<banner
|
|
||||||
v-if="!currentChat.can_reply && isAWhatsappChannel"
|
|
||||||
color-scheme="alert"
|
|
||||||
:banner-message="$t('CONVERSATION.TWILIO_WHATSAPP_CAN_REPLY')"
|
|
||||||
:href-link="twilioWhatsAppReplyPolicy"
|
|
||||||
:href-link-text="$t('CONVERSATION.TWILIO_WHATSAPP_24_HOURS_WINDOW')"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<banner
|
<banner
|
||||||
|
@ -32,8 +24,7 @@
|
||||||
class="sidebar-toggle--button"
|
class="sidebar-toggle--button"
|
||||||
:icon="isRightOrLeftIcon"
|
:icon="isRightOrLeftIcon"
|
||||||
@click="onToggleContactPanel"
|
@click="onToggleContactPanel"
|
||||||
>
|
/>
|
||||||
</woot-button>
|
|
||||||
</div>
|
</div>
|
||||||
<ul class="conversation-panel">
|
<ul class="conversation-panel">
|
||||||
<transition name="slide-up">
|
<transition name="slide-up">
|
||||||
|
@ -160,7 +151,6 @@ export default {
|
||||||
hasSelectedTweetId() {
|
hasSelectedTweetId() {
|
||||||
return !!this.selectedTweetId;
|
return !!this.selectedTweetId;
|
||||||
},
|
},
|
||||||
|
|
||||||
tweetBannerText() {
|
tweetBannerText() {
|
||||||
return !this.selectedTweetId
|
return !this.selectedTweetId
|
||||||
? this.$t('CONVERSATION.SELECT_A_TWEET_TO_REPLY')
|
? this.$t('CONVERSATION.SELECT_A_TWEET_TO_REPLY')
|
||||||
|
@ -238,12 +228,6 @@ export default {
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
facebookReplyPolicy() {
|
|
||||||
return REPLY_POLICY.FACEBOOK;
|
|
||||||
},
|
|
||||||
twilioWhatsAppReplyPolicy() {
|
|
||||||
return REPLY_POLICY.TWILIO_WHATSAPP;
|
|
||||||
},
|
|
||||||
isRightOrLeftIcon() {
|
isRightOrLeftIcon() {
|
||||||
if (this.isContactPanelOpen) {
|
if (this.isContactPanelOpen) {
|
||||||
return 'arrow-chevron-right';
|
return 'arrow-chevron-right';
|
||||||
|
@ -255,6 +239,41 @@ export default {
|
||||||
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
|
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
|
||||||
return contactLastSeenAt;
|
return contactLastSeenAt;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
replyWindowBannerMessage() {
|
||||||
|
if (this.isAWhatsappChannel) {
|
||||||
|
return this.$t('CONVERSATION.TWILIO_WHATSAPP_CAN_REPLY');
|
||||||
|
}
|
||||||
|
if (this.isAPIInbox) {
|
||||||
|
const { additional_attributes: additionalAttributes = {} } = this.inbox;
|
||||||
|
if (additionalAttributes) {
|
||||||
|
const {
|
||||||
|
agent_reply_time_window_message: agentReplyTimeWindowMessage,
|
||||||
|
} = additionalAttributes;
|
||||||
|
return agentReplyTimeWindowMessage;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return this.$t('CONVERSATION.CANNOT_REPLY');
|
||||||
|
},
|
||||||
|
replyWindowLink() {
|
||||||
|
if (this.isAWhatsappChannel) {
|
||||||
|
return REPLY_POLICY.FACEBOOK;
|
||||||
|
}
|
||||||
|
if (!this.isAPIInbox) {
|
||||||
|
return REPLY_POLICY.TWILIO_WHATSAPP;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
replyWindowLinkText() {
|
||||||
|
if (this.isAWhatsappChannel) {
|
||||||
|
return this.$t('CONVERSATION.24_HOURS_WINDOW');
|
||||||
|
}
|
||||||
|
if (!this.isAPIInbox) {
|
||||||
|
return this.$t('CONVERSATION.TWILIO_WHATSAPP_24_HOURS_WINDOW');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
@ -101,7 +101,7 @@
|
||||||
:toggle-audio-recorder="toggleAudioRecorder"
|
:toggle-audio-recorder="toggleAudioRecorder"
|
||||||
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
|
:toggle-audio-recorder-play-pause="toggleAudioRecorderPlayPause"
|
||||||
:show-emoji-picker="showEmojiPicker"
|
:show-emoji-picker="showEmojiPicker"
|
||||||
:on-send="sendMessage"
|
:on-send="onSendReply"
|
||||||
:is-send-disabled="isReplyButtonDisabled"
|
:is-send-disabled="isReplyButtonDisabled"
|
||||||
:recording-audio-duration-text="recordingAudioDurationText"
|
:recording-audio-duration-text="recordingAudioDurationText"
|
||||||
:recording-audio-state="recordingAudioState"
|
:recording-audio-state="recordingAudioState"
|
||||||
|
@ -112,7 +112,16 @@
|
||||||
:enable-rich-editor="isRichEditorEnabled"
|
:enable-rich-editor="isRichEditorEnabled"
|
||||||
:enter-to-send-enabled="enterToSendEnabled"
|
:enter-to-send-enabled="enterToSendEnabled"
|
||||||
:enable-multiple-file-upload="enableMultipleFileUpload"
|
:enable-multiple-file-upload="enableMultipleFileUpload"
|
||||||
|
:has-whatsapp-templates="hasWhatsappTemplates"
|
||||||
@toggleEnterToSend="toggleEnterToSend"
|
@toggleEnterToSend="toggleEnterToSend"
|
||||||
|
@selectWhatsappTemplate="openWhatsappTemplateModal"
|
||||||
|
/>
|
||||||
|
<whatsapp-templates
|
||||||
|
:inbox-id="inbox.id"
|
||||||
|
:show="showWhatsAppTemplatesModal"
|
||||||
|
@close="hideWhatsappTemplatesModal"
|
||||||
|
@on-send="onSendWhatsAppReply"
|
||||||
|
@cancel="hideWhatsappTemplatesModal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -137,7 +146,7 @@ import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||||
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
|
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
|
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
|
||||||
import {
|
import {
|
||||||
isEscape,
|
isEscape,
|
||||||
isEnter,
|
isEnter,
|
||||||
|
@ -162,6 +171,7 @@ export default {
|
||||||
WootMessageEditor,
|
WootMessageEditor,
|
||||||
WootAudioRecorder,
|
WootAudioRecorder,
|
||||||
Banner,
|
Banner,
|
||||||
|
WhatsappTemplates,
|
||||||
},
|
},
|
||||||
mixins: [
|
mixins: [
|
||||||
clickaway,
|
clickaway,
|
||||||
|
@ -201,6 +211,7 @@ export default {
|
||||||
hasSlashCommand: false,
|
hasSlashCommand: false,
|
||||||
bccEmails: '',
|
bccEmails: '',
|
||||||
ccEmails: '',
|
ccEmails: '',
|
||||||
|
showWhatsAppTemplatesModal: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -212,7 +223,6 @@ export default {
|
||||||
globalConfig: 'globalConfig/get',
|
globalConfig: 'globalConfig/get',
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
showRichContentEditor() {
|
showRichContentEditor() {
|
||||||
if (this.isOnPrivateNote) {
|
if (this.isOnPrivateNote) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -256,7 +266,10 @@ export default {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
hasWhatsappTemplates() {
|
||||||
|
return !!this.$store.getters['inboxes/getWhatsAppTemplates'](this.inboxId)
|
||||||
|
.length;
|
||||||
|
},
|
||||||
enterToSendEnabled() {
|
enterToSendEnabled() {
|
||||||
return !!this.uiSettings.enter_to_send_enabled;
|
return !!this.uiSettings.enter_to_send_enabled;
|
||||||
},
|
},
|
||||||
|
@ -484,7 +497,7 @@ export default {
|
||||||
hasSendOnEnterEnabled && !hasPressedShift(e) && this.isFocused;
|
hasSendOnEnterEnabled && !hasPressedShift(e) && this.isFocused;
|
||||||
if (shouldSendMessage) {
|
if (shouldSendMessage) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.sendMessage();
|
this.onSendReply();
|
||||||
}
|
}
|
||||||
} else if (hasPressedCommandPlusKKey(e)) {
|
} else if (hasPressedCommandPlusKKey(e)) {
|
||||||
this.openCommandBar();
|
this.openCommandBar();
|
||||||
|
@ -497,6 +510,12 @@ export default {
|
||||||
toggleEnterToSend(enterToSendEnabled) {
|
toggleEnterToSend(enterToSendEnabled) {
|
||||||
this.updateUISettings({ enter_to_send_enabled: enterToSendEnabled });
|
this.updateUISettings({ enter_to_send_enabled: enterToSendEnabled });
|
||||||
},
|
},
|
||||||
|
openWhatsappTemplateModal() {
|
||||||
|
this.showWhatsAppTemplatesModal = true;
|
||||||
|
},
|
||||||
|
hideWhatsappTemplatesModal() {
|
||||||
|
this.showWhatsAppTemplatesModal = false;
|
||||||
|
},
|
||||||
onClickSelfAssign() {
|
onClickSelfAssign() {
|
||||||
const {
|
const {
|
||||||
account_id,
|
account_id,
|
||||||
|
@ -520,7 +539,7 @@ export default {
|
||||||
};
|
};
|
||||||
this.assignedAgent = selfAssign;
|
this.assignedAgent = selfAssign;
|
||||||
},
|
},
|
||||||
async sendMessage() {
|
async onSendReply() {
|
||||||
if (this.isReplyButtonDisabled) {
|
if (this.isReplyButtonDisabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -531,6 +550,12 @@ export default {
|
||||||
}
|
}
|
||||||
const messagePayload = this.getMessagePayload(newMessage);
|
const messagePayload = this.getMessagePayload(newMessage);
|
||||||
this.clearMessage();
|
this.clearMessage();
|
||||||
|
this.sendMessage(messagePayload);
|
||||||
|
this.hideEmojiPicker();
|
||||||
|
this.$emit('update:popoutReplyBox', false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async sendMessage(messagePayload) {
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch(
|
await this.$store.dispatch(
|
||||||
'createPendingMessageAndSend',
|
'createPendingMessageAndSend',
|
||||||
|
@ -539,13 +564,16 @@ export default {
|
||||||
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error?.response?.data?.error ||
|
error?.response?.data?.error || this.$t('CONVERSATION.MESSAGE_ERROR');
|
||||||
this.$t('CONVERSATION.MESSAGE_ERROR');
|
|
||||||
this.showAlert(errorMessage);
|
this.showAlert(errorMessage);
|
||||||
}
|
}
|
||||||
this.hideEmojiPicker();
|
},
|
||||||
this.$emit('update:popoutReplyBox', false);
|
async onSendWhatsAppReply(messagePayload) {
|
||||||
}
|
this.sendMessage({
|
||||||
|
conversationId: this.currentChat.id,
|
||||||
|
...messagePayload,
|
||||||
|
});
|
||||||
|
this.hideWhatsappTemplatesModal();
|
||||||
},
|
},
|
||||||
replaceText(message) {
|
replaceText(message) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<woot-modal :show.sync="show" :on-close="onClose" size="modal-big">
|
||||||
|
<woot-modal-header
|
||||||
|
:header-title="$t('WHATSAPP_TEMPLATES.MODAL.TITLE')"
|
||||||
|
:header-content="modalHeaderContent"
|
||||||
|
/>
|
||||||
|
<div class="row modal-content">
|
||||||
|
<templates-picker
|
||||||
|
v-if="!selectedWaTemplate"
|
||||||
|
:inbox-id="inboxId"
|
||||||
|
@onSelect="pickTemplate"
|
||||||
|
/>
|
||||||
|
<template-parser
|
||||||
|
v-else
|
||||||
|
:template="selectedWaTemplate"
|
||||||
|
@resetTemplate="onResetTemplate"
|
||||||
|
@sendMessage="onSendMessage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</woot-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TemplatesPicker from './TemplatesPicker.vue';
|
||||||
|
import TemplateParser from './TemplateParser.vue';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
TemplatesPicker,
|
||||||
|
TemplateParser,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
inboxId: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedWaTemplate: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
modalHeaderContent() {
|
||||||
|
return this.selectedWaTemplate
|
||||||
|
? this.$t('WHATSAPP_TEMPLATES.MODAL.TEMPLATE_SELECTED_SUBTITLE', {
|
||||||
|
templateName: this.selectedWaTemplate.name,
|
||||||
|
})
|
||||||
|
: this.$t('WHATSAPP_TEMPLATES.MODAL.SUBTITLE');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
pickTemplate(template) {
|
||||||
|
this.selectedWaTemplate = template;
|
||||||
|
},
|
||||||
|
onResetTemplate() {
|
||||||
|
this.selectedWaTemplate = null;
|
||||||
|
},
|
||||||
|
onSendMessage(message) {
|
||||||
|
this.$emit('on-send', message);
|
||||||
|
},
|
||||||
|
onClose() {
|
||||||
|
this.$emit('cancel');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modal-content {
|
||||||
|
padding: 2.5rem 3.2rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,173 @@
|
||||||
|
<template>
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<textarea
|
||||||
|
v-model="processedString"
|
||||||
|
rows="4"
|
||||||
|
readonly
|
||||||
|
class="template-input"
|
||||||
|
/>
|
||||||
|
<div v-if="variables" class="template__variables-container">
|
||||||
|
<p class="variables-label">
|
||||||
|
{{ $t('WHATSAPP_TEMPLATES.PARSER.VARIABLES_LABEL') }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-for="(variable, key) in processedParams"
|
||||||
|
:key="key"
|
||||||
|
class="template__variable-item"
|
||||||
|
>
|
||||||
|
<span class="variable-label">
|
||||||
|
{{ key }}
|
||||||
|
</span>
|
||||||
|
<woot-input
|
||||||
|
v-model="processedParams[key]"
|
||||||
|
type="text"
|
||||||
|
class="variable-input"
|
||||||
|
:styles="{ marginBottom: 0 }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="$v.$dirty && $v.$invalid" class="error">
|
||||||
|
{{ $t('WHATSAPP_TEMPLATES.PARSER.FORM_ERROR_MESSAGE') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<woot-button variant="smooth" @click="$emit('resetTemplate')">
|
||||||
|
{{ $t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL') }}
|
||||||
|
</woot-button>
|
||||||
|
<woot-button @click="sendMessage">
|
||||||
|
{{ $t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL') }}
|
||||||
|
</woot-button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const allKeysRequired = value => {
|
||||||
|
const keys = Object.keys(value);
|
||||||
|
return keys.every(key => value[key]);
|
||||||
|
};
|
||||||
|
import { requiredIf } from 'vuelidate/lib/validators';
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
template: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validations: {
|
||||||
|
processedParams: {
|
||||||
|
requiredIfKeysPresent: requiredIf('variables'),
|
||||||
|
allKeysRequired,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processedParams: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
variables() {
|
||||||
|
const variables = this.templateString.match(/{{([^}]+)}}/g);
|
||||||
|
return variables;
|
||||||
|
},
|
||||||
|
templateString() {
|
||||||
|
return this.template.components.find(
|
||||||
|
component => component.type === 'BODY'
|
||||||
|
).text;
|
||||||
|
},
|
||||||
|
processedString() {
|
||||||
|
return this.templateString.replace(/{{([^}]+)}}/g, (match, variable) => {
|
||||||
|
const variableKey = this.processVariable(variable);
|
||||||
|
return this.processedParams[variableKey] || `{{${variable}}}`;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.generateVariables();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sendMessage() {
|
||||||
|
this.$v.$touch();
|
||||||
|
if (this.$v.$invalid) return;
|
||||||
|
const payload = {
|
||||||
|
message: this.processedString,
|
||||||
|
templateParams: {
|
||||||
|
name: this.template.name,
|
||||||
|
category: this.template.category,
|
||||||
|
language: this.template.language,
|
||||||
|
namespace: this.template.namespace,
|
||||||
|
processed_params: this.processedParams,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.$emit('sendMessage', payload);
|
||||||
|
},
|
||||||
|
processVariable(str) {
|
||||||
|
return str.replace(/{{|}}/g, '');
|
||||||
|
},
|
||||||
|
generateVariables() {
|
||||||
|
const matchedVariables = this.templateString.match(/{{([^}]+)}}/g);
|
||||||
|
if (!matchedVariables) return;
|
||||||
|
|
||||||
|
const variables = matchedVariables.map(i => this.processVariable(i));
|
||||||
|
this.processedParams = variables.reduce((acc, variable) => {
|
||||||
|
acc[variable] = '';
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.template__variables-container {
|
||||||
|
padding: var(--space-one);
|
||||||
|
}
|
||||||
|
|
||||||
|
.variables-label {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
margin-bottom: var(--space-one);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template__variable-item {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: var(--space-one);
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
}
|
||||||
|
|
||||||
|
.variable-input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
margin-left: var(--space-one);
|
||||||
|
}
|
||||||
|
|
||||||
|
.variable-label {
|
||||||
|
background-color: var(--s-75);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
display: inline-block;
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
padding: var(--space-one) var(--space-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-left: var(--space-one);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: var(--r-100);
|
||||||
|
border-radius: var(--border-radius-normal);
|
||||||
|
color: var(--r-800);
|
||||||
|
padding: var(--space-one);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.template-input {
|
||||||
|
background-color: var(--s-25);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,163 @@
|
||||||
|
<template>
|
||||||
|
<div class="medium-12 columns">
|
||||||
|
<div class="templates__list-search">
|
||||||
|
<fluent-icon icon="search" class="search-icon" size="16" />
|
||||||
|
<input
|
||||||
|
ref="search"
|
||||||
|
v-model="query"
|
||||||
|
type="search"
|
||||||
|
:placeholder="$t('WHATSAPP_TEMPLATES.PICKER.SEARCH_PLACEHOLDER')"
|
||||||
|
class="templates__search-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="template__list-container">
|
||||||
|
<div v-for="(template, i) in filteredTemplateMessages" :key="template.id">
|
||||||
|
<button
|
||||||
|
class="template__list-item"
|
||||||
|
@click="$emit('onSelect', template)"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div class="flex-between">
|
||||||
|
<p class="label-title">
|
||||||
|
{{ template.name }}
|
||||||
|
</p>
|
||||||
|
<span class="label-lang label">
|
||||||
|
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.LANGUAGE') }} :
|
||||||
|
{{ template.language }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="strong">
|
||||||
|
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.TEMPLATE_BODY') }}
|
||||||
|
</p>
|
||||||
|
<p class="label-body">{{ getTemplatebody(template) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="label-category">
|
||||||
|
<p class="strong">
|
||||||
|
{{ $t('WHATSAPP_TEMPLATES.PICKER.LABELS.CATEGORY') }}
|
||||||
|
</p>
|
||||||
|
<p>{{ template.category }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<hr v-if="i != filteredTemplateMessages.length - 1" :key="`hr-${i}`" />
|
||||||
|
</div>
|
||||||
|
<div v-if="!filteredTemplateMessages.length">
|
||||||
|
<p>
|
||||||
|
{{ $t('WHATSAPP_TEMPLATES.PICKER.NO_TEMPLATES_FOUND') }}
|
||||||
|
<strong>{{ query }}</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
inboxId: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
query: '',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
whatsAppTemplateMessages() {
|
||||||
|
return this.$store.getters['inboxes/getWhatsAppTemplates'](this.inboxId);
|
||||||
|
},
|
||||||
|
filteredTemplateMessages() {
|
||||||
|
return this.whatsAppTemplateMessages.filter(template =>
|
||||||
|
template.name.toLowerCase().includes(this.query.toLowerCase())
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getTemplatebody(template) {
|
||||||
|
return template.components.find(component => component.type === 'BODY')
|
||||||
|
.text;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.flex-between {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--space-one);
|
||||||
|
}
|
||||||
|
|
||||||
|
.templates__list-search {
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--s-25);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
border: 1px solid var(--s-100);
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: var(--space-one);
|
||||||
|
padding: 0 var(--space-one);
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
color: var(--s-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.templates__search-input {
|
||||||
|
background-color: transparent;
|
||||||
|
border: var(--space-large);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
height: unset;
|
||||||
|
margin: var(--space-zero);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.template__list-container {
|
||||||
|
background-color: var(--s-25);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
max-height: 30rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--space-one);
|
||||||
|
|
||||||
|
.template__list-item {
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
padding: var(--space-one);
|
||||||
|
text-align: left;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--w-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-title {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-category {
|
||||||
|
margin-top: var(--space-two);
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-body {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.strong {
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-bottom: 1px solid var(--s-100);
|
||||||
|
margin: var(--space-one) auto;
|
||||||
|
max-width: 95%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -7,7 +7,7 @@
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
|
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
|
||||||
icon="checkmark"
|
icon="checkmark"
|
||||||
size="16"
|
size="14"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
|
@ -165,7 +165,11 @@ export default {
|
||||||
return `https://www.instagram.com/stories/${storySender}/${storyId}`;
|
return `https://www.instagram.com/stories/${storySender}/${storyId}`;
|
||||||
},
|
},
|
||||||
showSentIndicator() {
|
showSentIndicator() {
|
||||||
return this.isOutgoing && this.sourceId && this.isAnEmailChannel;
|
return (
|
||||||
|
this.isOutgoing &&
|
||||||
|
this.sourceId &&
|
||||||
|
(this.isAnEmailChannel || this.isAWhatsappChannel)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -29,8 +29,11 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fileName() {
|
fileName() {
|
||||||
|
if (this.url) {
|
||||||
const filename = this.url.substring(this.url.lastIndexOf('/') + 1);
|
const filename = this.url.substring(this.url.lastIndexOf('/') + 1);
|
||||||
return filename;
|
return filename || this.$t('CONVERSATION.UNKNOWN_FILE_TYPE');
|
||||||
|
}
|
||||||
|
return this.$t('CONVERSATION.UNKNOWN_FILE_TYPE');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
'hide--quoted': !showQuotedContent,
|
'hide--quoted': !showQuotedContent,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div v-dompurify-html="message" class="text-content"></div>
|
<div v-dompurify-html="message" class="text-content" />
|
||||||
<button
|
<button
|
||||||
v-if="displayQuotedButton"
|
v-if="displayQuotedButton"
|
||||||
class="quoted-text--button"
|
class="quoted-text--button"
|
||||||
|
|
|
@ -0,0 +1,268 @@
|
||||||
|
<template>
|
||||||
|
<div class="bulk-action__agents">
|
||||||
|
<div class="triangle">
|
||||||
|
<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.AGENT_SELECT_LABEL') }}</span>
|
||||||
|
<woot-button
|
||||||
|
size="tiny"
|
||||||
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="dismiss"
|
||||||
|
@click="onClose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<div v-if="uiFlags.isUpdating" class="agent__list-loading">
|
||||||
|
<spinner />
|
||||||
|
<p>{{ $t('BULK_ACTION.AGENT_LIST_LOADING') }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="agent__list-container">
|
||||||
|
<ul v-if="!selectedAgent">
|
||||||
|
<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>
|
||||||
|
<li v-for="agent in filteredAgents" :key="agent.id">
|
||||||
|
<div class="agent-list-item" @click="assignAgent(agent)">
|
||||||
|
<thumbnail
|
||||||
|
src="agent.thumbnail"
|
||||||
|
:username="agent.name"
|
||||||
|
size="22px"
|
||||||
|
class="margin-right-small"
|
||||||
|
/>
|
||||||
|
<span class="reports-option__title">{{ agent.name }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div v-else class="agent-confirmation-container">
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
$t('BULK_ACTION.ASSIGN_CONFIRMATION_LABEL', {
|
||||||
|
conversationCount,
|
||||||
|
conversationLabel,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
<strong>
|
||||||
|
{{ selectedAgent.name }}
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
<div class="agent-confirmation-actions">
|
||||||
|
<woot-button
|
||||||
|
color-scheme="primary"
|
||||||
|
variant="smooth"
|
||||||
|
@click="goBack"
|
||||||
|
>
|
||||||
|
{{ $t('BULK_ACTION.GO_BACK_LABEL') }}
|
||||||
|
</woot-button>
|
||||||
|
<woot-button
|
||||||
|
color-scheme="primary"
|
||||||
|
variant="flat"
|
||||||
|
:is-loading="uiFlags.isUpdating"
|
||||||
|
@click="submit"
|
||||||
|
>
|
||||||
|
{{ $t('BULK_ACTION.ASSIGN_LABEL') }}
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
|
import Spinner from 'shared/components/Spinner';
|
||||||
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Thumbnail,
|
||||||
|
Spinner,
|
||||||
|
},
|
||||||
|
mixins: [clickaway],
|
||||||
|
props: {
|
||||||
|
selectedInboxes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
conversationCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
query: '',
|
||||||
|
selectedAgent: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
uiFlags: 'bulkActions/getUIFlags',
|
||||||
|
inboxes: 'inboxes/getInboxes',
|
||||||
|
assignableAgentsUiFlags: 'inboxAssignableAgents/getUIFlags',
|
||||||
|
}),
|
||||||
|
filteredAgents() {
|
||||||
|
if (this.query) {
|
||||||
|
return this.assignableAgents.filter(agent =>
|
||||||
|
agent.name.toLowerCase().includes(this.query.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return this.assignableAgents;
|
||||||
|
},
|
||||||
|
assignableAgents() {
|
||||||
|
return this.$store.getters['inboxAssignableAgents/getAssignableAgents'](
|
||||||
|
this.selectedInboxes.join(',')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
conversationLabel() {
|
||||||
|
return this.conversationCount > 1 ? 'conversations' : 'conversation';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('inboxAssignableAgents/fetch', this.selectedInboxes);
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submit() {
|
||||||
|
this.$emit('select', this.selectedAgent);
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
this.selectedAgent = null;
|
||||||
|
},
|
||||||
|
assignAgent(agent) {
|
||||||
|
this.selectedAgent = agent;
|
||||||
|
},
|
||||||
|
onClose() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.bulk-action__agents {
|
||||||
|
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);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: var(--space-one);
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-height: 24rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
.agent__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(--space-micro);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-confirmation-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
padding: var(--space-one);
|
||||||
|
p {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.agent-confirmation-actions {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: var(--space-one);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.search-container {
|
||||||
|
padding: 0 var(--space-one);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: var(--z-index-twenty);
|
||||||
|
background-color: var(--white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent__list-loading {
|
||||||
|
height: calc(95% - var(--space-one));
|
||||||
|
margin: var(--space-one);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
background-color: var(--s-50);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,211 @@
|
||||||
|
<template>
|
||||||
|
<div class="bulk-action__container">
|
||||||
|
<div class="flex-between">
|
||||||
|
<label class="bulk-action__panel flex-between">
|
||||||
|
<input
|
||||||
|
ref="selectAllCheck"
|
||||||
|
type="checkbox"
|
||||||
|
class="checkbox"
|
||||||
|
:checked="allConversationsSelected"
|
||||||
|
:indeterminate.prop="!allConversationsSelected"
|
||||||
|
@change="selectAll($event)"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
$t('BULK_ACTION.CONVERSATIONS_SELECTED', {
|
||||||
|
conversationCount: conversations.length,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div class="bulk-action__actions flex-between">
|
||||||
|
<woot-button
|
||||||
|
v-tooltip="$t('BULK_ACTION.LABELS.ASSIGN_LABELS')"
|
||||||
|
size="tiny"
|
||||||
|
variant="smooth"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="tag"
|
||||||
|
class="margin-right-smaller"
|
||||||
|
@click="toggleLabelActions"
|
||||||
|
/>
|
||||||
|
<woot-button
|
||||||
|
v-tooltip="$t('BULK_ACTION.UPDATE.CHANGE_STATUS')"
|
||||||
|
size="tiny"
|
||||||
|
variant="smooth"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="repeat"
|
||||||
|
class="margin-right-smaller"
|
||||||
|
@click="toggleUpdateActions"
|
||||||
|
/>
|
||||||
|
<woot-button
|
||||||
|
v-tooltip="$t('BULK_ACTION.ASSIGN_AGENT_TOOLTIP')"
|
||||||
|
size="tiny"
|
||||||
|
variant="smooth"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="person-assign"
|
||||||
|
@click="toggleAgentList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<transition name="popover-animation">
|
||||||
|
<label-actions
|
||||||
|
v-if="showLabelActions"
|
||||||
|
@assign="assignLabels"
|
||||||
|
@close="showLabelActions = false"
|
||||||
|
/>
|
||||||
|
</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">
|
||||||
|
<update-actions
|
||||||
|
v-if="showUpdateActions"
|
||||||
|
:selected-inboxes="selectedInboxes"
|
||||||
|
:conversation-count="conversations.length"
|
||||||
|
:show-resolve="!showResolvedAction"
|
||||||
|
:show-reopen="!showOpenAction"
|
||||||
|
:show-snooze="!showSnoozedAction"
|
||||||
|
@update="updateConversations"
|
||||||
|
@close="showUpdateActions = false"
|
||||||
|
/>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
<div v-if="allConversationsSelected" class="bulk-action__alert">
|
||||||
|
{{ $t('BULK_ACTION.ALL_CONVERSATIONS_SELECTED_ALERT') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AgentSelector from './AgentSelector.vue';
|
||||||
|
import UpdateActions from './UpdateActions.vue';
|
||||||
|
import LabelActions from './LabelActions.vue';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
AgentSelector,
|
||||||
|
UpdateActions,
|
||||||
|
LabelActions,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
conversations: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
allConversationsSelected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
selectedInboxes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
showOpenAction: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showResolvedAction: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showSnoozedAction: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showAgentsList: false,
|
||||||
|
showUpdateActions: false,
|
||||||
|
showLabelActions: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectAll(e) {
|
||||||
|
this.$emit('select-all-conversations', e.target.checked);
|
||||||
|
},
|
||||||
|
submit(agent) {
|
||||||
|
this.$emit('assign-agent', agent);
|
||||||
|
},
|
||||||
|
updateConversations(status) {
|
||||||
|
this.$emit('update-conversations', status);
|
||||||
|
},
|
||||||
|
assignLabels(labels) {
|
||||||
|
this.$emit('assign-labels', labels);
|
||||||
|
},
|
||||||
|
resolveConversations() {
|
||||||
|
this.$emit('resolve-conversations');
|
||||||
|
},
|
||||||
|
toggleUpdateActions() {
|
||||||
|
this.showUpdateActions = !this.showUpdateActions;
|
||||||
|
},
|
||||||
|
toggleLabelActions() {
|
||||||
|
this.showLabelActions = !this.showLabelActions;
|
||||||
|
},
|
||||||
|
toggleAgentList() {
|
||||||
|
this.showAgentsList = !this.showAgentsList;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.bulk-action__container {
|
||||||
|
border-bottom: 1px solid var(--s-100);
|
||||||
|
padding: var(--space-normal) var(--space-one);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-action__panel {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
margin-left: var(--space-smaller);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
cursor: pointer;
|
||||||
|
margin: var(--space-zero);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-action__alert {
|
||||||
|
background-color: var(--y-50);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
border: 1px solid var(--y-300);
|
||||||
|
color: var(--y-700);
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
margin-top: var(--space-small);
|
||||||
|
padding: var(--space-half) var(--space-one);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-animation-enter-active,
|
||||||
|
.popover-animation-leave-active {
|
||||||
|
transition: transform ease-out 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-animation-enter {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-animation-enter-to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-animation-leave {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-animation-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,282 @@
|
||||||
|
<template>
|
||||||
|
<div v-on-clickaway="onClose" class="labels-container">
|
||||||
|
<div class="triangle">
|
||||||
|
<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.LABELS.ASSIGN_LABELS') }}</span>
|
||||||
|
<woot-button
|
||||||
|
size="tiny"
|
||||||
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="dismiss"
|
||||||
|
@click="onClose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="labels-list">
|
||||||
|
<header class="labels-list__header">
|
||||||
|
<div class="label-list-search flex-between">
|
||||||
|
<fluent-icon icon="search" class="search-icon" size="16" />
|
||||||
|
<input
|
||||||
|
ref="search"
|
||||||
|
v-model="query"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search"
|
||||||
|
class="label--search_input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<ul class="labels-list__body">
|
||||||
|
<li
|
||||||
|
v-for="label in filteredLabels"
|
||||||
|
:key="label.id"
|
||||||
|
class="label__list-item"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="item"
|
||||||
|
:class="{ 'label-selected': isLabelSelected(label.title) }"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="selectedLabels"
|
||||||
|
type="checkbox"
|
||||||
|
:value="label.title"
|
||||||
|
class="label-checkbox"
|
||||||
|
/>
|
||||||
|
<span class="label-title">{{ label.title }}</span>
|
||||||
|
<span
|
||||||
|
class="label-pill"
|
||||||
|
:style="{ backgroundColor: label.color }"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<footer class="labels-list__footer">
|
||||||
|
<woot-button
|
||||||
|
size="small"
|
||||||
|
color-scheme="primary"
|
||||||
|
:disabled="!selectedLabels.length"
|
||||||
|
@click="$emit('assign', selectedLabels)"
|
||||||
|
>
|
||||||
|
<span>{{ $t('BULK_ACTION.LABELS.ASSIGN_SELECTED_LABELS') }}</span>
|
||||||
|
</woot-button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [clickaway],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
query: '',
|
||||||
|
selectedLabels: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({ labels: 'labels/getLabels' }),
|
||||||
|
filteredLabels() {
|
||||||
|
return this.labels.filter(label =>
|
||||||
|
label.title.toLowerCase().includes(this.query.toLowerCase())
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isLabelSelected(label) {
|
||||||
|
return this.selectedLabels.includes(label);
|
||||||
|
},
|
||||||
|
assignLabels(key) {
|
||||||
|
this.$emit('update', key);
|
||||||
|
},
|
||||||
|
onClose() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.labels-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 24rem;
|
||||||
|
min-height: auto;
|
||||||
|
|
||||||
|
.labels-list__header {
|
||||||
|
background-color: var(--white);
|
||||||
|
padding: 0 var(--space-one);
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-list__body {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--space-one) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-list__footer {
|
||||||
|
padding: var(--space-small);
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-list-search {
|
||||||
|
background-color: var(--s-50);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
border: 1px solid var(--s-100);
|
||||||
|
padding: 0 var(--space-one);
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
color: var(--s-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label--search_input {
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0;
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
height: unset;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-container {
|
||||||
|
background-color: var(--white);
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
border: 1px solid var(--s-50);
|
||||||
|
box-shadow: var(--shadow-dropdown-pane);
|
||||||
|
max-width: 24rem;
|
||||||
|
min-width: 24rem;
|
||||||
|
position: absolute;
|
||||||
|
right: 4.5rem;
|
||||||
|
top: var(--space-larger);
|
||||||
|
transform-origin: top right;
|
||||||
|
width: auto;
|
||||||
|
z-index: var(--z-index-twenty);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: var(--space-one);
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-height: 24rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.label__list-container {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.label-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label--search_input {
|
||||||
|
border: 0;
|
||||||
|
font-size: var(--font-size-mini);
|
||||||
|
margin: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.triangle {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
right: 2rem;
|
||||||
|
text-align: left;
|
||||||
|
top: calc(var(--space-slab) * -1);
|
||||||
|
z-index: var(--z-index-one);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labels-placeholder {
|
||||||
|
padding: var(--space-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label__list-item {
|
||||||
|
margin: var(--space-smaller) 0;
|
||||||
|
padding: 0 var(--space-one);
|
||||||
|
|
||||||
|
.item {
|
||||||
|
align-items: center;
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
padding: var(--space-smaller) var(--space-one);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--s-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.label-selected {
|
||||||
|
background-color: var(--s-50);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-checkbox {
|
||||||
|
margin: 0 var(--space-one) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-title {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-pill {
|
||||||
|
background-color: var(--s-50);
|
||||||
|
border-radius: var(--border-radius-medium);
|
||||||
|
height: var(--space-slab);
|
||||||
|
width: var(--space-slab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
background-color: var(--white);
|
||||||
|
padding: 0 var(--space-one);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: var(--z-index-twenty);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions-container {
|
||||||
|
background-color: var(--white);
|
||||||
|
bottom: 0;
|
||||||
|
padding: var(--space-small);
|
||||||
|
position: sticky;
|
||||||
|
z-index: var(--z-index-twenty);
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,166 @@
|
||||||
|
<template>
|
||||||
|
<div v-on-clickaway="onClose" class="actions-container">
|
||||||
|
<div class="triangle">
|
||||||
|
<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.UPDATE.CHANGE_STATUS') }}</span>
|
||||||
|
<woot-button
|
||||||
|
size="tiny"
|
||||||
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="dismiss"
|
||||||
|
@click="onClose"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<woot-dropdown-menu>
|
||||||
|
<template v-for="action in actions">
|
||||||
|
<woot-dropdown-item v-if="showAction(action.key)" :key="action.key">
|
||||||
|
<woot-button
|
||||||
|
variant="clear"
|
||||||
|
color-scheme="secondary"
|
||||||
|
size="small"
|
||||||
|
:icon="action.icon"
|
||||||
|
@click="updateConversations(action.key)"
|
||||||
|
>
|
||||||
|
{{ actionLabel(action.key) }}
|
||||||
|
</woot-button>
|
||||||
|
</woot-dropdown-item>
|
||||||
|
</template>
|
||||||
|
</woot-dropdown-menu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mixin as clickaway } from 'vue-clickaway';
|
||||||
|
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
|
||||||
|
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
WootDropdownItem,
|
||||||
|
WootDropdownMenu,
|
||||||
|
},
|
||||||
|
mixins: [clickaway],
|
||||||
|
props: {
|
||||||
|
selectedInboxes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
conversationCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
showResolve: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showReopen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showSnooze: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
query: '',
|
||||||
|
selectedAction: null,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
icon: 'checkmark',
|
||||||
|
key: 'resolved',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'arrow-redo',
|
||||||
|
key: 'open',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: 'send-clock',
|
||||||
|
key: 'snoozed',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateConversations(key) {
|
||||||
|
this.$emit('update', key);
|
||||||
|
},
|
||||||
|
goBack() {
|
||||||
|
this.selectedAgent = null;
|
||||||
|
},
|
||||||
|
onClose() {
|
||||||
|
this.$emit('close');
|
||||||
|
},
|
||||||
|
showAction(key) {
|
||||||
|
const actionsMap = {
|
||||||
|
resolved: this.showResolve,
|
||||||
|
open: this.showReopen,
|
||||||
|
snoozed: this.showSnooze,
|
||||||
|
};
|
||||||
|
return actionsMap[key] || false;
|
||||||
|
},
|
||||||
|
actionLabel(key) {
|
||||||
|
const labelsMap = {
|
||||||
|
resolved: this.$t('CONVERSATION.HEADER.RESOLVE_ACTION'),
|
||||||
|
open: this.$t('CONVERSATION.HEADER.REOPEN_ACTION'),
|
||||||
|
snoozed: this.$t('BULK_ACTION.UPDATE.SNOOZE_UNTIL_NEXT_REPLY'),
|
||||||
|
};
|
||||||
|
return labelsMap[key] || '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.actions-container {
|
||||||
|
background-color: var(--white);
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
border: 1px solid var(--s-50);
|
||||||
|
box-shadow: var(--shadow-dropdown-pane);
|
||||||
|
position: absolute;
|
||||||
|
right: var(--space-small);
|
||||||
|
top: 48px;
|
||||||
|
transform-origin: top right;
|
||||||
|
width: auto;
|
||||||
|
z-index: var(--z-index-twenty);
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: var(--space-one);
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: var(--font-size-small);
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
padding: var(--space-one);
|
||||||
|
padding-top: var(--space-zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
.triangle {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
right: 2.8rem;
|
||||||
|
text-align: left;
|
||||||
|
top: calc(var(--space-slab) * -1);
|
||||||
|
z-index: var(--z-index-one);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -22,7 +22,7 @@
|
||||||
accept="image/png, image/jpeg, image/gif"
|
accept="image/png, image/jpeg, image/gif"
|
||||||
@change="handleImageUpload"
|
@change="handleImageUpload"
|
||||||
/>
|
/>
|
||||||
<slot></slot>
|
<slot />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -6,10 +6,11 @@
|
||||||
:type="type"
|
:type="type"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:readonly="readonly"
|
:readonly="readonly"
|
||||||
|
:style="styles"
|
||||||
@input="onChange"
|
@input="onChange"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
/>
|
/>
|
||||||
<p v-if="helpText" class="help-text"></p>
|
<p v-if="helpText" class="help-text" />
|
||||||
<span v-if="error" class="message">
|
<span v-if="error" class="message">
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -47,6 +48,10 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
deafaut: false,
|
deafaut: false,
|
||||||
},
|
},
|
||||||
|
styles: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onChange(e) {
|
onChange(e) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<modal :show.sync="show" :on-close="cancel">
|
<modal :show.sync="show" :on-close="cancel">
|
||||||
<div class="column content-box">
|
<div class="column content-box">
|
||||||
<woot-modal-header :header-title="title"> </woot-modal-header>
|
<woot-modal-header :header-title="title" />
|
||||||
<div class="row modal-content">
|
<div class="row modal-content">
|
||||||
<div class="medium-12 columns">
|
<div class="medium-12 columns">
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
export const downloadCsvFile = (fileName, fileContent) => {
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.download = fileName;
|
|
||||||
link.href = `data:text/csv;charset=utf-8,` + encodeURI(fileContent);
|
|
||||||
link.click();
|
|
||||||
};
|
|
22
app/javascript/dashboard/helper/downloadHelper.js
Normal file
22
app/javascript/dashboard/helper/downloadHelper.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
|
||||||
|
export const downloadCsvFile = (fileName, content) => {
|
||||||
|
const contentType = 'data:text/csv;charset=utf-8;';
|
||||||
|
const blob = new Blob([content], { type: contentType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.setAttribute('download', fileName);
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.click();
|
||||||
|
return link;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateFileName = ({ type, to, businessHours = false }) => {
|
||||||
|
let name = `${type}-report-${format(fromUnixTime(to), 'dd-MM-yyyy')}`;
|
||||||
|
if (businessHours) {
|
||||||
|
name = `${name}-business-hours`;
|
||||||
|
}
|
||||||
|
return `${name}.csv`;
|
||||||
|
};
|
|
@ -35,3 +35,10 @@ export const getInboxClassByType = (type, phoneNumber) => {
|
||||||
return 'chat';
|
return 'chat';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getInboxWarningIconClass = (type, reauthorizationRequired) => {
|
||||||
|
if (type === INBOX_TYPES.FB && reauthorizationRequired) {
|
||||||
|
return 'warning';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { downloadCsvFile } from '../downloadCsvFile';
|
|
||||||
|
|
||||||
const fileName = 'test.csv';
|
|
||||||
const fileData = `Agent name,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes)
|
|
||||||
Pranav,36,114,28411`;
|
|
||||||
|
|
||||||
describe('#downloadCsvFile', () => {
|
|
||||||
it('should download the csv file', () => {
|
|
||||||
const link = {
|
|
||||||
click: jest.fn(),
|
|
||||||
};
|
|
||||||
jest.spyOn(document, 'createElement').mockImplementation(() => link);
|
|
||||||
|
|
||||||
downloadCsvFile(fileName, fileData);
|
|
||||||
expect(link.download).toEqual(fileName);
|
|
||||||
expect(link.href).toEqual(
|
|
||||||
`data:text/csv;charset=utf-8,${encodeURI(fileData)}`
|
|
||||||
);
|
|
||||||
expect(link.click).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
13
app/javascript/dashboard/helper/specs/downloadHelper.spec.js
Normal file
13
app/javascript/dashboard/helper/specs/downloadHelper.spec.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { generateFileName } from '../downloadHelper';
|
||||||
|
|
||||||
|
describe('#generateFileName', () => {
|
||||||
|
it('should generate the correct file name', () => {
|
||||||
|
expect(generateFileName({ type: 'csat', to: 1652812199 })).toEqual(
|
||||||
|
'csat-report-17-05-2022.csv'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
generateFileName({ type: 'csat', to: 1652812199, businessHours: true })
|
||||||
|
).toEqual('csat-report-17-05-2022-business-hours.csv');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,4 +1,4 @@
|
||||||
import { getInboxClassByType } from '../inbox';
|
import { getInboxClassByType, getInboxWarningIconClass } from '../inbox';
|
||||||
|
|
||||||
describe('#Inbox Helpers', () => {
|
describe('#Inbox Helpers', () => {
|
||||||
describe('getInboxClassByType', () => {
|
describe('getInboxClassByType', () => {
|
||||||
|
@ -34,4 +34,12 @@ describe('#Inbox Helpers', () => {
|
||||||
expect(getInboxClassByType('Channel::Email')).toEqual('mail');
|
expect(getInboxClassByType('Channel::Email')).toEqual('mail');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getInboxWarningIconClass', () => {
|
||||||
|
it('should return correct class for warning', () => {
|
||||||
|
expect(getInboxWarningIconClass('Channel::FacebookPage', true)).toEqual(
|
||||||
|
'warning'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
17
app/javascript/dashboard/i18n/locale/ar/bulkActions.json
Normal file
17
app/javascript/dashboard/i18n/locale/ar/bulkActions.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"BULK_ACTION": {
|
||||||
|
"CONVERSATIONS_SELECTED": "%{conversationCount} المحادثات المحددة",
|
||||||
|
"AGENT_SELECT_LABEL": "اختر وكيل",
|
||||||
|
"ASSIGN_CONFIRMATION_LABEL": "هل أنت متأكد من أنك تريد تعيين %{conversationCount} %{conversationLabel} إلى",
|
||||||
|
"GO_BACK_LABEL": "العودة للخلف",
|
||||||
|
"ASSIGN_LABEL": "تكليف",
|
||||||
|
"ASSIGN_AGENT_TOOLTIP": "إسناد وكيل",
|
||||||
|
"RESOLVE_TOOLTIP": "إغلاق المحادثة",
|
||||||
|
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح",
|
||||||
|
"ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى",
|
||||||
|
"RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح",
|
||||||
|
"RESOLVE_FAILED": "فشل في حل المحادثات، يرجى المحاولة مرة أخرى",
|
||||||
|
"ALL_CONVERSATIONS_SELECTED_ALERT": "المحادثات المرئية في هذه الصفحة هي المحددة فقط.",
|
||||||
|
"AGENT_LIST_LOADING": "تحميل الوكلاء"
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,33 +12,11 @@
|
||||||
"INPUT": "البحث عن جهات الاتصال، المحادثات، قوالب الردود الجاهزة .."
|
"INPUT": "البحث عن جهات الاتصال، المحادثات، قوالب الردود الجاهزة .."
|
||||||
},
|
},
|
||||||
"FILTER_ALL": "الكل",
|
"FILTER_ALL": "الكل",
|
||||||
"STATUS_TABS": [
|
"ASSIGNEE_TYPE_TABS": {
|
||||||
{
|
"me": "محادثاتي",
|
||||||
"NAME": "فتح",
|
"unassigned": "غير مسند",
|
||||||
"KEY": "openCount"
|
"all": "الكل"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"NAME": "مغلقة",
|
|
||||||
"KEY": "allConvCount"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"ASSIGNEE_TYPE_TABS": [
|
|
||||||
{
|
|
||||||
"NAME": "محادثاتي",
|
|
||||||
"KEY": "me",
|
|
||||||
"COUNT_KEY": "mineCount"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NAME": "غير مسند",
|
|
||||||
"KEY": "unassigned",
|
|
||||||
"COUNT_KEY": "unAssignedCount"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NAME": "الكل",
|
|
||||||
"KEY": "all",
|
|
||||||
"COUNT_KEY": "allCount"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"CHAT_STATUS_FILTER_ITEMS": {
|
"CHAT_STATUS_FILTER_ITEMS": {
|
||||||
"open": {
|
"open": {
|
||||||
"TEXT": "فتح"
|
"TEXT": "فتح"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"CONVERSATION": {
|
"CONVERSATION": {
|
||||||
"404": "الرجاء اختيار محادثة من قائمة المحادثات",
|
"404": "الرجاء اختيار محادثة من قائمة المحادثات",
|
||||||
|
"DASHBOARD_APP_TAB_MESSAGES": "الرسائل",
|
||||||
"UNVERIFIED_SESSION": "لم يتم التحقق من هوية هذا المستخدم",
|
"UNVERIFIED_SESSION": "لم يتم التحقق من هوية هذا المستخدم",
|
||||||
"NO_MESSAGE_1": "لا توجد رسائل بعد من العملاء في صندوق الوارد الخاص بك.",
|
"NO_MESSAGE_1": "لا توجد رسائل بعد من العملاء في صندوق الوارد الخاص بك.",
|
||||||
"NO_MESSAGE_2": " لإرسال رسالة إلى الصفحة الخاصة بك!",
|
"NO_MESSAGE_2": " لإرسال رسالة إلى الصفحة الخاصة بك!",
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
"REPLYING_TO": "أنت ترد على:",
|
"REPLYING_TO": "أنت ترد على:",
|
||||||
"REMOVE_SELECTION": "إزالة التحديد",
|
"REMOVE_SELECTION": "إزالة التحديد",
|
||||||
"DOWNLOAD": "تنزيل",
|
"DOWNLOAD": "تنزيل",
|
||||||
|
"UNKNOWN_FILE_TYPE": "ملف غير معروف",
|
||||||
"UPLOADING_ATTACHMENTS": "جاري تحميل المرفقات...",
|
"UPLOADING_ATTACHMENTS": "جاري تحميل المرفقات...",
|
||||||
"SUCCESS_DELETE_MESSAGE": "تم حذف الرسالة بنجاح",
|
"SUCCESS_DELETE_MESSAGE": "تم حذف الرسالة بنجاح",
|
||||||
"FAIL_DELETE_MESSSAGE": "تعذر حذف الرسالة! حاول مرة أخرى",
|
"FAIL_DELETE_MESSSAGE": "تعذر حذف الرسالة! حاول مرة أخرى",
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { default as _advancedFilters } from './advancedFilters.json';
|
||||||
import { default as _agentMgmt } from './agentMgmt.json';
|
import { default as _agentMgmt } from './agentMgmt.json';
|
||||||
import { default as _attributesMgmt } from './attributesMgmt.json';
|
import { default as _attributesMgmt } from './attributesMgmt.json';
|
||||||
import { default as _automation } from './automation.json';
|
import { default as _automation } from './automation.json';
|
||||||
|
import { default as _bulkActions } from './bulkActions.json';
|
||||||
import { default as _campaign } from './campaign.json';
|
import { default as _campaign } from './campaign.json';
|
||||||
import { default as _cannedMgmt } from './cannedMgmt.json';
|
import { default as _cannedMgmt } from './cannedMgmt.json';
|
||||||
import { default as _chatlist } from './chatlist.json';
|
import { default as _chatlist } from './chatlist.json';
|
||||||
|
@ -21,6 +22,7 @@ import { default as _setNewPassword } from './setNewPassword.json';
|
||||||
import { default as _settings } from './settings.json';
|
import { default as _settings } from './settings.json';
|
||||||
import { default as _signup } from './signup.json';
|
import { default as _signup } from './signup.json';
|
||||||
import { default as _teamsSettings } from './teamsSettings.json';
|
import { default as _teamsSettings } from './teamsSettings.json';
|
||||||
|
import { default as _whatsappTemplates } from './whatsappTemplates.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
..._advancedFilters,
|
..._advancedFilters,
|
||||||
|
@ -46,4 +48,6 @@ export default {
|
||||||
..._settings,
|
..._settings,
|
||||||
..._signup,
|
..._signup,
|
||||||
..._teamsSettings,
|
..._teamsSettings,
|
||||||
|
..._whatsappTemplates,
|
||||||
|
..._bulkActions,
|
||||||
};
|
};
|
||||||
|
|
|
@ -386,6 +386,7 @@
|
||||||
"CSAT_REPORTS": {
|
"CSAT_REPORTS": {
|
||||||
"HEADER": "تقارير CSAT",
|
"HEADER": "تقارير CSAT",
|
||||||
"NO_RECORDS": "لا توجد ردود متوفرة على الدراسة الاستقصائية CSAT.",
|
"NO_RECORDS": "لا توجد ردود متوفرة على الدراسة الاستقصائية CSAT.",
|
||||||
|
"DOWNLOAD": "تحميل تقرير رضاء خدمة العملاء",
|
||||||
"FILTERS": {
|
"FILTERS": {
|
||||||
"AGENTS": {
|
"AGENTS": {
|
||||||
"PLACEHOLDER": "اختر الوكلاء"
|
"PLACEHOLDER": "اختر الوكلاء"
|
||||||
|
|
|
@ -189,7 +189,8 @@
|
||||||
"REPORTS_TEAM": "الفريق",
|
"REPORTS_TEAM": "الفريق",
|
||||||
"SET_AVAILABILITY_TITLE": "تعيين نفسك كـ",
|
"SET_AVAILABILITY_TITLE": "تعيين نفسك كـ",
|
||||||
"BETA": "تجريبي",
|
"BETA": "تجريبي",
|
||||||
"REPORTS_OVERVIEW": "نظرة عامة"
|
"REPORTS_OVERVIEW": "نظرة عامة",
|
||||||
|
"FACEBOOK_REAUTHORIZE": "انتهت صلاحية اتصال الفيسبوك الخاص بك، يرجى إعادة الاتصال بصفحة الفيسبوك الخاصة بك لمواصلة الخدمات"
|
||||||
},
|
},
|
||||||
"CREATE_ACCOUNT": {
|
"CREATE_ACCOUNT": {
|
||||||
"NO_ACCOUNT_WARNING": "أوه! لم نتمكن من العثور على الحساب. الرجاء إنشاء حساب جديد للمتابعة.",
|
"NO_ACCOUNT_WARNING": "أوه! لم نتمكن من العثور على الحساب. الرجاء إنشاء حساب جديد للمتابعة.",
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"WHATSAPP_TEMPLATES": {
|
||||||
|
"MODAL": {
|
||||||
|
"TITLE": "قوالب Whatsapp",
|
||||||
|
"SUBTITLE": "حدد قالب ما تريد إرساله",
|
||||||
|
"TEMPLATE_SELECTED_SUBTITLE": "معالجة %{templateName}"
|
||||||
|
},
|
||||||
|
"PICKER": {
|
||||||
|
"SEARCH_PLACEHOLDER": "نماذج البحث",
|
||||||
|
"NO_TEMPLATES_FOUND": "لم يتم العثور على قوالب",
|
||||||
|
"LABELS": {
|
||||||
|
"LANGUAGE": "اللغة",
|
||||||
|
"TEMPLATE_BODY": "نص القالب",
|
||||||
|
"CATEGORY": "الفئة"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PARSER": {
|
||||||
|
"VARIABLES_LABEL": "المتغيرات",
|
||||||
|
"VARIABLE_PLACEHOLDER": "أدخل قيمة %{variable}",
|
||||||
|
"GO_BACK_LABEL": "العودة للخلف",
|
||||||
|
"SEND_MESSAGE_LABEL": "إرسال الرسالة",
|
||||||
|
"FORM_ERROR_MESSAGE": "يرجى ملء جميع المتغيرات قبل الإرسال"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
app/javascript/dashboard/i18n/locale/bg/bulkActions.json
Normal file
17
app/javascript/dashboard/i18n/locale/bg/bulkActions.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"BULK_ACTION": {
|
||||||
|
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||||
|
"AGENT_SELECT_LABEL": "Select Agent",
|
||||||
|
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
|
||||||
|
"GO_BACK_LABEL": "Go back",
|
||||||
|
"ASSIGN_LABEL": "Assign",
|
||||||
|
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
|
||||||
|
"RESOLVE_TOOLTIP": "Resolve",
|
||||||
|
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
|
||||||
|
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
|
||||||
|
"RESOLVE_SUCCESFUL": "Conversations resolved successfully",
|
||||||
|
"RESOLVE_FAILED": "Failed to resolve conversations, please try again",
|
||||||
|
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
|
||||||
|
"AGENT_LIST_LOADING": "Loading Agents"
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,33 +12,11 @@
|
||||||
"INPUT": "Търсене на хора, чатове, запазени отговори .."
|
"INPUT": "Търсене на хора, чатове, запазени отговори .."
|
||||||
},
|
},
|
||||||
"FILTER_ALL": "Всички",
|
"FILTER_ALL": "Всички",
|
||||||
"STATUS_TABS": [
|
"ASSIGNEE_TYPE_TABS": {
|
||||||
{
|
"me": "Мой",
|
||||||
"NAME": "Отворен",
|
"unassigned": "Неназначен",
|
||||||
"KEY": "openCount"
|
"all": "Всички"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"NAME": "Разрешен",
|
|
||||||
"KEY": "allConvCount"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"ASSIGNEE_TYPE_TABS": [
|
|
||||||
{
|
|
||||||
"NAME": "Мой",
|
|
||||||
"KEY": "me",
|
|
||||||
"COUNT_KEY": "mineCount"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NAME": "Неназначен",
|
|
||||||
"KEY": "unassigned",
|
|
||||||
"COUNT_KEY": "unAssignedCount"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NAME": "Всички",
|
|
||||||
"KEY": "all",
|
|
||||||
"COUNT_KEY": "allCount"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"CHAT_STATUS_FILTER_ITEMS": {
|
"CHAT_STATUS_FILTER_ITEMS": {
|
||||||
"open": {
|
"open": {
|
||||||
"TEXT": "Отворен"
|
"TEXT": "Отворен"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"CONVERSATION": {
|
"CONVERSATION": {
|
||||||
"404": "Please select a conversation from left pane",
|
"404": "Please select a conversation from left pane",
|
||||||
|
"DASHBOARD_APP_TAB_MESSAGES": "Messages",
|
||||||
"UNVERIFIED_SESSION": "The identity of this user is not verified",
|
"UNVERIFIED_SESSION": "The identity of this user is not verified",
|
||||||
"NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.",
|
"NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.",
|
||||||
"NO_MESSAGE_2": " to send a message to your page!",
|
"NO_MESSAGE_2": " to send a message to your page!",
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
"REPLYING_TO": "You are replying to:",
|
"REPLYING_TO": "You are replying to:",
|
||||||
"REMOVE_SELECTION": "Remove Selection",
|
"REMOVE_SELECTION": "Remove Selection",
|
||||||
"DOWNLOAD": "Download",
|
"DOWNLOAD": "Download",
|
||||||
|
"UNKNOWN_FILE_TYPE": "Unknown File",
|
||||||
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
|
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
|
||||||
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
|
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
|
||||||
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
|
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { default as _advancedFilters } from './advancedFilters.json';
|
||||||
import { default as _agentMgmt } from './agentMgmt.json';
|
import { default as _agentMgmt } from './agentMgmt.json';
|
||||||
import { default as _attributesMgmt } from './attributesMgmt.json';
|
import { default as _attributesMgmt } from './attributesMgmt.json';
|
||||||
import { default as _automation } from './automation.json';
|
import { default as _automation } from './automation.json';
|
||||||
|
import { default as _bulkActions } from './bulkActions.json';
|
||||||
import { default as _campaign } from './campaign.json';
|
import { default as _campaign } from './campaign.json';
|
||||||
import { default as _cannedMgmt } from './cannedMgmt.json';
|
import { default as _cannedMgmt } from './cannedMgmt.json';
|
||||||
import { default as _chatlist } from './chatlist.json';
|
import { default as _chatlist } from './chatlist.json';
|
||||||
|
@ -21,6 +22,7 @@ import { default as _setNewPassword } from './setNewPassword.json';
|
||||||
import { default as _settings } from './settings.json';
|
import { default as _settings } from './settings.json';
|
||||||
import { default as _signup } from './signup.json';
|
import { default as _signup } from './signup.json';
|
||||||
import { default as _teamsSettings } from './teamsSettings.json';
|
import { default as _teamsSettings } from './teamsSettings.json';
|
||||||
|
import { default as _whatsappTemplates } from './whatsappTemplates.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
..._advancedFilters,
|
..._advancedFilters,
|
||||||
|
@ -46,4 +48,6 @@ export default {
|
||||||
..._settings,
|
..._settings,
|
||||||
..._signup,
|
..._signup,
|
||||||
..._teamsSettings,
|
..._teamsSettings,
|
||||||
|
..._whatsappTemplates,
|
||||||
|
..._bulkActions,
|
||||||
};
|
};
|
||||||
|
|
|
@ -386,6 +386,7 @@
|
||||||
"CSAT_REPORTS": {
|
"CSAT_REPORTS": {
|
||||||
"HEADER": "CSAT Reports",
|
"HEADER": "CSAT Reports",
|
||||||
"NO_RECORDS": "There are no CSAT survey responses available.",
|
"NO_RECORDS": "There are no CSAT survey responses available.",
|
||||||
|
"DOWNLOAD": "Download CSAT Reports",
|
||||||
"FILTERS": {
|
"FILTERS": {
|
||||||
"AGENTS": {
|
"AGENTS": {
|
||||||
"PLACEHOLDER": "Choose Agents"
|
"PLACEHOLDER": "Choose Agents"
|
||||||
|
|
|
@ -189,7 +189,8 @@
|
||||||
"REPORTS_TEAM": "Team",
|
"REPORTS_TEAM": "Team",
|
||||||
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
"SET_AVAILABILITY_TITLE": "Set yourself as",
|
||||||
"BETA": "Beta",
|
"BETA": "Beta",
|
||||||
"REPORTS_OVERVIEW": "Overview"
|
"REPORTS_OVERVIEW": "Overview",
|
||||||
|
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services"
|
||||||
},
|
},
|
||||||
"CREATE_ACCOUNT": {
|
"CREATE_ACCOUNT": {
|
||||||
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"WHATSAPP_TEMPLATES": {
|
||||||
|
"MODAL": {
|
||||||
|
"TITLE": "Whatsapp Templates",
|
||||||
|
"SUBTITLE": "Select the whatsapp template you want to send",
|
||||||
|
"TEMPLATE_SELECTED_SUBTITLE": "Process %{templateName}"
|
||||||
|
},
|
||||||
|
"PICKER": {
|
||||||
|
"SEARCH_PLACEHOLDER": "Search Templates",
|
||||||
|
"NO_TEMPLATES_FOUND": "No templates found for",
|
||||||
|
"LABELS": {
|
||||||
|
"LANGUAGE": "Language",
|
||||||
|
"TEMPLATE_BODY": "Template Body",
|
||||||
|
"CATEGORY": "Category"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"PARSER": {
|
||||||
|
"VARIABLES_LABEL": "Variables",
|
||||||
|
"VARIABLE_PLACEHOLDER": "Enter %{variable} value",
|
||||||
|
"GO_BACK_LABEL": "Go Back",
|
||||||
|
"SEND_MESSAGE_LABEL": "Send Message",
|
||||||
|
"FORM_ERROR_MESSAGE": "Please fill all variables before sending"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
app/javascript/dashboard/i18n/locale/ca/bulkActions.json
Normal file
17
app/javascript/dashboard/i18n/locale/ca/bulkActions.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"BULK_ACTION": {
|
||||||
|
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
|
||||||
|
"AGENT_SELECT_LABEL": "Seleccionar Agent",
|
||||||
|
"ASSIGN_CONFIRMATION_LABEL": "Are you sure you want to assign %{conversationCount} %{conversationLabel} to",
|
||||||
|
"GO_BACK_LABEL": "Go back",
|
||||||
|
"ASSIGN_LABEL": "Assignar",
|
||||||
|
"ASSIGN_AGENT_TOOLTIP": "Assign Agent",
|
||||||
|
"RESOLVE_TOOLTIP": "Resoldre",
|
||||||
|
"ASSIGN_SUCCESFUL": "Conversations assigned successfully",
|
||||||
|
"ASSIGN_FAILED": "Failed to assign conversations, please try again",
|
||||||
|
"RESOLVE_SUCCESFUL": "Conversations resolved successfully",
|
||||||
|
"RESOLVE_FAILED": "Failed to resolve conversations, please try again",
|
||||||
|
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
|
||||||
|
"AGENT_LIST_LOADING": "Loading Agents"
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,33 +12,11 @@
|
||||||
"INPUT": "Cerca persones, xats, respostes desades .."
|
"INPUT": "Cerca persones, xats, respostes desades .."
|
||||||
},
|
},
|
||||||
"FILTER_ALL": "Totes",
|
"FILTER_ALL": "Totes",
|
||||||
"STATUS_TABS": [
|
"ASSIGNEE_TYPE_TABS": {
|
||||||
{
|
"me": "Meves",
|
||||||
"NAME": "Obrir",
|
"unassigned": "Sense assignar",
|
||||||
"KEY": "openCount"
|
"all": "Totes"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"NAME": "Resoltes",
|
|
||||||
"KEY": "allConvCount"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"ASSIGNEE_TYPE_TABS": [
|
|
||||||
{
|
|
||||||
"NAME": "Meves",
|
|
||||||
"KEY": "me",
|
|
||||||
"COUNT_KEY": "mineCount"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NAME": "Sense assignar",
|
|
||||||
"KEY": "unassigned",
|
|
||||||
"COUNT_KEY": "unAssignedCount"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"NAME": "Totes",
|
|
||||||
"KEY": "all",
|
|
||||||
"COUNT_KEY": "allCount"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"CHAT_STATUS_FILTER_ITEMS": {
|
"CHAT_STATUS_FILTER_ITEMS": {
|
||||||
"open": {
|
"open": {
|
||||||
"TEXT": "Obrir"
|
"TEXT": "Obrir"
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"CONVERSATION": {
|
"CONVERSATION": {
|
||||||
"404": "Si us plau, selecciona una conversa al panell de l’esquerra",
|
"404": "Si us plau, selecciona una conversa al panell de l’esquerra",
|
||||||
|
"DASHBOARD_APP_TAB_MESSAGES": "Messages",
|
||||||
"UNVERIFIED_SESSION": "The identity of this user is not verified",
|
"UNVERIFIED_SESSION": "The identity of this user is not verified",
|
||||||
"NO_MESSAGE_1": "Uh oh! Sembla que no hi ha missatges de clients a la safata d'entrada.",
|
"NO_MESSAGE_1": "Uh oh! Sembla que no hi ha missatges de clients a la safata d'entrada.",
|
||||||
"NO_MESSAGE_2": " per enviar un missatge a la vostra pàgina!",
|
"NO_MESSAGE_2": " per enviar un missatge a la vostra pàgina!",
|
||||||
|
@ -30,6 +31,7 @@
|
||||||
"REPLYING_TO": "Estas responent a:",
|
"REPLYING_TO": "Estas responent a:",
|
||||||
"REMOVE_SELECTION": "Elimina la selecció",
|
"REMOVE_SELECTION": "Elimina la selecció",
|
||||||
"DOWNLOAD": "Descarrega",
|
"DOWNLOAD": "Descarrega",
|
||||||
|
"UNKNOWN_FILE_TYPE": "Unknown File",
|
||||||
"UPLOADING_ATTACHMENTS": "Pujant fitxers adjunts...",
|
"UPLOADING_ATTACHMENTS": "Pujant fitxers adjunts...",
|
||||||
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
|
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
|
||||||
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
|
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue