Compare commits

..
Author SHA1 Message Date
Vishnu Narayanan cd016c5ad4 chore: log webhook response data on error 2023-11-24 14:18:13 +05:30
268 changed files with 1989 additions and 4713 deletions
+8 -8
View File
@@ -75,8 +75,8 @@ gem 'jwt'
gem 'pundit'
# super admin
gem 'administrate', '>= 0.19.0'
gem 'administrate-field-active_storage', '>= 1.0.1'
gem 'administrate-field-belongs_to_search', '>= 0.9.0'
gem 'administrate-field-active_storage'
gem 'administrate-field-belongs_to_search'
##--- gems for pubsub service ---##
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
@@ -109,14 +109,14 @@ gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.14.0', require: false
gem 'sentry-rails', '>= 5.13.0', require: false
gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.14.0', require: false
gem 'sentry-sidekiq', '>= 5.13.0', require: false
##-- background job processing --##
gem 'sidekiq', '>= 7.1.3'
# We want cron jobs
gem 'sidekiq-cron', '>= 1.12.0'
gem 'sidekiq-cron', '>= 1.11.0'
##-- Push notification service --##
gem 'fcm'
@@ -162,7 +162,7 @@ gem 'omniauth-oauth2'
gem 'audited', '~> 5.4', '>= 5.4.1'
# need for google auth
gem 'omniauth', '>= 2.1.2'
gem 'omniauth'
gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
@@ -198,7 +198,7 @@ group :development do
gem 'squasher'
# profiling
gem 'rack-mini-profiler', '>= 3.2.0', require: false
gem 'rack-mini-profiler', '>= 3.1.1', require: false
gem 'stackprof'
# Should install the associated chrome extension to view query logs
gem 'meta_request'
@@ -224,7 +224,7 @@ group :development, :test do
gem 'byebug', platform: :mri
gem 'climate_control'
gem 'debug', '~> 1.8'
gem 'factory_bot_rails', '>= 6.4.3'
gem 'factory_bot_rails'
gem 'listen'
gem 'mock_redis'
gem 'pry-rails'
+40 -40
View File
@@ -113,13 +113,13 @@ GEM
kaminari (>= 1.0)
sassc-rails (~> 2.1)
selectize-rails (~> 0.6)
administrate-field-active_storage (1.0.1)
administrate-field-active_storage (0.4.2)
administrate (>= 0.2.2)
rails (>= 7.0)
administrate-field-belongs_to_search (0.9.0)
administrate-field-belongs_to_search (0.8.0)
administrate (>= 0.3, < 1.0)
jbuilder (~> 2)
rails (>= 4.2, < 7.2)
rails (>= 4.2, < 7.1)
selectize-rails (~> 0.6)
annotate (3.2.0)
activerecord (>= 3.2, < 8.0)
@@ -183,7 +183,7 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
date (3.3.4)
date (3.3.3)
ddtrace (1.11.1)
debase-ruby_core_source (>= 0.10.16, <= 3.2.0)
libdatadog (~> 2.0.0.1.0)
@@ -230,10 +230,10 @@ GEM
facebook-messenger (2.0.1)
httparty (~> 0.13, >= 0.13.7)
rack (>= 1.4.5)
factory_bot (6.4.5)
factory_bot (6.2.1)
activesupport (>= 5.0.0)
factory_bot_rails (6.4.3)
factory_bot (~> 6.4)
factory_bot_rails (6.2.0)
factory_bot (~> 6.2.0)
railties (>= 5.0.0)
faker (3.2.0)
i18n (>= 1.8.11, < 2)
@@ -256,7 +256,7 @@ GEM
fcm (1.0.8)
faraday (>= 1.0.0, < 3.0)
googleauth (~> 1)
ffi (1.16.3)
ffi (1.15.5)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
rake
@@ -440,7 +440,7 @@ GEM
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.22.0)
loofah (2.21.4)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@@ -472,14 +472,14 @@ GEM
activerecord (>= 5.2)
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
net-imap (0.4.9)
net-imap (0.3.7)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
net-protocol (0.2.1)
timeout
net-smtp (0.4.0)
net-smtp (0.3.3)
net-protocol
netrc (0.11.0)
newrelic-sidekiq-metrics (1.6.2)
@@ -487,15 +487,15 @@ GEM
sidekiq
newrelic_rpm (9.6.0)
base64
nio4r (2.7.0)
nokogiri (1.16.0)
nio4r (2.5.9)
nokogiri (1.15.4)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.16.0-arm64-darwin)
nokogiri (1.15.4-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.0-x86_64-darwin)
nokogiri (1.15.4-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.16.0-x86_64-linux)
nokogiri (1.15.4-x86_64-linux)
racc (~> 1.4)
numo-narray (0.9.2.1)
oauth (1.1.0)
@@ -511,7 +511,7 @@ GEM
rack (>= 1.2, < 4)
snaky_hash (~> 2.0)
version_gem (~> 1.1)
omniauth (2.1.2)
omniauth (2.1.1)
hashie (>= 3.4.6)
rack (>= 2.2.3)
rack-protection
@@ -553,7 +553,7 @@ GEM
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (5.0.1)
puma (6.4.2)
puma (6.3.1)
nio4r (~> 2.0)
pundit (2.3.0)
activesupport (>= 3.0.0)
@@ -566,10 +566,10 @@ GEM
rack (< 4)
rack-cors (2.0.1)
rack (>= 2.0.0)
rack-mini-profiler (3.2.0)
rack-mini-profiler (3.1.1)
rack (>= 1.2.0)
rack-protection (3.1.0)
rack (~> 2.2, >= 2.2.4)
rack-protection (3.0.6)
rack
rack-proxy (0.7.6)
rack
rack-test (2.1.0)
@@ -610,7 +610,7 @@ GEM
ffi (~> 1.0)
redis (5.0.6)
redis-client (>= 0.9.0)
redis-client (0.19.0)
redis-client (0.18.0)
connection_pool
redis-namespace (1.10.0)
redis (>= 4)
@@ -709,13 +709,13 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.14.0)
sentry-rails (5.13.0)
railties (>= 5.0)
sentry-ruby (~> 5.14.0)
sentry-ruby (5.14.0)
sentry-ruby (~> 5.13.0)
sentry-ruby (5.13.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.14.0)
sentry-ruby (~> 5.14.0)
sentry-sidekiq (5.13.0)
sentry-ruby (~> 5.13.0)
sidekiq (>= 3.0)
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
@@ -725,7 +725,7 @@ GEM
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.14.0)
sidekiq-cron (1.12.0)
sidekiq-cron (1.11.0)
fugit (~> 1.8)
globalid (>= 1.0.1)
sidekiq (>= 6)
@@ -752,7 +752,7 @@ GEM
spring-watcher-listen (2.1.0)
listen (>= 2.7, < 4.0)
spring (>= 4)
sprockets (4.2.1)
sprockets (4.2.0)
concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4)
sprockets-rails (3.4.2)
@@ -766,11 +766,11 @@ GEM
telephone_number (1.4.20)
test-prof (1.2.1)
thor (1.3.0)
tilt (2.3.0)
tilt (2.2.0)
time_diff (0.3.0)
activesupport
i18n
timeout (0.4.1)
timeout (0.4.0)
trailblazer-option (0.1.2)
twilio-ruby (5.77.0)
faraday (>= 0.9, < 3.0)
@@ -841,8 +841,8 @@ DEPENDENCIES
activerecord-import
acts-as-taggable-on
administrate (>= 0.19.0)
administrate-field-active_storage (>= 1.0.1)
administrate-field-belongs_to_search (>= 0.9.0)
administrate-field-active_storage
administrate-field-belongs_to_search
annotate
attr_extras
audited (~> 5.4, >= 5.4.1)
@@ -870,7 +870,7 @@ DEPENDENCIES
elastic-apm
email_reply_trimmer
facebook-messenger
factory_bot_rails (>= 6.4.3)
factory_bot_rails
faker
fcm
flag_shih_tzu
@@ -905,7 +905,7 @@ DEPENDENCIES
neighbor
newrelic-sidekiq-metrics (>= 1.6.2)
newrelic_rpm
omniauth (>= 2.1.2)
omniauth
omniauth-google-oauth2
omniauth-oauth2
omniauth-rails_csrf_protection (~> 1.0)
@@ -918,7 +918,7 @@ DEPENDENCIES
pundit
rack-attack (>= 6.7.0)
rack-cors
rack-mini-profiler (>= 3.2.0)
rack-mini-profiler (>= 3.1.1)
rack-timeout
rails (~> 7.0.8.0)
redis
@@ -935,12 +935,12 @@ DEPENDENCIES
scout_apm
scss_lint
seed_dump
sentry-rails (>= 5.14.0)
sentry-rails (>= 5.13.0)
sentry-ruby
sentry-sidekiq (>= 5.14.0)
sentry-sidekiq (>= 5.13.0)
shoulda-matchers
sidekiq (>= 7.1.3)
sidekiq-cron (>= 1.12.0)
sidekiq-cron (>= 1.11.0)
simplecov (= 0.17.1)
slack-ruby-client (~> 2.2.0)
spring
+1 -1
View File
@@ -118,4 +118,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
*Chatwoot* &copy; 2017-2024, Chatwoot Inc - Released under the MIT License.
*Chatwoot* &copy; 2017-2023, Chatwoot Inc - Released under the MIT License.
+1 -1
View File
@@ -1 +1 @@
3.3.1
3.1.0
+1 -1
View File
@@ -1 +1 @@
2.7.0
2.6.0
@@ -25,6 +25,7 @@
@import 'components/flashes';
@import 'components/form-actions';
@import 'components/main-content';
@import 'components/navigation';
@import 'components/pagination';
@import 'components/search';
@import 'components/reports';
@@ -1,7 +1,7 @@
html {
background-color: $color-white;
box-sizing: border-box;
font-size: 16px;
font-size: 10px;
-webkit-font-smoothing: antialiased;
}
@@ -16,8 +16,8 @@
.attribute-data {
float: left;
margin-bottom: $base-spacing;
margin-left: 1.25rem;
width: calc(84% - 0.625rem);
margin-left: 2rem;
width: calc(84% - 1rem);
}
.attribute--nested {
@@ -9,22 +9,22 @@
.field-unit__label {
float: left;
margin-left: 0.625rem;
margin-left: 1rem;
text-align: right;
width: calc(15% - 0.625rem);
width: calc(15% - 1rem);
}
.field-unit__field {
float: left;
margin-left: 1.25rem;
max-width: 31.15rem;
margin-left: 2rem;
max-width: 50rem;
width: 100%;
}
.field-unit--nested {
border: $base-border;
margin-left: 7.5%;
max-width: 37.5rem;
max-width: 60rem;
padding: $small-spacing;
width: 100%;
@@ -1,3 +1,3 @@
.form-actions {
margin-left: calc(15% + 1.25rem);
margin-left: calc(15% + 2rem);
}
@@ -13,10 +13,6 @@
table {
font-size: $font-size-small;
}
form {
margin-top: $space-two;
}
}
.main-content__header {
@@ -24,7 +20,7 @@
background-color: $color-white;
border-bottom: 1px solid $color-border;
display: flex;
min-height: 3.5rem;
min-height: 5.6rem;
padding: $space-small $space-normal;
}
@@ -0,0 +1,88 @@
.logo-brand {
margin-bottom: $space-normal;
padding: $space-normal $space-smaller $space-small;
text-align: left;
img {
margin-bottom: $space-smaller;
max-height: 3rem;
}
}
.navigation {
background: $white;
border-right: 1px solid $color-border;
display: flex;
flex-direction: column;
font-size: $font-size-default;
font-weight: $font-weight-medium;
height: 100%;
justify-content: flex-start;
left: 0;
margin: 0;
overflow: auto;
padding: $space-normal;
position: fixed;
top: 0;
width: 21rem;
z-index: 1023;
li {
align-items: center;
display: flex;
font-size: $font-size-small;
a {
color: $color-gray;
text-decoration: none;
}
i {
min-width: $space-medium;
}
}
hr {
margin: $space-slab;
}
}
.navigation__link {
background-color: transparent;
color: $color-gray;
display: block;
line-height: 1;
margin-bottom: $space-smaller;
padding: $space-small;
&:hover {
color: $blue;
a {
color: $blue;
}
}
&.navigation__link--active {
background-color: $color-background;
border-radius: $base-border-radius;
color: $blue;
a {
color: $blue;
}
}
}
.logout {
bottom: $space-normal;
left: $space-normal;
position: fixed;
}
.app-version {
color: $color-gray;
font-size: $font-size-small;
padding-top: $space-smaller;
}
@@ -1,7 +1,7 @@
.search {
margin-left: auto;
margin-right: 1.25rem;
max-width: 27.5rem;
margin-right: 2rem;
max-width: 44rem;
position: relative;
width: 100%;
}
@@ -1,10 +1,10 @@
// Typography
$base-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
$base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif !default;
$heading-font-family: $base-font-family !default;
$base-font-size: 16px !default;
$base-font-size: 14px !default;
$base-line-height: 1.5 !default;
$heading-line-height: 1.2 !default;
@@ -1,30 +1,30 @@
// Font sizes
$font-size-nano: 0.5rem;
$font-size-micro: 0.675rem;
$font-size-mini: 0.75rem;
$font-size-small: 0.875rem;
$font-size-default: 1rem;
$font-size-medium: 1.125rem;
$font-size-large: 1.375rem;
$font-size-big: 1.5rem;
$font-size-bigger: 1.75rem;
$font-size-mega: 2.125rem;
$font-size-giga: 2.5rem;
$font-size-nano: 0.8rem;
$font-size-micro: 1.0rem;
$font-size-mini: 1.2rem;
$font-size-small: 1.4rem;
$font-size-default: 1.6rem;
$font-size-medium: 1.8rem;
$font-size-large: 2.2rem;
$font-size-big: 2.4rem;
$font-size-bigger: 3.0rem;
$font-size-mega: 3.4rem;
$font-size-giga: 4.0rem;
// spaces
$zero: 0;
$space-micro: 0.125rem;
$space-smaller: 0.25rem;
$space-small: 0.5rem;
$space-one: 0.675rem;
$space-slab: 0.75rem;
$space-normal: 1rem;
$space-two: 1.25rem;
$space-medium: 1.5rem;
$space-large: 2rem;
$space-larger: 3rem;
$space-jumbo: 4rem;
$space-mega: 6.25rem;
$space-micro: 0.2rem;
$space-smaller: 0.4rem;
$space-small: 0.8rem;
$space-one: 1rem;
$space-slab: 1.2rem;
$space-normal: 1.6rem;
$space-two: 2.0rem;
$space-medium: 2.4rem;
$space-large: 3.2rem;
$space-larger: 4.8rem;
$space-jumbo: 6.4rem;
$space-mega: 10.0rem;
// font-weight
$font-weight-feather: 100;
@@ -25,9 +25,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
build_contact_inbox
build_message
end
rescue Koala::Facebook::AuthenticationError => e
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
Rails.logger.error e
rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error!
rescue StandardError => e
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
@@ -110,15 +108,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
}
end
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def contact_params
begin
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
result = k.get_object(@sender_id) || {}
rescue Koala::Facebook::AuthenticationError => e
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
Rails.logger.error e
rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error!
raise
rescue Koala::Facebook::ClientError => e
@@ -136,6 +130,4 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end
process_contact_params_result(result)
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
end
@@ -20,9 +20,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
ActiveRecord::Base.transaction do
build_message
end
rescue Koala::Facebook::AuthenticationError => e
Rails.logger.warn("Instagram authentication error for inbox: #{@inbox.id} with error: #{e.message}")
Rails.logger.error e
rescue Koala::Facebook::AuthenticationError
@inbox.channel.authorization_error!
raise
rescue StandardError => e
@@ -48,10 +46,6 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
@outgoing_echo ? recipient_id : sender_id
end
def message_is_unsupported?
message[:is_unsupported].present? && @messaging[:message][:is_unsupported] == true
end
def sender_id
@messaging[:sender][:id]
end
@@ -122,7 +116,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
end
def message_params
params = {
{
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: message_type,
@@ -133,9 +127,6 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
in_reply_to_external_id: message_reply_attributes
}
}
params[:content_attributes][:is_unsupported] = true if message_is_unsupported?
params
end
def already_sent_from_chatwoot?
+1 -2
View File
@@ -149,8 +149,7 @@ class Messages::MessageBuilder
content_type: @params[:content_type],
items: @items,
in_reply_to: @in_reply_to,
echo_id: @params[:echo_id],
source_id: @params[:source_id]
echo_id: @params[:echo_id]
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
end
end
+3 -4
View File
@@ -1,5 +1,5 @@
class NotificationBuilder
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!, :secondary_actor]
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!]
def perform
return unless user_subscribed_to_notification?
@@ -9,7 +9,7 @@ class NotificationBuilder
private
def current_user
def secondary_actor
Current.user
end
@@ -29,8 +29,7 @@ class NotificationBuilder
notification_type: notification_type,
account: account,
primary_actor: primary_actor,
# secondary_actor is secondary_actor if present, else current_user
secondary_actor: secondary_actor || current_user
secondary_actor: secondary_actor
)
end
end
@@ -10,18 +10,11 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
def show; end
def create
@agent_bot = Current.account.agent_bots.create!(permitted_params.except(:avatar_url))
process_avatar_from_url
@agent_bot = Current.account.agent_bots.create!(permitted_params)
end
def update
@agent_bot.update!(permitted_params.except(:avatar_url))
process_avatar_from_url
end
def avatar
@agent_bot.avatar.purge if @agent_bot.avatar.attached?
@agent_bot
@agent_bot.update!(permitted_params)
end
def destroy
@@ -37,10 +30,6 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
end
def permitted_params
params.permit(:name, :description, :outgoing_url, :avatar, :avatar_url, :bot_type, bot_config: [:csml_content])
end
def process_avatar_from_url
::Avatar::AvatarFromUrlJob.perform_later(@agent_bot, params[:avatar_url]) if params[:avatar_url].present?
params.permit(:name, :description, :outgoing_url, :bot_type, bot_config: [:csml_content])
end
end
@@ -83,14 +83,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@contact = Current.account.contacts.new(permitted_params.except(:avatar_url))
@contact.save!
@contact_inbox = build_contact_inbox
process_avatar_from_url
process_avatar
end
end
def update
@contact.assign_attributes(contact_update_params)
@contact.save!
process_avatar_from_url
process_avatar if permitted_params[:avatar].present? || permitted_params[:avatar_url].present?
end
def destroy
@@ -174,7 +174,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
end
def process_avatar_from_url
def process_avatar
::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end
@@ -14,8 +14,7 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account
def set_agent
@agent = Current.account.users.find_by(id: params[:assignee_id])
@conversation.assignee = @agent
@conversation.save!
@conversation.update_assignee(@agent)
render_agent
end
@@ -18,15 +18,6 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
end
end
def retry
return if message.blank?
message.update!(status: :sent, content_attributes: {})
::SendReplyJob.perform_later(message.id)
rescue StandardError => e
render_could_not_create_error(e.message)
end
def translate
return head :ok if already_translated_content_available?
@@ -60,26 +60,13 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
def toggle_status
# FIXME: move this logic into a service object
if pending_to_open_by_bot?
@conversation.bot_handoff!
elsif params[:status].present?
if params[:status].present?
set_conversation_status
@status = @conversation.save!
else
@status = @conversation.toggle_status
end
assign_conversation if should_assign_conversation?
end
def pending_to_open_by_bot?
return false unless Current.user.is_a?(AgentBot)
@conversation.status == 'pending' && params[:status] == 'open'
end
def should_assign_conversation?
@conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
end
def toggle_priority
@@ -123,8 +110,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
def assign_conversation
@conversation.assignee = current_user
@conversation.save!
@agent = Current.account.users.find(current_user.id)
@conversation.update_assignee(@agent)
end
def conversation
@@ -1,8 +1,7 @@
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
RESULTS_PER_PAGE = 15
include DateRangeHelper
before_action :fetch_notification, only: [:update, :destroy, :snooze]
before_action :fetch_notification, only: [:update]
before_action :set_primary_actor, only: [:read_all]
before_action :set_current_page, only: [:index]
@@ -29,21 +28,11 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
render json: @notification
end
def destroy
@notification.destroy
head :ok
end
def unread_count
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
render json: @unread_count
end
def snooze
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s)) if params[:snoozed_until]
render json: @notification
end
private
def set_primary_actor
@@ -14,7 +14,7 @@ module AccessTokenAuthHelper
render_unauthorized('Invalid Access Token') && return if @access_token.blank?
@resource = @access_token.owner
Current.user = @resource if [User, AgentBot].include?(@resource.class)
Current.user = @resource if current_user.is_a?(User)
end
def validate_bot_access_token!
@@ -9,15 +9,13 @@ class Platform::Api::V1::AgentBotsController < PlatformController
def show; end
def create
@resource = AgentBot.new(agent_bot_params.except(:avatar_url))
@resource = AgentBot.new(agent_bot_params)
@resource.save!
process_avatar_from_url
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
end
def update
@resource.update!(agent_bot_params.except(:avatar_url))
process_avatar_from_url
@resource.update!(agent_bot_params)
end
def destroy
@@ -25,11 +23,6 @@ class Platform::Api::V1::AgentBotsController < PlatformController
head :ok
end
def avatar
@resource.avatar.purge if @resource.avatar.attached?
@resource
end
private
def set_resource
@@ -37,10 +30,6 @@ class Platform::Api::V1::AgentBotsController < PlatformController
end
def agent_bot_params
params.permit(:name, :description, :account_id, :outgoing_url, :avatar, :avatar_url)
end
def process_avatar_from_url
::Avatar::AvatarFromUrlJob.perform_later(@resource, params[:avatar_url]) if params[:avatar_url].present?
params.permit(:name, :description, :account_id, :outgoing_url)
end
end
@@ -11,7 +11,11 @@ class Public::Api::V1::Portals::BaseController < PublicController
end
def set_color_scheme
@theme_from_params = params[:theme] if %w[dark light].include?(params[:theme])
@theme = if %w[dark light].include?(params[:theme])
params[:theme]
else
'system'
end
end
def portal
@@ -1,41 +1,21 @@
class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
before_action :set_config
before_action :allowed_configs
def show
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
# ref: https://github.com/rubocop/rubocop/issues/7767
# rubocop:disable Style/HashTransformValues
@app_config = InstallationConfig.where(name: @allowed_configs)
.pluck(:name, :serialized_value)
.map { |name, serialized_value| [name, serialized_value['value']] }
.to_h
@fb_config = InstallationConfig.where(name: @allowed_configs)
.pluck(:name, :serialized_value)
.map { |name, serialized_value| [name, serialized_value['value']] }
.to_h
# rubocop:enable Style/HashTransformValues
@installation_configs = ConfigLoader.new.general_configs.each_with_object({}) do |config_hash, result|
result[config_hash['name']] = config_hash.except('name')
end
end
def create
params['app_config'].each do |key, value|
next unless @allowed_configs.include?(key)
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
i.value = value
i.save!
end
# rubocop:disable Rails/I18nLocaleTexts
redirect_to super_admin_settings_path, notice: 'App Configs updated successfully'
# rubocop:enable Rails/I18nLocaleTexts
end
private
def set_config
@config = params[:config]
end
def allowed_configs
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
redirect_to super_admin_app_config_url
end
end
SuperAdmin::AppConfigsController.prepend_mod_with('SuperAdmin::AppConfigsController')
@@ -20,13 +20,4 @@ class SuperAdmin::ApplicationController < Administrate::ApplicationController
params.fetch(resource_name, {}).fetch(:direction, 'desc')
)
end
private
def invalid_action_perfomed
# rubocop:disable Rails/I18nLocaleTexts
flash[:error] = 'Invalid action performed'
# rubocop:enable Rails/I18nLocaleTexts
redirect_back(fallback_location: root_path)
end
end
@@ -27,8 +27,7 @@ class SuperAdmin::Devise::SessionsController < Devise::SessionsController
true
rescue StandardError => e
Rails.logger.error e.message
@error_message = 'Invalid credentials. Please try again.'
@error_message = e.message
false
end
end
@@ -1,5 +1,4 @@
class SuperAdmin::InstallationConfigsController < SuperAdmin::ApplicationController
rescue_from ActiveRecord::RecordNotUnique, :with => :invalid_action_perfomed
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
@@ -1,10 +0,0 @@
class SuperAdmin::SettingsController < SuperAdmin::ApplicationController
def show; end
def refresh
Internal::CheckNewVersionsJob.perform_now
# rubocop:disable Rails/I18nLocaleTexts
redirect_to super_admin_settings_path, notice: 'Instance status refreshed'
# rubocop:enable Rails/I18nLocaleTexts
end
end
+4
View File
@@ -30,7 +30,11 @@ class AccessTokenDashboard < Administrate::BaseDashboard
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
owner
id
token
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
-1
View File
@@ -46,7 +46,6 @@ class AgentBotDashboard < Administrate::BaseDashboard
name
description
outgoing_url
access_token
].freeze
# FORM_ATTRIBUTES
-1
View File
@@ -32,7 +32,6 @@ class PlatformAppDashboard < Administrate::BaseDashboard
name
created_at
updated_at
access_token
].freeze
# FORM_ATTRIBUTES
+1 -3
View File
@@ -36,8 +36,7 @@ class UserDashboard < Administrate::BaseDashboard
updated_at: Field::DateTime,
pubsub_token: Field::String,
type: Field::Select.with_options(collection: [nil, 'SuperAdmin']),
accounts: CountField,
access_token: Field::HasOne
accounts: CountField
}.freeze
# COLLECTION_ATTRIBUTES
@@ -68,7 +67,6 @@ class UserDashboard < Administrate::BaseDashboard
updated_at
confirmed_at
account_users
access_token
].freeze
# FORM_ATTRIBUTES
-5
View File
@@ -18,9 +18,4 @@ class ContactDrop < BaseDrop
def last_name
@obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1
end
def custom_attribute
custom_attributes = @obj.try(:custom_attributes) || {}
custom_attributes.transform_keys(&:to_s)
end
end
-5
View File
@@ -19,11 +19,6 @@ class ConversationDrop < BaseDrop
end
end
def custom_attribute
custom_attributes = @obj.try(:custom_attributes) || {}
custom_attributes.transform_keys(&:to_s)
end
private
def message_sender_name(sender)
+8 -17
View File
@@ -3,21 +3,13 @@ class ConversationFinder
DEFAULT_STATUS = 'open'.freeze
SORT_OPTIONS = {
'last_activity_at_asc' => %w[sort_on_last_activity_at asc],
'last_activity_at_desc' => %w[sort_on_last_activity_at desc],
'created_at_asc' => %w[sort_on_created_at asc],
'created_at_desc' => %w[sort_on_created_at desc],
'priority_asc' => %w[sort_on_priority asc],
'priority_desc' => %w[sort_on_priority desc],
'waiting_since_asc' => %w[sort_on_waiting_since asc],
'waiting_since_desc' => %w[sort_on_waiting_since desc],
# To be removed in v3.5.0
'latest' => %w[sort_on_last_activity_at desc],
'sort_on_created_at' => %w[sort_on_created_at asc],
'sort_on_priority' => %w[sort_on_priority desc],
'sort_on_waiting_since' => %w[sort_on_waiting_since asc]
latest: 'latest',
sort_on_created_at: 'sort_on_created_at',
last_user_message_at: 'last_user_message_at',
sort_on_priority: 'sort_on_priority',
sort_on_waiting_since: 'sort_on_waiting_since'
}.with_indifferent_access
# assumptions
# inbox_id if not given, take from all conversations, else specific to inbox
# assignee_type if not given, take 'all'
@@ -167,8 +159,7 @@ class ConversationFinder
@conversations = @conversations.includes(
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
)
sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc']
@conversations.send(sort_by, sort_order).page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
sort_by = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['latest']
@conversations.send(sort_by).page(current_page)
end
end
+19 -42
View File
@@ -6,17 +6,7 @@ module PortalHelper
def generate_portal_bg(portal_color, theme)
bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
"url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
end
def generate_gradient_to_bottom(theme)
base_color = theme == 'dark' ? '#151718' : 'white'
"linear-gradient(to bottom, transparent, #{base_color})"
end
def generate_portal_hover_color(portal_color, theme)
base_color = theme == 'dark' ? '#1B1B1B' : '#F9F9F9'
"color-mix(in srgb, #{portal_color} 5%, #{base_color})"
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
end
def language_name(locale)
@@ -24,48 +14,35 @@ module PortalHelper
language_map[locale] || locale
end
def theme_query_string(theme)
theme.present? && theme != 'system' ? "?theme=#{theme}" : ''
end
def generate_home_link(portal_slug, portal_locale, theme, is_plain_layout_enabled)
if is_plain_layout_enabled
"/hc/#{portal_slug}/#{portal_locale}#{theme_query_string(theme)}"
def get_theme_names(theme)
if theme == 'light'
I18n.t('public_portal.header.appearance.light')
elsif theme == 'dark'
I18n.t('public_portal.header.appearance.dark')
else
"/hc/#{portal_slug}/#{portal_locale}"
I18n.t('public_portal.header.appearance.system')
end
end
def generate_category_link(params)
portal_slug = params[:portal_slug]
category_locale = params[:category_locale]
category_slug = params[:category_slug]
theme = params[:theme]
is_plain_layout_enabled = params[:is_plain_layout_enabled]
if is_plain_layout_enabled
"/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}#{theme_query_string(theme)}"
def get_theme_icon(theme)
if theme == 'light'
'icons/sun'
elsif theme == 'dark'
'icons/moon'
else
"/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}"
'icons/monitor'
end
end
def generate_article_link(portal_slug, article_slug, theme, is_plain_layout_enabled)
if is_plain_layout_enabled
"/hc/#{portal_slug}/articles/#{article_slug}#{theme_query_string(theme)}"
else
"/hc/#{portal_slug}/articles/#{article_slug}"
end
def generate_gradient_to_bottom(theme)
"background-image: linear-gradient(to bottom, transparent, #{theme == 'dark' ? '#151718' : 'white'})"
end
def generate_article_link(portal_slug, article_slug, theme)
"/hc/#{portal_slug}/articles/#{article_slug}#{theme.present? && theme != 'system' ? "?theme=#{theme}" : ''}"
end
def render_category_content(content)
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
end
def thumbnail_bg_color(username)
colors = ['#6D95BA', '#A4C3C3', '#E19191']
return colors.sample if username.blank?
colors[username.length % colors.size]
end
end
@@ -86,12 +86,6 @@ class MessageApi extends ApiClient {
return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`);
}
retry(conversationID, messageId) {
return axios.post(
`${this.url}/${conversationID}/messages/${messageId}/retry`
);
}
getPreviousMessages({ conversationId, after, before }) {
const params = { before };
if (after && Number(after) !== Number(before)) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -1,8 +1,39 @@
@import 'shared/assets/fonts/inter';
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import '../variables';
body {
font-family: Inter, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
.superadmin-body {
background: var(--color-background);
.hero--title {
font-size: var(--font-size-mega);
font-weight: var(--font-weight-light);
margin-top: var(--space-large);
}
.update-subscription--checkbox {
display: flex;
input {
line-height: 1.5;
margin-right: var(--space-one);
margin-top: var(--space-smaller);
}
label {
font-size: var(--font-size-small);
line-height: 1.5;
margin-bottom: var(--space-normal);
}
}
}
.alert-box {
background-color: var(--r-500);
border-radius: 5px;
color: var(--color-white);
font-size: 14px;
margin-bottom: 14px;
padding: 10px;
text-align: center;
}
@@ -0,0 +1,3 @@
@import 'shared/assets/fonts/plus-jakarta';
@import '../variables';
@import '~shared/assets/stylesheets/ionicons';
@@ -76,11 +76,7 @@
&.left {
.bubble {
@apply rounded-r-lg rounded-l mr-auto break-words;
&:not(.is-unsupported) {
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50
}
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50 rounded-r-lg rounded-l mr-auto break-words;
&.is-image {
@apply rounded-lg;
@@ -126,33 +126,49 @@
@assign-team="onAssignTeamsForBulk"
/>
<div
ref="conversationList"
ref="activeConversation"
class="conversations-list flex-1"
:class="{ 'overflow-hidden': isContextMenuOpen }"
>
<virtual-list
ref="conversationVirtualList"
:data-key="'id'"
:data-sources="conversationList"
:data-component="itemComponent"
:extra-props="virtualListExtraProps"
class="w-full overflow-auto h-full"
footer-tag="div"
<div>
<conversation-card
v-for="chat in conversationList"
:key="chat.id"
:active-label="label"
:team-id="teamId"
:folders-id="foldersId"
:chat="chat"
:conversation-type="conversationType"
:show-assignee="showAssigneeInConversationCard"
:selected="isConversationSelected(chat.id)"
@select-conversation="selectConversation"
@de-select-conversation="deSelectConversation"
@assign-agent="onAssignAgent"
@assign-team="onAssignTeam"
@assign-label="onAssignLabels"
@update-conversation-status="toggleConversationStatus"
@context-menu-toggle="onContextMenuToggle"
@mark-as-unread="markAsUnread"
@assign-priority="assignPriority"
/>
</div>
<div v-if="chatListLoading" class="text-center">
<span class="spinner mt-4 mb-4" />
</div>
<woot-button
v-if="!hasCurrentPageEndReached && !chatListLoading"
variant="clear"
size="expanded"
class="load-more--button"
@click="loadMoreConversations"
>
<template #footer>
<div v-if="chatListLoading" class="text-center">
<span class="spinner mt-4 mb-4" />
</div>
<p v-if="showEndOfListMessage" class="text-center text-muted p-4">
{{ $t('CHAT_LIST.EOF') }}
</p>
<intersection-observer
v-if="!showEndOfListMessage && !chatListLoading"
:options="infiniteLoaderOptions"
@observed="loadMoreConversations"
/>
</template>
</virtual-list>
{{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
</woot-button>
<p v-if="showEndOfListMessage" class="text-center text-muted p-4">
{{ $t('CHAT_LIST.EOF') }}
</p>
</div>
<woot-modal
:show.sync="showAdvancedFilters"
@@ -175,12 +191,11 @@
<script>
import { mapGetters } from 'vuex';
import VirtualList from 'vue-virtual-scroll-list';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationItem from './ConversationItem.vue';
import ConversationCard from './widgets/conversation/ConversationCard.vue';
import timeMixin from '../mixins/time';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import conversationMixin from '../mixins/conversations';
@@ -207,20 +222,16 @@ import {
isOnUnattendedView,
} from '../store/modules/conversations/helpers/actionHelpers';
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
import IntersectionObserver from './IntersectionObserver.vue';
export default {
components: {
AddCustomViews,
ChatTypeTabs,
// eslint-disable-next-line vue/no-unused-components
ConversationItem,
ConversationCard,
ConversationAdvancedFilter,
DeleteCustomViews,
ConversationBulkActions,
ConversationBasicFilter,
IntersectionObserver,
VirtualList,
},
mixins: [
timeMixin,
@@ -230,20 +241,6 @@ export default {
filterMixin,
uiSettingsMixin,
],
provide() {
return {
// Actions to be performed on virtual list item and context menu.
selectConversation: this.selectConversation,
deSelectConversation: this.deSelectConversation,
assignAgent: this.onAssignAgent,
assignTeam: this.onAssignTeam,
assignLabels: this.onAssignLabels,
updateConversationStatus: this.toggleConversationStatus,
toggleContextMenu: this.onContextMenuToggle,
markAsUnread: this.markAsUnread,
assignPriority: this.assignPriority,
};
},
props: {
conversationInbox: {
type: [String, Number],
@@ -278,7 +275,7 @@ export default {
return {
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
activeStatus: wootConstants.STATUS_TYPE.OPEN,
activeSortBy: wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC,
activeSortBy: wootConstants.SORT_BY_TYPE.LATEST,
showAdvancedFilters: false,
advancedFilterTypes: advancedFilterTypes.map(filter => ({
...filter,
@@ -294,21 +291,6 @@ export default {
selectedInboxes: [],
isContextMenuOpen: false,
appliedFilter: [],
infiniteLoaderOptions: {
root: this.$refs.conversationList,
rootMargin: '100px 0px 100px 0px',
},
itemComponent: ConversationItem,
// virtualListExtraProps is to pass the props to the conversationItem component.
virtualListExtraProps: {
label: this.label,
teamId: this.teamId,
foldersId: this.foldersId,
conversationType: this.conversationType,
showAssignee: false,
isConversationSelected: this.isConversationSelected,
},
};
},
computed: {
@@ -527,22 +509,16 @@ export default {
},
label() {
this.resetAndFetchData();
this.updateVirtualListProps('label', this.label);
},
conversationType() {
this.resetAndFetchData();
this.updateVirtualListProps('conversationType', this.conversationType);
},
activeFolder() {
this.resetAndFetchData();
this.updateVirtualListProps('foldersId', this.foldersId);
},
chatLists() {
this.chatsOnView = this.conversationList;
},
showAssigneeInConversationCard(newVal) {
this.updateVirtualListProps('showAssignee', newVal);
},
},
mounted() {
this.setFiltersFromUISettings();
@@ -559,12 +535,6 @@ export default {
});
},
methods: {
updateVirtualListProps(key, value) {
this.virtualListExtraProps = {
...this.virtualListExtraProps,
[key]: value,
};
},
onApplyFilter(payload) {
this.resetBulkActions();
this.foldersQuery = filterQueryGenerator(payload);
@@ -585,10 +555,7 @@ export default {
const { conversations_filter_by: filterBy = {} } = this.uiSettings;
const { status, order_by: orderBy } = filterBy;
this.activeStatus = status || wootConstants.STATUS_TYPE.OPEN;
this.activeSortBy =
Object.keys(wootConstants.SORT_BY_TYPE).find(
sortField => sortField === orderBy
) || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC;
this.activeSortBy = orderBy || wootConstants.SORT_BY_TYPE.LATEST;
},
onClickOpenAddFoldersModal() {
this.showAddFoldersModal = true;
@@ -668,10 +635,10 @@ export default {
);
},
getKeyboardListenerParams() {
const allConversations = this.$refs.conversationList.querySelectorAll(
const allConversations = this.$refs.activeConversation.querySelectorAll(
'div.conversations-list div.conversation'
);
const activeConversation = this.$refs.conversationList.querySelector(
const activeConversation = this.$refs.activeConversation.querySelector(
'div.conversations-list div.conversation.active'
);
const activeConversationIndex = [...allConversations].indexOf(
@@ -727,12 +694,9 @@ export default {
fetchConversations() {
this.$store
.dispatch('fetchAllConversations', this.conversationFilters)
.then(this.emitConversationLoaded);
.then(() => this.$emit('conversation-load'));
},
loadMoreConversations() {
if (this.hasCurrentPageEndReached || this.chatListLoading) {
return;
}
if (!this.hasAppliedFiltersOrActiveFolders) {
this.fetchConversations();
}
@@ -751,7 +715,7 @@ export default {
queryData: filterQueryGenerator(payload),
page,
})
.then(this.emitConversationLoaded);
.then(() => this.$emit('conversation-load'));
this.showAdvancedFilters = false;
},
fetchSavedFilteredConversations(payload) {
@@ -761,7 +725,7 @@ export default {
queryData: payload,
page,
})
.then(this.emitConversationLoaded);
.then(() => this.$emit('conversation-load'));
},
updateAssigneeTab(selectedTab) {
if (this.activeAssigneeTab !== selectedTab) {
@@ -773,20 +737,6 @@ export default {
}
}
},
emitConversationLoaded() {
this.$emit('conversation-load');
this.$nextTick(() => {
// Addressing a known issue in the virtual list library where dynamically added items
// might not render correctly. This workaround involves a slight manual adjustment
// to the scroll position, triggering the list to refresh its rendering.
const virtualList = this.$refs.conversationVirtualList;
const scrollToOffset = virtualList?.scrollToOffset;
const currentOffset = virtualList?.getOffset() || 0;
if (scrollToOffset) {
scrollToOffset(currentOffset + 1);
}
});
},
resetBulkActions() {
this.selectedConversations = [];
this.selectedInboxes = [];
@@ -1,72 +0,0 @@
<template>
<conversation-card
:key="source.id"
:active-label="label"
:team-id="teamId"
:folders-id="foldersId"
:chat="source"
:conversation-type="conversationType"
:selected="isConversationSelected(source.id)"
:show-assignee="showAssignee"
:enable-context-menu="true"
@select-conversation="selectConversation"
@de-select-conversation="deSelectConversation"
@assign-agent="assignAgent"
@assign-team="assignTeam"
@assign-label="assignLabels"
@update-conversation-status="updateConversationStatus"
@context-menu-toggle="toggleContextMenu"
@mark-as-unread="markAsUnread"
@assign-priority="assignPriority"
/>
</template>
<script>
import ConversationCard from './widgets/conversation/ConversationCard.vue';
export default {
components: {
ConversationCard,
},
inject: [
'selectConversation',
'deSelectConversation',
'assignAgent',
'assignTeam',
'assignLabels',
'updateConversationStatus',
'toggleContextMenu',
'markAsUnread',
'assignPriority',
],
props: {
source: {
type: Object,
required: true,
},
teamId: {
type: [String, Number],
default: 0,
},
label: {
type: String,
default: '',
},
conversationType: {
type: String,
default: '',
},
foldersId: {
type: [String, Number],
default: 0,
},
isConversationSelected: {
type: Function,
default: () => {},
},
showAssignee: {
type: Boolean,
default: false,
},
},
};
</script>
@@ -1,34 +0,0 @@
<template>
<div ref="observedElement" class="h-6 w-full" />
</template>
<script>
export default {
props: {
options: {
type: Object,
default: () => ({ root: document, rootMargin: '100px 0 100px 0)' }),
},
},
mounted() {
this.intersectionObserver = null;
this.registerInfiniteLoader();
},
beforeDestroy() {
this.unobserveInfiniteLoadObserver();
},
methods: {
registerInfiniteLoader() {
this.intersectionObserver = new IntersectionObserver(entries => {
if (entries && entries[0].isIntersecting) {
this.$emit('observed');
}
}, this.options);
this.intersectionObserver.observe(this.$refs.observedElement);
},
unobserveInfiniteLoadObserver() {
this.intersectionObserver.unobserve(this.$refs.observedElement);
},
},
};
</script>
@@ -1,33 +1,24 @@
<template>
<transition name="network-notification-fade" tag="div">
<div v-show="showNotification" class="fixed top-4 left-2 z-50 group">
<div
class="flex items-center justify-between py-1 px-2 w-full rounded-lg shadow-lg bg-yellow-200 dark:bg-yellow-700 relative"
>
<fluent-icon
icon="wifi-off"
class="text-yellow-700/50 dark:text-yellow-50"
size="18"
/>
<span
class="text-xs tracking-wide font-medium px-2 text-yellow-700/70 dark:text-yellow-50"
>
{{ $t('NETWORK.NOTIFICATION.OFFLINE') }}
</span>
<div v-show="showNotification" class="ui-notification-container">
<div class="ui-notification">
<fluent-icon icon="wifi-off" />
<p class="ui-notification-text">
{{
useInstallationName(
$t('NETWORK.NOTIFICATION.TEXT'),
globalConfig.installationName
)
}}
</p>
<woot-button variant="clear" size="small" @click="refreshPage">
{{ $t('NETWORK.BUTTON.REFRESH') }}
</woot-button>
<woot-button
:title="$t('NETWORK.BUTTON.REFRESH')"
variant="clear"
variant="smooth"
size="small"
color-scheme="warning"
icon="arrow-clockwise"
class="visible transition-all duration-500 ease-in-out ml-1"
@click="refreshPage"
/>
<woot-button
variant="clear"
size="small"
color-scheme="warning"
icon="dismiss"
icon="dismiss-circle"
@click="closeNotification"
/>
</div>
@@ -56,12 +47,7 @@ export default {
mounted() {
window.addEventListener('offline', this.updateOnlineStatus);
window.bus.$on(BUS_EVENTS.WEBSOCKET_DISCONNECT, () => {
// TODO: Remove this after completing the conversation list refetching
// TODO: DIRTY FIX : CLEAN UP THIS WITH PROPER FIX, DELAYING THE RECONNECT FOR NOW
// THE CABLE IS FIRING IS VERY COMMON AND THUS INTERFERING USER EXPERIENCE
setTimeout(() => {
this.updateOnlineStatus({ type: 'offline' });
}, 4000);
this.updateOnlineStatus({ type: 'offline' });
});
},
@@ -86,3 +72,33 @@ export default {
},
};
</script>
<style scoped lang="scss">
@import '~dashboard/assets/scss/mixins';
.ui-notification-container {
max-width: 25rem;
position: absolute;
right: var(--space-normal);
top: var(--space-normal);
z-index: var(--z-index-very-high);
}
.ui-notification {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
background-color: var(--y-100);
border-radius: var(--border-radius-medium);
box-shadow: var(--shadow-large);
min-width: 15rem;
padding: var(--space-normal);
}
.ui-notification-text {
margin: 0 var(--space-small);
}
</style>
@@ -84,10 +84,7 @@ import {
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import {
replaceVariablesInMessage,
createTypingIndicator,
} from '@chatwoot/utils';
import { replaceVariablesInMessage } from '@chatwoot/utils';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { uploadFile } from 'dashboard/helper/uploadHelper';
@@ -143,15 +140,6 @@ export default {
},
data() {
return {
typingIndicator: createTypingIndicator(
() => {
this.$emit('typing-on');
},
() => {
this.$emit('typing-off');
},
TYPING_INDICATOR_IDLE_TIME
),
showUserMentions: false,
showCannedMenu: false,
showVariables: false,
@@ -650,6 +638,15 @@ export default {
hideMentions() {
this.showUserMentions = false;
},
resetTyping() {
this.$emit('typing-off');
this.idleTimer = null;
},
turnOffIdleTimer() {
if (this.idleTimer) {
clearTimeout(this.idleTimer);
}
},
handleLineBreakWhenEnterToSendEnabled(event) {
if (
hasPressedEnterAndNotCmdOrShift(event) &&
@@ -669,7 +666,14 @@ export default {
}
},
onKeyup() {
this.typingIndicator.start();
if (!this.idleTimer) {
this.$emit('typing-on');
}
this.turnOffIdleTimer();
this.idleTimer = setTimeout(
() => this.resetTyping(),
TYPING_INDICATOR_IDLE_TIME
);
this.updateImgToolbarOnDelete();
},
onKeydown(event) {
@@ -681,7 +685,8 @@ export default {
}
},
onBlur() {
this.typingIndicator.stop();
this.turnOffIdleTimer();
this.resetTyping();
this.$emit('blur');
},
onFocus() {
@@ -144,7 +144,6 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import {
ALLOWED_FILE_TYPES,
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
ALLOWED_FILE_TYPES_FOR_LINE,
} from 'shared/constants/messages';
import VideoCallButton from '../VideoCallButton.vue';
import AIAssistanceButton from '../AIAssistanceButton.vue';
@@ -271,9 +270,6 @@ export default {
return this.showFileUpload || this.isNote;
},
showAudioRecorderButton() {
if (this.isALineChannel) {
return false;
}
// Disable audio recorder for safari browser as recording is not supported
const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(
navigator.userAgent
@@ -295,9 +291,6 @@ export default {
if (this.isATwilioWhatsAppChannel) {
return ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP;
}
if (this.isALineChannel) {
return ALLOWED_FILE_TYPES_FOR_LINE;
}
return ALLOWED_FILE_TYPES;
},
enableDragAndDrop() {
@@ -1,9 +1,5 @@
<template>
<mention-box
v-if="items.length"
:items="items"
@mention-select="handleMentionClick"
>
<mention-box :items="items" @mention-select="handleMentionClick">
<template slot-scope="{ item }">
<strong>{{ item.label }}</strong> - {{ item.description }}
</template>
@@ -34,7 +34,7 @@
type="sort"
:selected-value="sortFilter"
:items="chatSortItems"
path-prefix="CHAT_LIST.SORT_ORDER_ITEMS"
path-prefix="CHAT_LIST.CHAT_SORT_FILTER_ITEMS"
@onChangeFilter="onChangeFilter"
/>
</div>
@@ -58,7 +58,7 @@ export default {
return {
showActionsDropdown: false,
chatStatusItems: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS'),
chatSortItems: this.$t('CHAT_LIST.SORT_ORDER_ITEMS'),
chatSortItems: this.$t('CHAT_LIST.CHAT_SORT_FILTER_ITEMS'),
};
},
computed: {
@@ -70,9 +70,7 @@ export default {
return this.chatStatusFilter || wootConstants.STATUS_TYPE.OPEN;
},
sortFilter() {
return (
this.chatSortFilter || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC
);
return this.chatSortFilter || wootConstants.SORT_BY_TYPE.LATEST;
},
},
methods: {
@@ -31,7 +31,7 @@
size="40px"
/>
<div
class="px-0 py-3 border-b group-hover:border-transparent border-slate-50 dark:border-slate-800/75 columns"
class="px-0 py-3 border-b group-last:border-transparent group-hover:border-transparent border-slate-50 dark:border-slate-800/75 columns"
>
<div class="flex justify-between">
<inbox-name v-if="showInboxName" :inbox="inbox" />
@@ -175,10 +175,6 @@ export default {
type: Boolean,
default: false,
},
enableContextMenu: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -293,7 +289,6 @@ export default {
this.$emit(action, this.chat.id, this.inbox.id);
},
openContextMenu(e) {
if (!this.enableContextMenu) return;
e.preventDefault();
this.$emit('context-menu-toggle', true);
this.contextMenu.x = e.pageX || e.clientX;
@@ -1,7 +1,7 @@
<template>
<li v-if="shouldRenderMessage" :id="`message${data.id}`" :class="alignBubble">
<div :class="wrapClass">
<div v-if="isFailed && !hasOneDayPassed" class="message-failed--alert">
<div v-if="isFailed" class="message-failed--alert">
<woot-button
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
size="tiny"
@@ -29,19 +29,8 @@
:message-type="data.message_type"
:parent-has-attachments="hasAttachments"
/>
<div v-if="isUnsupported">
<template v-if="isAFacebookInbox && isInstagram">
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_INSTAGRAM') }}
</template>
<template v-else-if="isAFacebookInbox">
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_FACEBOOK') }}
</template>
<template v-else>
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE') }}
</template>
</div>
<bubble-text
v-else-if="data.content"
v-if="data.content"
:message="message"
:is-email="isEmailContentType"
:display-quoted-button="displayQuotedButton"
@@ -159,7 +148,6 @@ import { BUS_EVENTS } from 'shared/constants/busEvents';
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper';
export default {
components: {
@@ -187,14 +175,6 @@ export default {
type: Boolean,
default: false,
},
isAFacebookInbox: {
type: Boolean,
default: false,
},
isInstagram: {
type: Boolean,
default: false,
},
isAWhatsAppChannel: {
type: Boolean,
default: false,
@@ -229,16 +209,11 @@ export default {
created_at: this.data.created_at || '',
}));
},
hasOneDayPassed() {
// Disable retry button if the message is failed and the message is older than 24 hours
return getDayDifferenceFromNow(new Date(), this.data?.created_at) >= 1;
},
shouldRenderMessage() {
return (
this.hasAttachments ||
this.data.content ||
this.isEmailContentType ||
this.isUnsupported ||
this.isAnIntegrationMessage
);
},
@@ -427,7 +402,6 @@ export default {
return {
bubble: this.isBubble,
'is-private': this.data.private,
'is-unsupported': this.isUnsupported,
'is-image': this.hasMediaAttachment('image'),
'is-video': this.hasMediaAttachment('video'),
'is-text': this.hasText,
@@ -436,9 +410,6 @@ export default {
'is-email': this.isEmailContentType,
};
},
isUnsupported() {
return this.contentAttributes.is_unsupported ?? false;
},
isPending() {
return this.data.status === MESSAGE_STATUS.PROGRESS;
},
@@ -450,7 +421,11 @@ export default {
return !this.sender.type || this.sender.type === 'agent_bot';
},
shouldShowContextMenu() {
return !(this.isFailed || this.isPending || this.isUnsupported);
return !(this.isFailed || this.isPending);
},
errorMessage() {
const { meta } = this.data;
return meta ? meta.error : '';
},
showAvatar() {
if (this.isOutgoing || this.isTemplate) {
@@ -552,14 +527,6 @@ export default {
> .bubble {
@apply min-w-[128px];
&.is-unsupported {
@apply text-xs max-w-[300px] border-dashed border border-slate-200 text-slate-600 dark:text-slate-200 bg-slate-50 dark:bg-slate-700 dark:border-slate-500;
.message-text--metadata .time {
@apply text-slate-400 dark:text-slate-300;
}
}
&.is-image,
&.is-video {
@apply p-0 overflow-hidden;
@@ -572,12 +539,10 @@ export default {
> video {
@apply rounded-lg;
}
> video {
@apply h-full w-full object-cover;
}
}
.video {
@apply h-[11.25rem];
}
@@ -592,11 +557,9 @@ export default {
.file--icon {
@apply text-woot-400 dark:text-woot-400;
}
.text-block-title {
@apply text-slate-700 dark:text-slate-700;
}
.download.button {
@apply text-woot-400 dark:text-woot-400;
}
@@ -605,7 +568,6 @@ export default {
&.is-private.is-text > .message-text__wrap .link {
@apply text-woot-600 dark:text-woot-200;
}
&.is-private.is-text > .message-text__wrap .prosemirror-mention-node {
@apply font-bold bg-none rounded-sm p-0 bg-yellow-100 dark:bg-yellow-700 text-slate-700 dark:text-slate-25 underline;
}
@@ -616,7 +578,6 @@ export default {
.message-text--metadata .time {
@apply text-violet-50 dark:text-violet-50;
}
&.is-private .message-text--metadata .time {
@apply text-slate-400 dark:text-slate-400;
}
@@ -20,15 +20,7 @@
icon="info"
/>
</template>
<span v-if="message.content && isMessageSticker">
<fluent-icon
size="16"
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
icon="image"
/>
{{ $t('CHAT_LIST.ATTACHMENTS.image.CONTENT') }}
</span>
<span v-else-if="message.content">
<span v-if="message.content">
{{ parsedLastMessage }}
</span>
<span v-else-if="message.attachments">
@@ -96,9 +88,6 @@ export default {
attachmentMessageContent() {
return `CHAT_LIST.ATTACHMENTS.${this.lastMessageFileType}.CONTENT`;
},
isMessageSticker() {
return this.message && this.message.content_type === 'sticker';
},
},
};
</script>
@@ -32,8 +32,6 @@
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:is-web-widget-inbox="isAWebWidgetInbox"
:is-a-facebook-inbox="isAFacebookInbox"
:is-instagram="isInstagramDM"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)"
/>
@@ -56,8 +54,6 @@
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:is-web-widget-inbox="isAWebWidgetInbox"
:is-a-facebook-inbox="isAFacebookInbox"
:is-instagram-dm="isInstagramDM"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)"
/>
@@ -287,16 +283,11 @@ export default {
unreadMessageCount() {
return this.currentChat.unread_count || 0;
},
isInstagramDM() {
return this.conversationType === 'instagram_direct_message';
},
inboxSupportsReplyTo() {
const incoming = this.inboxHasFeature(INBOX_FEATURES.REPLY_TO);
const outgoing =
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING) &&
!this.is360DialogWhatsAppChannel;
return { incoming, outgoing };
return {
incoming: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO),
outgoing: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING),
};
},
},
@@ -271,17 +271,11 @@ export default {
accountId: 'getCurrentAccountId',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}),
currentContact() {
return this.$store.getters['contacts/getContact'](
this.currentChat.meta.sender.id
);
},
shouldShowReplyToMessage() {
return (
this.inReplyTo?.id &&
!this.isPrivate &&
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
!this.is360DialogWhatsAppChannel
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO)
);
},
showRichContentEditor() {
@@ -398,8 +392,7 @@ export default {
this.isAPIInbox ||
this.isAnEmailChannel ||
this.isASmsInbox ||
this.isATelegramChannel ||
this.isALineChannel
this.isATelegramChannel
);
},
replyButtonLabel() {
@@ -502,11 +495,7 @@ export default {
return `draft-${this.conversationIdByRoute}-${this.replyType}`;
},
audioRecordFormat() {
if (
this.isAWhatsAppChannel ||
this.isAPIInbox ||
this.isATelegramChannel
) {
if (this.isAWhatsAppChannel || this.isAPIInbox) {
return AUDIO_FORMATS.OGG;
}
return AUDIO_FORMATS.WAV;
@@ -514,7 +503,6 @@ export default {
messageVariables() {
const variables = getMessageVariables({
conversation: this.currentChat,
contact: this.currentContact,
});
return variables;
},
@@ -636,8 +624,6 @@ export default {
`${this.$t('CONVERSATION.REPLYBOX.INSERT_READ_MORE')} ${url}`
);
}
this.$track(CONVERSATION_EVENTS.INSERT_ARTICLE_LINK);
},
toggleRichContentEditor() {
this.updateUISettings({
@@ -1,10 +1,5 @@
<template>
<mention-box
v-if="items.length"
type="variable"
:items="items"
@mention-select="handleVariableClick"
>
<mention-box :items="items" @mention-select="handleVariableClick">
<template slot-scope="{ item }">
<span class="text-capitalize variable--list-label">
{{ item.description }}
@@ -15,7 +10,6 @@
</template>
<script>
import { mapGetters } from 'vuex';
import { MESSAGE_VARIABLES } from 'shared/constants/messages';
import MentionBox from '../mentions/MentionBox.vue';
@@ -28,16 +22,7 @@ export default {
},
},
computed: {
...mapGetters({
customAttributes: 'attributes/getAttributes',
}),
items() {
return [
...this.standardAttributeVariables,
...this.customAttributeVariables,
];
},
standardAttributeVariables() {
return MESSAGE_VARIABLES.filter(variable => {
return (
variable.label.includes(this.searchKey) ||
@@ -49,20 +34,6 @@ export default {
description: variable.label,
}));
},
customAttributeVariables() {
return this.customAttributes.map(attribute => {
const attributePrefix =
attribute.attribute_model === 'conversation_attribute'
? 'conversation'
: 'contact';
return {
label: `${attributePrefix}.custom_attribute.${attribute.attribute_key}`,
key: `${attributePrefix}.custom_attribute.${attribute.attribute_key}`,
description: attribute.attribute_description,
};
});
},
},
methods: {
handleVariableClick(item = {}) {
@@ -7,11 +7,7 @@
}"
>
<div v-if="!isEmail" v-dompurify-html="message" class="text-content" />
<letter
v-else
class="text-content bg-white dark:bg-white text-slate-900 dark:text-slate-900 p-2 rounded-[4px]"
:html="message"
/>
<letter v-else class="text-content" :html="message" />
<button
v-if="showQuoteToggle"
class="text-slate-300 dark:text-slate-300 cursor-pointer text-xs py-1"
@@ -46,25 +46,9 @@
</span>
</div>
<div
class="items-center flex gap-2 justify-end min-w-[8rem] sm:min-w-[15rem]"
class="items-center flex gap-2 justify-end min-w-[15rem]"
@click.stop
>
<woot-button
v-if="isImage"
size="large"
color-scheme="secondary"
variant="clear"
icon="arrow-rotate-counter-clockwise"
@click="onRotate('counter-clockwise')"
/>
<woot-button
v-if="isImage"
size="large"
color-scheme="secondary"
variant="clear"
icon="arrow-rotate-clockwise"
@click="onRotate('clockwise')"
/>
<woot-button
size="large"
color-scheme="secondary"
@@ -105,7 +89,6 @@
:key="activeAttachment.message_id"
:src="activeAttachment.data_url"
class="modal-image skip-context-menu my-0 mx-auto"
:style="imageRotationStyle"
@click.stop
/>
<video
@@ -203,7 +186,6 @@ export default {
this.allAttachments.findIndex(
attachment => attachment.message_id === this.attachment.message_id
) || 0,
activeImageRotation: 0,
};
},
computed: {
@@ -250,11 +232,6 @@ export default {
const fileName = dataUrl?.split('/').pop();
return fileName || '';
},
imageRotationStyle() {
return {
transform: `rotate(${this.activeImageRotation}deg)`,
};
},
},
mounted() {
this.setImageAndVideoSrc(this.attachment);
@@ -269,7 +246,6 @@ export default {
}
this.activeImageIndex = index;
this.setImageAndVideoSrc(attachment);
this.activeImageRotation = 0;
},
setImageAndVideoSrc(attachment) {
const { file_type: type } = attachment;
@@ -304,20 +280,6 @@ export default {
link.download = `attachment.${type}`;
link.click();
},
onRotate(type) {
if (!this.isImage) {
return;
}
const rotation = type === 'clockwise' ? 90 : -90;
// Reset rotation if it is 360
if (Math.abs(this.activeImageRotation) === 360) {
this.activeImageRotation = rotation;
} else {
this.activeImageRotation += rotation;
}
},
},
};
</script>
@@ -25,7 +25,7 @@
id="file"
ref="file"
type="file"
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
accept="image/png, image/jpeg, image/gif"
@change="handleImageUpload"
/>
<slot />
@@ -1,40 +1,20 @@
<template>
<div
ref="mentionsListContainer"
class="bg-white dark:bg-slate-800 rounded-md overflow-auto absolute w-full z-20 pb-0 shadow-md left-0 bottom-full max-h-[9.75rem] border border-solid border-slate-100 dark:border-slate-700 mention--box"
>
<div v-if="items.length" ref="mentionsListContainer" class="mention--box">
<ul class="vertical dropdown menu">
<woot-dropdown-item
v-for="(item, index) in items"
:id="`mention-item-${index}`"
:key="item.key"
class="!mb-0"
@mouseover="onHover(index)"
>
<button
class="flex group flex-col gap-0.5 overflow-hidden cursor-pointer items-start py-2.5 px-2.5 justify-center w-full h-full text-left hover:bg-woot-50 dark:hover:bg-woot-800 border-b border-solid border-slate-100 dark:border-slate-700"
:class="{
' bg-woot-25 dark:bg-woot-800': index === selectedIndex,
}"
<woot-button
class="canned-item__button"
:variant="index === selectedIndex ? '' : 'clear'"
:class="{ active: index === selectedIndex }"
@click="onListItemSelection(index)"
>
<p
class="text-slate-900 dark:text-slate-100 group-hover:text-woot-500 dark:group-hover:text-woot-500 font-medium mb-0 text-sm overflow-hidden text-ellipsis whitespace-nowrap min-w-0 max-w-full"
:class="{
'text-woot-500 dark:text-woot-500': index === selectedIndex,
}"
>
{{ item.description }}
</p>
<p
class="text-slate-500 dark:text-slate-300 group-hover:text-woot-500 dark:group-hover:text-woot-500 mb-0 text-xs overflow-hidden text-ellipsis whitespace-nowrap min-w-0 max-w-full"
:class="{
'text-woot-500 dark:text-woot-500': index === selectedIndex,
}"
>
{{ variableKey(item) }}
</p>
</button>
<strong>{{ item.label }}</strong> - {{ item.description }}
</woot-button>
</woot-dropdown-item>
</ul>
</div>
@@ -49,10 +29,6 @@ export default {
type: Array,
default: () => {},
},
type: {
type: String,
default: 'canned',
},
},
data() {
return {
@@ -97,17 +73,28 @@ export default {
onSelect() {
this.$emit('mention-select', this.items[this.selectedIndex]);
},
variableKey(item = {}) {
return this.type === 'variable' ? `{{${item.label}}}` : `/${item.label}`;
},
},
};
</script>
<style scoped lang="scss">
.mention--box {
.dropdown-menu__item:last-child > button {
@apply border-0;
@apply bg-white dark:bg-slate-700 rounded-md overflow-auto absolute w-full z-20 pt-2 px-2 pb-0 shadow-md left-0 bottom-full max-h-[9.75rem] border-t border-solid border-slate-75 dark:border-slate-800;
.dropdown-menu__item:last-child {
@apply pb-1;
}
.active {
@apply text-white dark:text-white;
&:hover {
@apply bg-woot-700 dark:bg-woot-700;
}
}
.button {
@apply transition-none h-8 leading-[1.4];
}
}
@@ -13,14 +13,10 @@ export default {
ALL: 'all',
},
SORT_BY_TYPE: {
LAST_ACTIVITY_AT_ASC: 'last_activity_at_asc',
LAST_ACTIVITY_AT_DESC: 'last_activity_at_desc',
CREATED_AT_ASC: 'created_at_asc',
CREATED_AT_DESC: 'created_at_desc',
PRIORITY_ASC: 'priority_asc',
PRIORITY_DESC: 'priority_desc',
WAITING_SINCE_ASC: 'waiting_since_asc',
WAITING_SINCE_DESC: 'waiting_since_desc',
LATEST: 'latest',
CREATED_AT: 'sort_on_created_at',
PRIORITY: 'sort_on_priority',
WATIING_SINCE: 'waiting_since',
},
ARTICLE_STATUS_TYPES: {
DRAFT: 0,
@@ -9,7 +9,6 @@ export const CONVERSATION_EVENTS = Object.freeze({
SEARCH_CONVERSATION: 'Searched conversations',
APPLY_FILTER: 'Applied filters in the conversation list',
CHANGE_PRIORITY: 'Assigned priority to a conversation',
INSERT_ARTICLE_LINK: 'Inserted article into reply via article search',
});
export const ACCOUNT_EVENTS = Object.freeze({
@@ -10,7 +10,6 @@ export class DataManager {
async initDb() {
if (this.db) return this.db;
const dbName = `cw-store-${this.accountId}`;
this.db = await openDB(`cw-store-${this.accountId}`, DATA_VERSION, {
upgrade(db) {
db.createObjectStore('cache-keys');
@@ -20,13 +19,6 @@ export class DataManager {
},
});
// Store the database name in LocalStorage
const dbNames = JSON.parse(localStorage.getItem('cw-idb-names') || '[]');
if (!dbNames.includes(dbName)) {
dbNames.push(dbName);
localStorage.setItem('cw-idb-names', JSON.stringify(dbNames));
}
return this.db;
}
@@ -23,7 +23,6 @@ class ActionCableConnector extends BaseActionCableConnector {
'contact.updated': this.onContactUpdate,
'conversation.mentioned': this.onConversationMentioned,
'notification.created': this.onNotificationCreated,
'notification.deleted': this.onNotificationDeleted,
'first.reply.created': this.onFirstReplyCreated,
'conversation.read': this.onConversationRead,
'conversation.updated': this.onConversationUpdated,
@@ -196,10 +195,6 @@ class ActionCableConnector extends BaseActionCableConnector {
this.app.$store.dispatch('notifications/addNotification', data);
};
onNotificationDeleted = data => {
this.app.$store.dispatch('notifications/deleteNotification', data);
};
// eslint-disable-next-line class-methods-use-this
onFirstReplyCreated = () => {
bus.$emit('fetch_overview_reports');
@@ -51,30 +51,18 @@
"ACTIVE": "Last activity"
}
},
"SORT_ORDER_ITEMS": {
"last_activity_at_asc": {
"TEXT": "Last activity: Oldest first"
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
},
"last_activity_at_desc": {
"TEXT": "Last activity: Newest first"
"sort_on_created_at": {
"TEXT": "Created at"
},
"created_at_desc": {
"TEXT": "Created at: Newest first"
"sort_on_priority": {
"TEXT": "Priority"
},
"created_at_asc": {
"TEXT": "Created at: Oldest first"
},
"priority_desc": {
"TEXT": "Priority: Highest first"
},
"priority_asc": {
"TEXT": "Priority: Lowest first"
},
"waiting_since_asc": {
"TEXT": "Pending Response: Longest first"
},
"waiting_since_desc": {
"TEXT": "Pending Response: Shortest first"
"sort_on_waiting_since": {
"TEXT": "Pending Response"
}
},
"ATTACHMENTS": {
@@ -41,9 +41,6 @@
"SAVE_CONTACT": "Save",
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
"REPLIED_TO_STORY": "Replied to your story",
"UNSUPPORTED_MESSAGE": "This message is unsupported.",
"UNSUPPORTED_MESSAGE_FACEBOOK": "This message is unsupported. You can view this message on the Facebook Messenger app.",
"UNSUPPORTED_MESSAGE_INSTAGRAM": "This message is unsupported. You can view this message on the Instagram app.",
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
"NO_RESPONSE": "No response",
@@ -92,7 +92,7 @@
},
"NETWORK": {
"NOTIFICATION": {
"OFFLINE": "Offline"
"TEXT": "Disconnected from Chatwoot"
},
"BUTTON": {
"REFRESH": "Refresh"
@@ -3,7 +3,6 @@
"HEADER": {
"FILTER": "Filter by",
"SORT": "Sort by",
"LOCALE": "Locale",
"SETTINGS_BUTTON": "Settings",
"NEW_BUTTON": "New Article",
"DROPDOWN_OPTIONS": {
@@ -16,12 +15,6 @@
"MINE": "My Articles",
"DRAFT": "Draft Articles",
"ARCHIVED": "Archived Articles"
},
"LOCALE_SELECT": {
"TITLE": "Select locale",
"PLACEHOLDER": "Select locale",
"NO_RESULT": "No locale found",
"SEARCH_PLACEHOLDER": "Search locale"
}
},
"EDIT_HEADER": {
@@ -10,17 +10,6 @@ describe('#messageStamp', () => {
});
describe('#messageTimestamp', () => {
beforeEach(() => {
jest.useFakeTimers('modern');
const mockDate = new Date(2023, 4, 5);
jest.setSystemTime(mockDate);
});
afterEach(() => {
jest.useRealTimers();
});
it('should return the message date in the specified format if the message was sent in the current year', () => {
expect(TimeMixin.methods.messageTimestamp(1680777464)).toEqual(
'Apr 6, 2023'
@@ -1,8 +1,7 @@
/* eslint-disable storybook/default-exports */
import SearchView from './components/SearchView.vue';
import { frontendURL } from '../../helper/URLHelper';
const SearchView = () => import('./components/SearchView.vue');
export const routes = [
{
path: frontendURL('accounts/:accountId/search'),
@@ -1,7 +1,7 @@
/* eslint arrow-body-style: 0 */
import ContactsView from './components/ContactsView';
import ContactManageView from './pages/ContactManageView';
import { frontendURL } from '../../../helper/URLHelper';
const ContactsView = () => import('./components/ContactsView.vue');
const ContactManageView = () => import('./pages/ContactManageView.vue');
export const routes = [
{
@@ -1,6 +1,6 @@
/* eslint arrow-body-style: 0 */
import ConversationView from './ConversationView';
import { frontendURL } from '../../../helper/URLHelper';
const ConversationView = () => import('./ConversationView');
export default {
routes: [
@@ -1,3 +1,4 @@
import AppContainer from './Dashboard';
import settings from './settings/settings.routes';
import conversation from './conversation/conversation.routes';
import { routes as searchRoutes } from '../../modules/search/search.routes';
@@ -6,8 +7,7 @@ import { routes as notificationRoutes } from './notifications/routes';
import { frontendURL } from '../../helper/URLHelper';
import helpcenterRoutes from './helpcenter/helpcenter.routes';
const AppContainer = () => import('./Dashboard.vue');
const Suspended = () => import('./suspended/Index.vue');
const Suspended = () => import('./suspended/Index');
export default {
routes: [
@@ -161,7 +161,7 @@ export default {
}
span.article-column {
@apply text-slate-700 dark:text-slate-100 text-sm font-semibold py-2 px-0 text-left capitalize last:text-right;
@apply text-slate-700 dark:text-slate-100 text-sm font-semibold py-2 px-0 text-right capitalize;
&.article-title {
@apply items-start flex gap-2 col-span-4 text-left;
@@ -10,17 +10,17 @@
{{ $t('HELP_CENTER.TABLE.HEADERS.TITLE') }}
</div>
<div
class="font-semibold capitalize text-sm py-2 px-0 text-slate-700 dark:text-slate-100 text-left"
class="font-semibold capitalize text-sm py-2 px-0 text-slate-700 dark:text-slate-100 text-right"
>
{{ $t('HELP_CENTER.TABLE.HEADERS.CATEGORY') }}
</div>
<div
class="font-semibold capitalize text-sm py-2 px-0 text-slate-700 dark:text-slate-100 text-left hidden lg:block"
class="font-semibold capitalize text-sm py-2 px-0 text-slate-700 dark:text-slate-100 text-right hidden lg:block"
>
{{ $t('HELP_CENTER.TABLE.HEADERS.READ_COUNT') }}
</div>
<div
class="font-semibold capitalize text-sm py-2 px-0 text-slate-700 dark:text-slate-100 text-left"
class="font-semibold capitalize text-sm py-2 px-0 text-slate-700 dark:text-slate-100 text-right"
>
{{ $t('HELP_CENTER.TABLE.HEADERS.STATUS') }}
</div>
@@ -1,6 +1,6 @@
<template>
<div
class="flex px-4 items-center justify-between w-full h-16 pt-2 sticky top-0 z-10 bg-white dark:bg-slate-900"
class="flex px-4 items-center justify-between w-full h-16 pt-2 sticky top-0 bg-white dark:bg-slate-900"
>
<div class="flex items-center">
<woot-sidemenu-icon />
@@ -86,45 +86,6 @@
size="small"
color-scheme="secondary"
/>
<div class="relative">
<woot-button
v-if="shouldShowLocaleDropdown"
icon="globe"
color-scheme="secondary"
size="small"
variant="hollow"
@click="openLocaleDropdown"
>
<div class="flex justify-between w-full min-w-0 items-center">
<span
class="inline-flex ml-1 rtl:ml-0 rtl:mr-1 items-center text-slate-800 dark:text-slate-100"
>
{{ selectedLocale }}
<Fluent-icon
class="dropdown-arrow"
icon="chevron-down"
size="14"
/>
</span>
</div>
</woot-button>
<div
v-if="showLocaleDropdown"
v-on-clickaway="closeLocaleDropdown"
class="dropdown-pane dropdown-pane--open"
>
<multiselect-dropdown-items
:options="switchableLocales"
:has-thumbnail="false"
:selected-items="[selectedLocale]"
:input-placeholder="
$t('HELP_CENTER.HEADER.LOCALE_SELECT.SEARCH_PLACEHOLDER')
"
:no-search-result="$t('HELP_CENTER.HEADER.LOCALE_SELECT.NO_RESULT')"
@click="onClickSelectItem"
/>
</div>
</div>
<woot-button
size="small"
icon="add"
@@ -142,15 +103,12 @@ 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';
import MultiselectDropdownItems from 'shared/components/ui/MultiselectDropdownItems.vue';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue';
export default {
components: {
FluentIcon,
WootDropdownItem,
WootDropdownMenu,
MultiselectDropdownItems,
},
mixins: [clickaway],
props: {
@@ -166,35 +124,16 @@ export default {
type: String,
default: '',
},
selectedLocale: {
type: String,
default: '',
},
shouldShowSettings: {
type: Boolean,
default: false,
},
allLocales: {
type: Array,
default: () => [],
},
},
data() {
return {
showSortByDropdown: false,
showLocaleDropdown: false,
};
},
computed: {
shouldShowLocaleDropdown() {
return this.allLocales.length > 1;
},
switchableLocales() {
return this.allLocales.filter(
locale => locale.name !== this.selectedLocale
);
},
},
methods: {
openFilterModal() {
this.$emit('openModal');
@@ -207,22 +146,8 @@ export default {
this.$emit('close');
this.showSortByDropdown = false;
},
openLocaleDropdown() {
this.showLocaleDropdown = true;
},
closeLocaleDropdown() {
this.showLocaleDropdown = false;
},
onClickNewArticlePage() {
this.$emit('new-article-page');
},
onClickSelectItem(value) {
const { name, code } = value;
this.closeLocaleDropdown();
if (!name || name === this.selectedLocale) {
return;
}
this.$emit('change-locale', code);
this.$emit('newArticlePage');
},
},
};
@@ -230,7 +155,7 @@ export default {
<style scoped lang="scss">
.dropdown-pane--open {
@apply absolute top-10 right-0 z-50 min-w-[8rem];
@apply top-12 right-[9.25rem];
}
.dropdown-arrow {
@apply ml-1 rtl:ml-0 rtl:mr-1;
@@ -22,7 +22,7 @@
v-if="isHelpCenterEnabled"
class="flex h-full min-h-0 overflow-hidden flex-1 px-0 bg-white dark:bg-slate-900"
>
<router-view @reload-locale="fetchPortalAndItsCategories" />
<router-view />
<command-bar />
<account-selector
:show-account-modal="showAccountModal"
@@ -91,7 +91,6 @@
@search-change="handleSearchChange"
@close="onBlur"
@tag="addTagValue"
@remove="removeTag"
/>
</label>
</div>
@@ -158,23 +157,16 @@ export default {
return this.metaTags.map(item => item.name);
},
},
watch: {
article: {
handler() {
if (!isEmptyObject(this.article.meta || {})) {
const {
meta: { title = '', description = '', tags = [] },
} = this.article;
this.metaTitle = title;
this.metaDescription = description;
this.metaTags = this.formattedTags({ tags });
}
},
deep: true,
immediate: true,
},
},
mounted() {
if (!isEmptyObject(this.article.meta || {})) {
const {
meta: { title = '', description = '', tags = [] },
} = this.article;
this.metaTitle = title;
this.metaDescription = description;
this.metaTags = this.formattedTags({ tags });
}
this.saveArticle = debounce(
() => {
this.$emit('save-article', {
@@ -204,9 +196,6 @@ export default {
this.metaTags.push(...this.formattedTags({ tags: [...new Set(tags)] }));
this.saveArticle();
},
removeTag() {
this.saveArticle();
},
handleSearchChange(value) {
this.tagInputValue = value;
},
@@ -5,11 +5,8 @@
<article-header
:header-title="headerTitle"
:count="meta.count"
:selected-locale="activeLocaleName"
:all-locales="allowedLocales"
selected-value="Published"
@new-article-page="newArticlePage"
@change-locale="onChangeLocale"
@newArticlePage="newArticlePage"
/>
<article-table
:articles="articles"
@@ -35,7 +32,6 @@
</template>
<script>
import { mapGetters } from 'vuex';
import allLocales from 'shared/constants/locales.js';
import Spinner from 'shared/components/Spinner.vue';
import ArticleHeader from 'dashboard/routes/dashboard/helpcenter/components/Header/ArticleHeader.vue';
@@ -62,7 +58,6 @@ export default {
meta: 'articles/getMeta',
isFetching: 'articles/isFetching',
currentUserId: 'getCurrentUserID',
getPortalBySlug: 'portals/portalBySlug',
}),
selectedCategory() {
return this.categories.find(
@@ -123,28 +118,6 @@ export default {
? this.selectedCategory.name
: '';
},
activeLocale() {
return this.$route.params.locale;
},
activeLocaleName() {
return allLocales[this.activeLocale];
},
portal() {
return this.getPortalBySlug(this.selectedPortalSlug);
},
allowedLocales() {
if (!this.portal) {
return [];
}
const { allowed_locales: allowedLocales } = this.portal.config;
return allowedLocales.map(locale => {
return {
id: locale.code,
name: allLocales[locale.code],
code: locale.code,
};
});
},
},
watch: {
$route() {
@@ -164,7 +137,7 @@ export default {
this.$store.dispatch('articles/index', {
pageNumber: pageNumber || this.pageNumber,
portalSlug: this.$route.params.portalSlug,
locale: this.activeLocale,
locale: this.$route.params.locale,
status: this.status,
authorId: this.author,
categorySlug: this.selectedCategorySlug,
@@ -179,16 +152,6 @@ export default {
portalSlug: this.$route.params.portalSlug,
});
},
onChangeLocale(locale) {
this.$router.push({
name: 'list_all_locale_articles',
params: {
portalSlug: this.$route.params.portalSlug,
locale,
},
});
this.$emit('reload-locale');
},
},
};
</script>
@@ -50,7 +50,6 @@
:is-loading="uiFlags.isFetching"
:on-click-notification="openConversation"
:in-last-page="inLastPage"
@close="closeNotificationPanel"
/>
<div
v-if="records.length !== 0"
@@ -1,103 +0,0 @@
<template>
<div>
<woot-button
size="expanded"
color-scheme="secondary"
variant="link"
@click="onClickOpenNotification()"
>
<div
class="flex-row items-center p-2.5 leading-[1.4] border-b border-solid border-slate-50 dark:border-slate-700 flex w-full hover:bg-slate-75 dark:hover:bg-slate-900 hover:rounded-md"
>
<div
v-if="!notificationItem.read_at"
class="w-2 h-2 rounded-full bg-woot-500"
/>
<div v-else class="w-2 flex" />
<div
class="flex-col ml-2.5 overflow-hidden w-full flex justify-between"
>
<div class="flex justify-between">
<div class="items-center flex">
<span class="font-bold text-slate-800 dark:text-slate-100">
{{
`#${
notificationItem.primary_actor
? notificationItem.primary_actor.id
: $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`)
}`
}}
</span>
<span
class="text-xxs p-0.5 px-1 my-0 mx-2 bg-slate-50 dark:bg-slate-700 rounded-md"
>
{{
$t(
`NOTIFICATIONS_PAGE.TYPE_LABEL.${notificationItem.notification_type}`
)
}}
</span>
</div>
<div v-if="hasNotificationAssignee">
<thumbnail
:src="notificationAssigneeThumbnail"
size="16px"
:username="notificationAssigneeName"
/>
</div>
</div>
<div class="w-full flex">
<span
class="text-slate-700 dark:text-slate-200 font-normal overflow-hidden whitespace-nowrap text-ellipsis"
>
{{ notificationItem.push_message_title }}
</span>
</div>
<span
class="mt-1 text-slate-500 dark:text-slate-400 text-xxs font-semibold flex"
>
{{ dynamicTime(notificationItem.created_at) }}
</span>
</div>
</div>
</woot-button>
</div>
</template>
<script>
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import timeMixin from 'dashboard/mixins/time';
export default {
components: {
Thumbnail,
},
mixins: [timeMixin],
props: {
notificationItem: {
type: Object,
default: () => {},
},
},
computed: {
notificationAssignee() {
const { primary_actor: primaryActor } = this.notificationItem;
return primaryActor?.meta?.assignee;
},
hasNotificationAssignee() {
return !!this.notificationAssignee;
},
notificationAssigneeName() {
return this.notificationAssignee?.name || '';
},
notificationAssigneeThumbnail() {
return this.notificationAssignee?.thumbnail || '';
},
},
methods: {
onClickOpenNotification() {
this.$emit('open-notification', this.notificationItem);
},
},
};
</script>
@@ -1,12 +1,70 @@
<template>
<div class="flex-col py-2 px-2.5 overflow-auto h-full flex">
<notification-panel-item
<woot-button
v-for="notificationItem in notifications"
v-show="!isLoading"
:key="notificationItem.id"
:notification-item="notificationItem"
@open-notification="onClickNotification"
/>
size="expanded"
color-scheme="secondary"
variant="link"
@click="() => onClickNotification(notificationItem)"
>
<div
class="flex-row items-center p-2.5 leading-[1.4] border-b border-solid border-slate-50 dark:border-slate-700 flex w-full hover:bg-slate-75 dark:hover:bg-slate-900 hover:rounded-md"
>
<div
v-if="!notificationItem.read_at"
class="w-2 h-2 rounded-full bg-woot-500"
/>
<div v-else class="w-2 flex" />
<div
class="flex-col ml-2.5 overflow-hidden w-full flex justify-between"
>
<div class="flex justify-between">
<div class="items-center flex">
<span class="font-bold text-slate-800 dark:text-slate-100">
{{
`#${
notificationItem.primary_actor
? notificationItem.primary_actor.id
: $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`)
}`
}}
</span>
<span
class="text-xxs p-0.5 px-1 my-0 mx-2 bg-slate-50 dark:bg-slate-700 rounded-md"
>
{{
$t(
`NOTIFICATIONS_PAGE.TYPE_LABEL.${notificationItem.notification_type}`
)
}}
</span>
</div>
<div>
<thumbnail
v-if="hasAssignee(notificationItem)"
:src="notificationItem.primary_actor.meta.assignee.thumbnail"
size="16px"
:username="notificationItem.primary_actor.meta.assignee.name"
/>
</div>
</div>
<div class="w-full flex">
<span
class="text-slate-700 dark:text-slate-200 font-normal overflow-hidden whitespace-nowrap text-ellipsis"
>
{{ notificationItem.push_message_title }}
</span>
</div>
<span
class="mt-1 text-slate-500 dark:text-slate-400 text-xxs font-semibold flex"
>
{{ dynamicTime(notificationItem.created_at) }}
</span>
</div>
</div>
</woot-button>
<empty-state
v-if="showEmptyResult"
:title="$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.EMPTY_MESSAGE')"
@@ -36,16 +94,18 @@
<script>
import { mapGetters } from 'vuex';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import Spinner from 'shared/components/Spinner.vue';
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
import NotificationPanelItem from './NotificationPanelItem.vue';
import timeMixin from '../../../../mixins/time';
export default {
components: {
NotificationPanelItem,
Thumbnail,
Spinner,
EmptyState,
},
mixins: [timeMixin],
props: {
notifications: {
type: Array,
@@ -79,7 +139,9 @@ export default {
name: 'notifications_index',
});
}
this.$emit('close');
},
hasAssignee(notification) {
return notification.primary_actor.meta?.assignee;
},
},
};
@@ -1,7 +1,7 @@
/* eslint arrow-body-style: 0 */
import NotificationsView from './components/NotificationsView.vue';
import { frontendURL } from '../../../helper/URLHelper';
const SettingsWrapper = () => import('../settings/Wrapper.vue');
const NotificationsView = () => import('./components/NotificationsView.vue');
import SettingsWrapper from '../settings/Wrapper';
export const routes = [
{
@@ -1,6 +1,6 @@
import SettingsContent from '../Wrapper';
import Index from './Index.vue';
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
const Index = () => import('./Index.vue');
export default {
routes: [
@@ -1,8 +1,8 @@
import SettingsContent from '../Wrapper';
const Bot = () => import('./Index.vue');
const CsmlEditBot = () => import('./csml/Edit.vue');
const CsmlNewBot = () => import('./csml/New.vue');
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
export default {
routes: [
@@ -1,6 +1,6 @@
import SettingsContent from '../Wrapper';
import AgentHome from './Index';
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
const AgentHome = () => import('./Index.vue');
export default {
routes: [
@@ -1,6 +1,6 @@
import SettingsContent from '../Wrapper';
import AttributesHome from './Index';
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
const AttributesHome = () => import('./Index.vue');
export default {
routes: [
@@ -1,8 +1,7 @@
import SettingsContent from '../Wrapper';
import AuditLogsHome from './Index';
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
const AuditLogsHome = () => import('./Index.vue');
export default {
routes: [
{
@@ -1,6 +1,6 @@
import SettingsContent from '../Wrapper';
import Automation from './Index';
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
const Automation = () => import('./Index.vue');
export default {
routes: [
@@ -1,6 +1,6 @@
import SettingsContent from '../Wrapper';
import Index from './Index.vue';
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
const Index = () => import('./Index.vue');
export default {
routes: [
@@ -1,6 +1,6 @@
import Index from './Index';
import SettingsContent from '../Wrapper';
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
const Index = () => import('./Index.vue');
export default {
routes: [
@@ -20,7 +20,7 @@ export default {
path: 'ongoing',
name: 'settings_account_campaigns',
roles: ['administrator'],
component: Index,
component: { ...Index },
},
],
},
@@ -36,7 +36,7 @@ export default {
path: 'one_off',
name: 'one_off',
roles: ['administrator'],
component: Index,
component: { ...Index },
},
],
},
@@ -1,8 +1,7 @@
import SettingsContent from '../Wrapper';
import CannedHome from './Index';
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
const CannedHome = () => import('./Index.vue');
export default {
routes: [
{
@@ -1,14 +1,13 @@
/* eslint arrow-body-style: 0 */
import { frontendURL } from '../../../../helper/URLHelper';
import SettingsContent from '../Wrapper';
import Settings from './Settings';
import InboxHome from './Index';
import InboxChannel from './InboxChannels';
import ChannelList from './ChannelList';
import channelFactory from './channel-factory';
const SettingsContent = () => import('../Wrapper.vue');
const InboxHome = () => import('./Index.vue');
const Settings = () => import('./Settings.vue');
const InboxChannel = () => import('./InboxChannels.vue');
const ChannelList = () => import('./ChannelList.vue');
const AddAgents = () => import('./AddAgents.vue');
const FinishSetup = () => import('./FinishSetup.vue');
import AddAgents from './AddAgents';
import FinishSetup from './FinishSetup';
import { frontendURL } from '../../../../helper/URLHelper';
export default {
routes: [
@@ -1,7 +1,7 @@
import Index from './Index';
import SettingsContent from '../Wrapper';
import IntegrationHooks from './IntegrationHooks';
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
const IntegrationHooks = () => import('./IntegrationHooks.vue');
const Index = () => import('./Index.vue');
export default {
routes: [
@@ -1,12 +1,11 @@
import Index from './Index';
import SettingsContent from '../Wrapper';
import Webhook from './Webhooks/Index';
import DashboardApps from './DashboardApps/Index';
import ShowIntegration from './ShowIntegration';
import Slack from './Slack';
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
const Webhook = () => import('./Webhooks/Index.vue');
const DashboardApps = () => import('./DashboardApps/Index.vue');
const ShowIntegration = () => import('./ShowIntegration.vue');
const Slack = () => import('./Slack.vue');
const Index = () => import('./Index.vue');
export default {
routes: [
{
@@ -1,8 +1,7 @@
import SettingsContent from '../Wrapper';
import Index from './Index';
import { frontendURL } from '../../../../helper/URLHelper';
const SettingsContent = () => import('../Wrapper.vue');
const Index = () => import('./Index.vue');
export default {
routes: [
{

Some files were not shown because too many files have changed in this diff Show More