Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e66c706ff0 | ||
|
|
c3e90d4cbc |
@@ -157,7 +157,7 @@ jobs:
|
||||
- run:
|
||||
name: Code Climate Test Coverage
|
||||
command: |
|
||||
~/tmp/cc-test-reporter format-coverage -t lcov -o "coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json"
|
||||
~/tmp/cc-test-reporter format-coverage -t lcov -o coverage/codeclimate.frontend_$CIRCLE_NODE_INDEX.json buildreports/lcov.info
|
||||
|
||||
- persist_to_workspace:
|
||||
root: coverage
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
name: Run Size Limit Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: yarn
|
||||
run: yarn install
|
||||
|
||||
- name: Strip enterprise code
|
||||
run: |
|
||||
rm -rf enterprise
|
||||
rm -rf spec/enterprise
|
||||
|
||||
- name: Run asset compile
|
||||
run: bundle exec rake assets:precompile
|
||||
|
||||
- name: Size Check
|
||||
run: yarn run size
|
||||
@@ -75,7 +75,6 @@ gem 'jwt'
|
||||
gem 'pundit'
|
||||
# super admin
|
||||
gem 'administrate'
|
||||
gem 'administrate-field-active_storage'
|
||||
|
||||
##--- gems for pubsub service ---##
|
||||
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
|
||||
|
||||
+68
-73
@@ -33,70 +33,70 @@ GIT
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actioncable (7.0.4.3)
|
||||
actionpack (= 7.0.4.3)
|
||||
activesupport (= 7.0.4.3)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activestorage (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionmailbox (7.0.4.3)
|
||||
actionpack (= 7.0.4.3)
|
||||
activejob (= 7.0.4.3)
|
||||
activerecord (= 7.0.4.3)
|
||||
activestorage (= 7.0.4.3)
|
||||
activesupport (= 7.0.4.3)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
actionview (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionmailer (7.0.4.3)
|
||||
actionpack (= 7.0.4.3)
|
||||
actionview (= 7.0.4.3)
|
||||
activejob (= 7.0.4.3)
|
||||
activesupport (= 7.0.4.3)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (7.0.5)
|
||||
actionview (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
rack (~> 2.0, >= 2.2.4)
|
||||
actionpack (7.0.4.3)
|
||||
actionview (= 7.0.4.3)
|
||||
activesupport (= 7.0.4.3)
|
||||
rack (~> 2.0, >= 2.2.0)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activestorage (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actiontext (7.0.4.3)
|
||||
actionpack (= 7.0.4.3)
|
||||
activerecord (= 7.0.4.3)
|
||||
activestorage (= 7.0.4.3)
|
||||
activesupport (= 7.0.4.3)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
actionview (7.0.4.3)
|
||||
activesupport (= 7.0.4.3)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
active_record_query_trace (1.8)
|
||||
activejob (7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activejob (7.0.4.3)
|
||||
activesupport (= 7.0.4.3)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activerecord (7.0.5)
|
||||
activemodel (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activemodel (7.0.4.3)
|
||||
activesupport (= 7.0.4.3)
|
||||
activerecord (7.0.4.3)
|
||||
activemodel (= 7.0.4.3)
|
||||
activesupport (= 7.0.4.3)
|
||||
activerecord-import (1.4.1)
|
||||
activerecord (>= 4.2)
|
||||
activestorage (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
activestorage (7.0.4.3)
|
||||
actionpack (= 7.0.4.3)
|
||||
activejob (= 7.0.4.3)
|
||||
activerecord (= 7.0.4.3)
|
||||
activesupport (= 7.0.4.3)
|
||||
marcel (~> 1.0)
|
||||
mini_mime (>= 1.1.0)
|
||||
activesupport (7.0.5)
|
||||
activesupport (7.0.4.3)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
@@ -113,9 +113,6 @@ GEM
|
||||
kaminari (>= 1.0)
|
||||
sassc-rails (~> 2.1)
|
||||
selectize-rails (~> 0.6)
|
||||
administrate-field-active_storage (0.4.2)
|
||||
administrate (>= 0.2.2)
|
||||
rails (>= 7.0)
|
||||
annotate (3.2.0)
|
||||
activerecord (>= 3.2, < 8.0)
|
||||
rake (>= 10.4, < 14.0)
|
||||
@@ -356,7 +353,7 @@ GEM
|
||||
mini_mime (>= 1.0.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.1)
|
||||
i18n (1.13.0)
|
||||
concurrent-ruby (~> 1.0)
|
||||
image_processing (1.12.2)
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
@@ -423,9 +420,9 @@ GEM
|
||||
activesupport (>= 4)
|
||||
railties (>= 4)
|
||||
request_store (~> 1.0)
|
||||
loofah (2.21.3)
|
||||
loofah (2.20.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.12.0)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.8.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
net-imap
|
||||
@@ -465,14 +462,14 @@ GEM
|
||||
sidekiq
|
||||
newrelic_rpm (8.16.0)
|
||||
nio4r (2.5.9)
|
||||
nokogiri (1.15.2)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
nokogiri (1.14.3)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.15.2-arm64-darwin)
|
||||
nokogiri (1.14.3-arm64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.15.2-x86_64-darwin)
|
||||
nokogiri (1.14.3-x86_64-darwin)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.15.2-x86_64-linux)
|
||||
nokogiri (1.14.3-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oauth (1.1.0)
|
||||
oauth-tty (~> 1.0, >= 1.0.1)
|
||||
@@ -525,7 +522,7 @@ GEM
|
||||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.7.0)
|
||||
racc (1.6.2)
|
||||
rack (2.2.7)
|
||||
rack-attack (6.6.1)
|
||||
rack (>= 1.0, < 3)
|
||||
@@ -540,29 +537,28 @@ GEM
|
||||
rack-test (2.1.0)
|
||||
rack (>= 1.3)
|
||||
rack-timeout (0.6.3)
|
||||
rails (7.0.5)
|
||||
actioncable (= 7.0.5)
|
||||
actionmailbox (= 7.0.5)
|
||||
actionmailer (= 7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
actiontext (= 7.0.5)
|
||||
actionview (= 7.0.5)
|
||||
activejob (= 7.0.5)
|
||||
activemodel (= 7.0.5)
|
||||
activerecord (= 7.0.5)
|
||||
activestorage (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
rails (7.0.4.3)
|
||||
actioncable (= 7.0.4.3)
|
||||
actionmailbox (= 7.0.4.3)
|
||||
actionmailer (= 7.0.4.3)
|
||||
actionpack (= 7.0.4.3)
|
||||
actiontext (= 7.0.4.3)
|
||||
actionview (= 7.0.4.3)
|
||||
activejob (= 7.0.4.3)
|
||||
activemodel (= 7.0.4.3)
|
||||
activerecord (= 7.0.4.3)
|
||||
activestorage (= 7.0.4.3)
|
||||
activesupport (= 7.0.4.3)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.0.5)
|
||||
railties (= 7.0.4.3)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.6.0)
|
||||
loofah (~> 2.21)
|
||||
nokogiri (~> 1.14)
|
||||
railties (7.0.5)
|
||||
actionpack (= 7.0.5)
|
||||
activesupport (= 7.0.5)
|
||||
rails-html-sanitizer (1.5.0)
|
||||
loofah (~> 2.19, >= 2.19.1)
|
||||
railties (7.0.4.3)
|
||||
actionpack (= 7.0.4.3)
|
||||
activesupport (= 7.0.4.3)
|
||||
method_source
|
||||
rake (>= 12.2)
|
||||
thor (~> 1.0)
|
||||
@@ -726,8 +722,8 @@ GEM
|
||||
stripe (8.5.0)
|
||||
telephone_number (1.4.20)
|
||||
test-prof (1.2.1)
|
||||
thor (1.2.2)
|
||||
tilt (2.2.0)
|
||||
thor (1.2.1)
|
||||
tilt (2.1.0)
|
||||
time_diff (0.3.0)
|
||||
activesupport
|
||||
i18n
|
||||
@@ -802,7 +798,6 @@ DEPENDENCIES
|
||||
activerecord-import
|
||||
acts-as-taggable-on
|
||||
administrate
|
||||
administrate-field-active_storage
|
||||
annotate
|
||||
attr_extras
|
||||
audited (~> 5.3)
|
||||
@@ -921,4 +916,4 @@ RUBY VERSION
|
||||
ruby 3.2.2p185
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.6
|
||||
2.4.10
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
//= link_tree ../images
|
||||
//= link administrate/application.css
|
||||
//= link administrate/application.js
|
||||
//= link administrate-field-active_storage/application.css
|
||||
//= link dashboardChart.js
|
||||
//= link secretField.js
|
||||
|
||||
@@ -96,9 +96,10 @@ class V2::ReportBuilder
|
||||
|
||||
def conversations
|
||||
@open_conversations = scope.conversations.where(account_id: @account.id).open
|
||||
first_response_count = @account.reporting_events.where(name: 'first_response', conversation_id: @open_conversations.pluck('id')).count
|
||||
metric = {
|
||||
open: @open_conversations.count,
|
||||
unattended: @open_conversations.unattended.count
|
||||
unattended: @open_conversations.count - first_response_count
|
||||
}
|
||||
metric[:unassigned] = @open_conversations.unassigned.count if params[:type].equal?(:account)
|
||||
metric
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::Contacts::BaseController
|
||||
include HmacConcern
|
||||
before_action :ensure_inbox, only: [:create]
|
||||
|
||||
def create
|
||||
@contact_inbox = ContactInboxBuilder.new(
|
||||
contact: @contact,
|
||||
inbox: @inbox,
|
||||
source_id: params[:source_id],
|
||||
hmac_verified: hmac_verified?
|
||||
source_id: params[:source_id]
|
||||
).perform
|
||||
end
|
||||
|
||||
|
||||
@@ -42,12 +42,6 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
head :ok
|
||||
end
|
||||
|
||||
def export
|
||||
column_names = params['column_names']
|
||||
Account::ContactsExportJob.perform_later(Current.account.id, column_names)
|
||||
head :ok, message: I18n.t('errors.contacts.export.success')
|
||||
end
|
||||
|
||||
# returns online contacts
|
||||
def active
|
||||
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseController
|
||||
include Events::Types
|
||||
include DateRangeHelper
|
||||
include HmacConcern
|
||||
|
||||
before_action :conversation, except: [:index, :meta, :search, :create, :filter]
|
||||
before_action :inbox, :contact, :contact_inbox, only: [:create]
|
||||
@@ -105,6 +104,9 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
end
|
||||
|
||||
def set_conversation_status
|
||||
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
|
||||
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
|
||||
# status = params[:status] == 'bot' ? 'pending' : params[:status]
|
||||
@conversation.status = params[:status]
|
||||
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
|
||||
end
|
||||
@@ -150,8 +152,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
|
||||
ContactInboxBuilder.new(
|
||||
contact: @contact,
|
||||
inbox: @inbox,
|
||||
source_id: params[:source_id],
|
||||
hmac_verified: hmac_verified?
|
||||
source_id: params[:source_id]
|
||||
).perform
|
||||
end
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def cache_keys
|
||||
expires_in 10.seconds, public: false, stale_while_revalidate: 5.minutes
|
||||
render json: { cache_keys: get_cache_keys }, status: :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module HmacConcern
|
||||
def hmac_verified?
|
||||
ActiveModel::Type::Boolean.new.cast(params[:hmac_verified]).present?
|
||||
end
|
||||
end
|
||||
@@ -43,6 +43,8 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
|
||||
end
|
||||
|
||||
def render_article_content(content)
|
||||
ChatwootMarkdownRenderer.new(content).render_article
|
||||
# rubocop:disable Rails/OutputSafety
|
||||
CommonMarker.render_html(content).html_safe
|
||||
# rubocop:enable Rails/OutputSafety
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,8 +6,6 @@ class Public::Api::V1::Portals::BaseController < PublicController
|
||||
def set_locale(&)
|
||||
switch_locale_with_portal(&) if params[:locale].present?
|
||||
switch_locale_with_article(&) if params[:article_slug].present?
|
||||
|
||||
yield
|
||||
end
|
||||
|
||||
def switch_locale_with_portal(&)
|
||||
|
||||
@@ -50,13 +50,6 @@ class SuperAdmin::AccountsController < SuperAdmin::ApplicationController
|
||||
# rubocop:enable Rails/I18nLocaleTexts
|
||||
end
|
||||
|
||||
def reset_cache
|
||||
requested_resource.reset_cache_keys
|
||||
# rubocop:disable Rails/I18nLocaleTexts
|
||||
redirect_back(fallback_location: [namespace, requested_resource], notice: 'Cache keys cleared')
|
||||
# rubocop:enable Rails/I18nLocaleTexts
|
||||
end
|
||||
|
||||
def destroy
|
||||
account = Account.find(params[:id])
|
||||
|
||||
|
||||
@@ -41,14 +41,4 @@ class SuperAdmin::AgentBotsController < SuperAdmin::ApplicationController
|
||||
|
||||
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
|
||||
# for more information
|
||||
|
||||
def destroy_avatar
|
||||
avatar = requested_resource.avatar
|
||||
avatar.purge
|
||||
redirect_back(fallback_location: super_admin_agent_bots_path)
|
||||
end
|
||||
|
||||
def scoped_resource
|
||||
resource_class.with_attached_avatar
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,17 +5,6 @@ class SuperAdmin::InstanceStatusesController < SuperAdmin::ApplicationController
|
||||
sha
|
||||
postgres_status
|
||||
redis_metrics
|
||||
chatwoot_edition
|
||||
end
|
||||
|
||||
def chatwoot_edition
|
||||
@metrics['Chatwoot edition'] = if ChatwootApp.enterprise?
|
||||
'Enterprise'
|
||||
elsif ChatwootApp.custom?
|
||||
'Custom'
|
||||
else
|
||||
'Community'
|
||||
end
|
||||
end
|
||||
|
||||
def chatwoot_version
|
||||
|
||||
@@ -45,17 +45,6 @@ class SuperAdmin::UsersController < SuperAdmin::ApplicationController
|
||||
# empty values into nil values. It uses other APIs such as `resource_class`
|
||||
# and `dashboard`:
|
||||
#
|
||||
|
||||
def destroy_avatar
|
||||
avatar = requested_resource.avatar
|
||||
avatar.purge
|
||||
redirect_back(fallback_location: super_admin_users_path)
|
||||
end
|
||||
|
||||
def scoped_resource
|
||||
resource_class.with_attached_avatar
|
||||
end
|
||||
|
||||
def resource_params
|
||||
permitted_params = super
|
||||
permitted_params.delete(:password) if permitted_params[:password].blank?
|
||||
|
||||
@@ -10,11 +10,6 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
||||
ATTRIBUTE_TYPES = {
|
||||
access_token: Field::HasOne,
|
||||
avatar_url: AvatarField,
|
||||
avatar: Field::ActiveStorage.with_options(
|
||||
destroy_url: proc do |_namespace, _resource, attachment|
|
||||
[:avatar_super_admin_agent_bot, { attachment_id: attachment.id }]
|
||||
end
|
||||
),
|
||||
id: Field::Number,
|
||||
name: Field::String,
|
||||
account: Field::BelongsTo.with_options(searchable: true, searchable_field: 'name', order: 'id DESC'),
|
||||
@@ -41,7 +36,6 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
||||
# an array of attributes that will be displayed on the model's show page.
|
||||
SHOW_PAGE_ATTRIBUTES = %i[
|
||||
id
|
||||
avatar_url
|
||||
account
|
||||
name
|
||||
description
|
||||
@@ -53,7 +47,6 @@ class AgentBotDashboard < Administrate::BaseDashboard
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
name
|
||||
avatar
|
||||
account
|
||||
description
|
||||
outgoing_url
|
||||
|
||||
@@ -11,11 +11,6 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
account_users: Field::HasMany,
|
||||
id: Field::Number,
|
||||
avatar_url: AvatarField,
|
||||
avatar: Field::ActiveStorage.with_options(
|
||||
destroy_url: proc do |_namespace, _resource, attachment|
|
||||
[:avatar_super_admin_user, { attachment_id: attachment.id }]
|
||||
end
|
||||
),
|
||||
provider: Field::String,
|
||||
uid: Field::String,
|
||||
password: Field::Password,
|
||||
@@ -74,7 +69,6 @@ class UserDashboard < Administrate::BaseDashboard
|
||||
# on the model's form (`new` and `edit`) pages.
|
||||
FORM_ATTRIBUTES = %i[
|
||||
name
|
||||
avatar
|
||||
display_name
|
||||
email
|
||||
password
|
||||
|
||||
@@ -102,7 +102,7 @@ class ConversationFinder
|
||||
when 'participating'
|
||||
@conversations = current_user.participating_conversations.where(account_id: current_account.id)
|
||||
when 'unattended'
|
||||
@conversations = @conversations.unattended
|
||||
@conversations = @conversations.where(first_reply_created_at: nil)
|
||||
end
|
||||
@conversations
|
||||
end
|
||||
|
||||
@@ -7,6 +7,8 @@ module MessageFormatHelper
|
||||
end
|
||||
|
||||
def render_message_content(message_content)
|
||||
ChatwootMarkdownRenderer.new(message_content).render_message
|
||||
# rubocop:disable Rails/OutputSafety
|
||||
CommonMarker.render_html(message_content).html_safe
|
||||
# rubocop:enable Rails/OutputSafety
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,9 +29,7 @@ module ReportHelper
|
||||
end
|
||||
|
||||
def resolutions_count
|
||||
object_scope = scope.reporting_events.joins(:conversation).select(:conversation_id).where(account_id: account.id, name: :conversation_resolved,
|
||||
conversations: { status: :resolved }).distinct
|
||||
(get_grouped_values object_scope).count
|
||||
(get_grouped_values scope.conversations.where(account_id: account.id).resolved).count
|
||||
end
|
||||
|
||||
def avg_first_response_time
|
||||
|
||||
@@ -28,6 +28,7 @@ import UpdateBanner from './components/app/UpdateBanner.vue';
|
||||
import vueActionCable from './helper/actionCable';
|
||||
import WootSnackbarBox from './components/SnackbarContainer';
|
||||
import rtlMixin from 'shared/mixins/rtlMixin';
|
||||
import * as chrono from 'chrono-node';
|
||||
import {
|
||||
registerSubscription,
|
||||
verifyServiceWorkerExistence,
|
||||
@@ -81,6 +82,25 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.setLocale(window.chatwootConfig.selectedLocale);
|
||||
console.log(
|
||||
'An appointment on Sep 12-13unted --->',
|
||||
chrono.parseDate('An appointment on Sep 12-13')
|
||||
);
|
||||
console.log('tomorrow --->', chrono.parseDate('tomorrow'));
|
||||
console.log(
|
||||
'tomorrow at 9.00AM --->',
|
||||
chrono.parseDate('tomorrow at 9.00AM')
|
||||
);
|
||||
console.log('may 31 --->', chrono.parseDate('may 31'));
|
||||
console.log('12.30 --->', chrono.parseDate('12.30'));
|
||||
console.log('9.00AM --->', chrono.parseDate('9.00AM'));
|
||||
console.log('tuesday --->', chrono.parseDate('tuesday'));
|
||||
console.log('after 5 days --->', chrono.parseDate('after 5 days'));
|
||||
console.log('tom 13.00 --->', chrono.parseDate('tom 13.00'));
|
||||
console.log(
|
||||
'next month 10.00AM --->',
|
||||
chrono.parseDate('next month 10.00AM')
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
setLocale(locale) {
|
||||
@@ -115,6 +135,11 @@ export default {
|
||||
|
||||
<style lang="scss">
|
||||
@import './assets/scss/app';
|
||||
.update-banner {
|
||||
height: var(--space-larger);
|
||||
align-items: center;
|
||||
font-size: var(--font-size-small) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
|
||||
@@ -75,10 +75,6 @@ class ContactAPI extends ApiClient {
|
||||
destroyAvatar(contactId) {
|
||||
return axios.delete(`${this.url}/${contactId}/avatar`);
|
||||
}
|
||||
|
||||
exportContacts() {
|
||||
return axios.get(`${this.url}/export`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ContactAPI();
|
||||
|
||||
@@ -127,10 +127,6 @@ class ConversationApi extends ApiClient {
|
||||
user_ids: userIds,
|
||||
});
|
||||
}
|
||||
|
||||
getAllAttachments(conversationId) {
|
||||
return axios.get(`${this.url}/${conversationId}/attachments`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new ConversationApi();
|
||||
|
||||
@@ -210,12 +210,5 @@ describe('#ConversationAPI', () => {
|
||||
{ params: { page: payload.page } }
|
||||
);
|
||||
});
|
||||
|
||||
it('#getAllAttachments', () => {
|
||||
conversationAPI.getAllAttachments(1);
|
||||
expect(context.axiosMock.get).toHaveBeenCalledWith(
|
||||
'/api/v1/conversations/1/attachments'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,13 +29,13 @@
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
max-height: 80vh;
|
||||
max-width: 80vw;
|
||||
max-height: 90%;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.modal-video {
|
||||
max-height: 80vh;
|
||||
max-width: 80vw;
|
||||
max-height: 75vh;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&::before {
|
||||
@@ -53,6 +53,16 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.video {
|
||||
.modal-container {
|
||||
width: auto;
|
||||
|
||||
.modal--close {
|
||||
z-index: var(--z-index-low);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversations-list-wrap {
|
||||
@@ -390,3 +400,4 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,14 +47,6 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasActiveFolders">
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.EDIT.EDIT_BUTTON')"
|
||||
size="tiny"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
@click="onToggleAdvanceFiltersModal"
|
||||
/>
|
||||
<woot-button
|
||||
v-tooltip.top-end="$t('FILTER.CUSTOM_VIEWS.DELETE.DELETE_BUTTON')"
|
||||
size="tiny"
|
||||
@@ -176,11 +168,8 @@
|
||||
v-if="showAdvancedFilters"
|
||||
:initial-filter-types="advancedFilterTypes"
|
||||
:initial-applied-filters="appliedFilter"
|
||||
:active-folder-name="activeFolderName"
|
||||
:on-close="closeAdvanceFiltersModal"
|
||||
:is-folder-view="hasActiveFolders"
|
||||
@applyFilter="onApplyFilter"
|
||||
@updateFolder="onUpdateSavedFilter"
|
||||
/>
|
||||
</woot-modal>
|
||||
</div>
|
||||
@@ -204,9 +193,6 @@ import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCust
|
||||
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import filterMixin from 'shared/mixins/filterMixin';
|
||||
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
import countries from 'shared/constants/countries';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
|
||||
import {
|
||||
hasPressedAltAndJKey,
|
||||
@@ -303,11 +289,6 @@ export default {
|
||||
appliedFilters: 'getAppliedConversationFilters',
|
||||
folders: 'customViews/getCustomViews',
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
agentList: 'agents/getAgents',
|
||||
teamsList: 'teams/getTeams',
|
||||
inboxesList: 'inboxes/getInboxes',
|
||||
campaigns: 'campaigns/getAllCampaigns',
|
||||
labels: 'labels/getLabels',
|
||||
}),
|
||||
hasAppliedFilters() {
|
||||
return this.appliedFilters.length !== 0;
|
||||
@@ -470,9 +451,6 @@ export default {
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
activeFolderName() {
|
||||
return this.activeFolder?.name;
|
||||
},
|
||||
activeTeam() {
|
||||
if (this.teamId) {
|
||||
return this.$store.getters['teams/getTeam'](this.teamId);
|
||||
@@ -505,7 +483,9 @@ export default {
|
||||
this.resetAndFetchData();
|
||||
},
|
||||
activeFolder() {
|
||||
this.resetAndFetchData();
|
||||
if (!this.hasAppliedFilters) {
|
||||
this.resetAndFetchData();
|
||||
}
|
||||
},
|
||||
chatLists() {
|
||||
this.chatsOnView = this.conversationList;
|
||||
@@ -516,10 +496,6 @@ export default {
|
||||
this.$store.dispatch('setChatSortFilter', this.activeSortBy);
|
||||
this.resetAndFetchData();
|
||||
|
||||
if (this.hasActiveFolders) {
|
||||
this.$store.dispatch('campaigns/get');
|
||||
}
|
||||
|
||||
bus.$on('fetch_conversation_stats', () => {
|
||||
this.$store.dispatch('conversationStats/get', this.conversationFilters);
|
||||
});
|
||||
@@ -532,15 +508,6 @@ export default {
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
this.fetchFilteredConversations(payload);
|
||||
},
|
||||
onUpdateSavedFilter(payload, folderName) {
|
||||
const payloadData = {
|
||||
...this.activeFolder,
|
||||
name: folderName,
|
||||
query: filterQueryGenerator(payload),
|
||||
};
|
||||
this.$store.dispatch('customViews/update', payloadData);
|
||||
this.closeAdvanceFiltersModal();
|
||||
},
|
||||
onClickOpenAddFoldersModal() {
|
||||
this.showAddFoldersModal = true;
|
||||
},
|
||||
@@ -554,70 +521,15 @@ export default {
|
||||
this.showDeleteFoldersModal = false;
|
||||
},
|
||||
onToggleAdvanceFiltersModal() {
|
||||
if (!this.hasAppliedFilters && !this.hasActiveFolders) {
|
||||
if (!this.hasAppliedFilters) {
|
||||
this.initializeExistingFilterToModal();
|
||||
}
|
||||
if (this.hasActiveFolders) {
|
||||
this.initializeFolderToFilterModal(this.activeFolder);
|
||||
}
|
||||
this.showAdvancedFilters = true;
|
||||
},
|
||||
closeAdvanceFiltersModal() {
|
||||
this.showAdvancedFilters = false;
|
||||
this.appliedFilter = [];
|
||||
},
|
||||
setParamsForEditFolderModal() {
|
||||
// Here we are setting the params for edit folder modal to show the existing values.
|
||||
|
||||
// For agent, team, inboxes,and campaigns we get only the id's from the query.
|
||||
// So we are mapping the id's to the actual values.
|
||||
|
||||
// For labels we get the name of the label from the query.
|
||||
// If we delete the label from the label list then we will not be able to show the label name.
|
||||
|
||||
// For custom attributes we get only attribute key.
|
||||
// So we are mapping it to find the input type of the attribute to show in the edit folder modal.
|
||||
const params = {
|
||||
agents: this.agentList,
|
||||
teams: this.teamsList,
|
||||
inboxes: this.inboxesList,
|
||||
labels: this.labels,
|
||||
campaigns: this.campaigns,
|
||||
languages: languages,
|
||||
countries: countries,
|
||||
filterTypes: advancedFilterTypes,
|
||||
allCustomAttributes: this.$store.getters[
|
||||
'attributes/getAttributesByModel'
|
||||
]('conversation_attribute'),
|
||||
};
|
||||
return params;
|
||||
},
|
||||
initializeFolderToFilterModal(activeFolder) {
|
||||
// Here we are setting the params for edit folder modal.
|
||||
// To show the existing values. when we click on edit folder button.
|
||||
|
||||
// Here we get the query from the active folder.
|
||||
// And we are mapping the query to the actual values.
|
||||
// To show in the edit folder modal by the help of generateValuesForEditCustomViews helper.
|
||||
const query = activeFolder?.query?.payload;
|
||||
if (!Array.isArray(query)) return;
|
||||
|
||||
this.appliedFilter.push(
|
||||
...query.map(filter => ({
|
||||
attribute_key: filter.attribute_key,
|
||||
attribute_model: filter.attribute_model,
|
||||
filter_operator: filter.filter_operator,
|
||||
values: Array.isArray(filter.values)
|
||||
? generateValuesForEditCustomViews(
|
||||
filter,
|
||||
this.setParamsForEditFolderModal()
|
||||
)
|
||||
: [],
|
||||
query_operator: filter.query_operator,
|
||||
custom_attribute_type: filter.custom_attribute_type,
|
||||
}))
|
||||
);
|
||||
},
|
||||
getKeyboardListenerParams() {
|
||||
const allConversations = this.$refs.activeConversation.querySelectorAll(
|
||||
'div.conversations-list div.conversation'
|
||||
@@ -663,7 +575,6 @@ export default {
|
||||
}
|
||||
},
|
||||
resetAndFetchData() {
|
||||
this.appliedFilter = [];
|
||||
this.resetBulkActions();
|
||||
this.$store.dispatch('conversationPage/reset');
|
||||
this.$store.dispatch('emptyAllConversations');
|
||||
@@ -676,6 +587,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.fetchConversations();
|
||||
this.appliedFilter = [];
|
||||
},
|
||||
fetchConversations() {
|
||||
this.$store
|
||||
|
||||
@@ -115,12 +115,10 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { required, url } from 'vuelidate/lib/validators';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import MultiselectDropdown from 'shared/components/ui/MultiselectDropdown.vue';
|
||||
import { isValidURL } from '../helper/URLHelper';
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -143,20 +141,12 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
displayValue() {
|
||||
if (this.isAttributeTypeDate) {
|
||||
return new Date(this.value || new Date()).toLocaleDateString();
|
||||
}
|
||||
formattedValue() {
|
||||
if (this.isAttributeTypeCheckbox) {
|
||||
return this.value === 'false' ? false : this.value;
|
||||
}
|
||||
return this.value;
|
||||
},
|
||||
formattedValue() {
|
||||
return this.isAttributeTypeDate
|
||||
? format(this.value ? new Date(this.value) : new Date(), DATE_FORMAT)
|
||||
: this.value;
|
||||
},
|
||||
listOptions() {
|
||||
return this.values.map((value, index) => ({
|
||||
id: index + 1,
|
||||
@@ -197,11 +187,20 @@ export default {
|
||||
}
|
||||
return this.$t('CUSTOM_ATTRIBUTES.VALIDATIONS.REQUIRED');
|
||||
},
|
||||
displayValue() {
|
||||
if (this.attributeType === 'date' && this.editedValue) {
|
||||
return this.editedValue
|
||||
.split('-')
|
||||
.reverse()
|
||||
.join('-');
|
||||
}
|
||||
return this.editedValue;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
this.isEditing = false;
|
||||
this.editedValue = this.formattedValue;
|
||||
this.editedValue = this.value;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -246,16 +245,12 @@ export default {
|
||||
}
|
||||
},
|
||||
onUpdate() {
|
||||
const updatedValue =
|
||||
this.attributeType === 'date'
|
||||
? parseISO(this.editedValue)
|
||||
: this.editedValue;
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
return;
|
||||
}
|
||||
this.isEditing = false;
|
||||
this.$emit('update', this.attributeKey, updatedValue);
|
||||
this.$emit('update', this.attributeKey, this.editedValue);
|
||||
},
|
||||
onDelete() {
|
||||
this.isEditing = false;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<banner
|
||||
v-if="shouldShowBanner"
|
||||
class="update-banner"
|
||||
color-scheme="primary"
|
||||
:banner-message="bannerMessage"
|
||||
href-link="https://github.com/chatwoot/chatwoot/releases"
|
||||
|
||||
@@ -11,29 +11,26 @@
|
||||
{{ hrefLinkText }}
|
||||
</a>
|
||||
</span>
|
||||
<div class="actions">
|
||||
<woot-button
|
||||
v-if="hasActionButton"
|
||||
size="tiny"
|
||||
icon="arrow-right"
|
||||
:variant="actionButtonVariant"
|
||||
color-scheme="primary"
|
||||
class-names="banner-action__button"
|
||||
@click="onClick"
|
||||
>
|
||||
{{ actionButtonLabel }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="hasCloseButton"
|
||||
size="tiny"
|
||||
:color-scheme="colorScheme"
|
||||
icon="dismiss-circle"
|
||||
class-names="banner-action__button"
|
||||
@click="onClickClose"
|
||||
>
|
||||
{{ $t('GENERAL_SETTINGS.DISMISS') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<woot-button
|
||||
v-if="hasActionButton"
|
||||
size="small"
|
||||
variant="link"
|
||||
icon="arrow-right"
|
||||
color-scheme="primary"
|
||||
class-names="banner-action__button"
|
||||
@click="onClick"
|
||||
>
|
||||
{{ actionButtonLabel }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="hasCloseButton"
|
||||
size="small"
|
||||
variant="link"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss-circle"
|
||||
class-names="banner-action__button"
|
||||
@click="onClickClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -56,10 +53,6 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
actionButtonVariant: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
actionButtonLabel: {
|
||||
type: String,
|
||||
default: '',
|
||||
@@ -75,12 +68,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
bannerClasses() {
|
||||
const classList = [this.colorScheme, `banner-align-${this.align}`];
|
||||
|
||||
if (this.hasActionButton || this.hasCloseButton) {
|
||||
classList.push('has-button');
|
||||
}
|
||||
return classList;
|
||||
return [this.colorScheme];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -96,26 +84,17 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.banner {
|
||||
--x-padding: var(--space-normal);
|
||||
--y-padding: var(--space-slab);
|
||||
|
||||
display: flex;
|
||||
gap: var(--x-padding);
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-mini);
|
||||
padding: var(--y-padding) var(--x-padding);
|
||||
padding: var(--space-slab) var(--space-normal);
|
||||
justify-content: center;
|
||||
position: sticky;
|
||||
|
||||
&.primary {
|
||||
background: var(--w-500);
|
||||
.banner-action__button {
|
||||
background: var(--w-600);
|
||||
border: none;
|
||||
color: var(--white);
|
||||
|
||||
&:hover {
|
||||
background: var(--w-800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,16 +107,7 @@ export default {
|
||||
}
|
||||
|
||||
&.alert {
|
||||
background: var(--r-500);
|
||||
.banner-action__button {
|
||||
background: var(--r-700);
|
||||
border: none;
|
||||
color: var(--white);
|
||||
|
||||
&:hover {
|
||||
background: var(--r-800);
|
||||
}
|
||||
}
|
||||
background: var(--r-400);
|
||||
}
|
||||
|
||||
&.warning {
|
||||
@@ -163,6 +133,8 @@ export default {
|
||||
}
|
||||
|
||||
.banner-action__button {
|
||||
margin: 0 var(--space-smaller);
|
||||
|
||||
::v-deep .button__content {
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -172,11 +144,5 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: var(--space-smaller);
|
||||
right: var(--y-padding);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+11
-79
@@ -1,21 +1,9 @@
|
||||
<template>
|
||||
<div class="column">
|
||||
<woot-modal-header :header-title="filterModalHeaderTitle">
|
||||
<p>{{ filterModalSubTitle }}</p>
|
||||
<woot-modal-header :header-title="$t('FILTER.TITLE')">
|
||||
<p>{{ $t('FILTER.SUBTITLE') }}</p>
|
||||
</woot-modal-header>
|
||||
<div class="column modal-content">
|
||||
<div v-if="isFolderView" class="columns">
|
||||
<label class="input-label" :class="{ error: !activeFolderNewName }">
|
||||
{{ $t('FILTER.FOLDER_LABEL') }}
|
||||
<input v-model="activeFolderNewName" type="text" class="name-input" />
|
||||
<span v-if="!activeFolderNewName" class="message">
|
||||
{{ $t('FILTER.EMPTY_VALUE_ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-label">
|
||||
{{ $t('FILTER.FOLDER_QUERY_LABEL') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="row modal-content">
|
||||
<div class="medium-12 columns filters-wrap">
|
||||
<filter-input-box
|
||||
v-for="(filter, i) in appliedFilters"
|
||||
@@ -54,14 +42,7 @@
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('FILTER.CANCEL_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="isFolderView"
|
||||
:disabled="!activeFolderNewName"
|
||||
@click="updateSavedCustomViews"
|
||||
>
|
||||
{{ $t('FILTER.UPDATE_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button v-else @click="submitFilterQuery">
|
||||
<woot-button @click="submitFilterQuery">
|
||||
{{ $t('FILTER.SUBMIT_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
@@ -100,14 +81,6 @@ export default {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
activeFolderName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isFolderView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
validations: {
|
||||
appliedFilters: {
|
||||
@@ -134,7 +107,6 @@ export default {
|
||||
return {
|
||||
show: true,
|
||||
appliedFilters: this.initialAppliedFilters,
|
||||
activeFolderNewName: this.activeFolderName,
|
||||
filterTypes: this.initialFilterTypes,
|
||||
filterAttributeGroups,
|
||||
filterGroups: [],
|
||||
@@ -147,16 +119,6 @@ export default {
|
||||
...mapGetters({
|
||||
getAppliedConversationFilters: 'getAppliedConversationFilters',
|
||||
}),
|
||||
filterModalHeaderTitle() {
|
||||
return !this.isFolderView
|
||||
? this.$t('FILTER.TITLE')
|
||||
: this.$t('FILTER.EDIT_CUSTOM_FILTER');
|
||||
},
|
||||
filterModalSubTitle() {
|
||||
return !this.isFolderView
|
||||
? this.$t('FILTER.SUBTITLE')
|
||||
: this.$t('FILTER.CUSTOM_VIEWS_SUBTITLE');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setFilterAttributes();
|
||||
@@ -164,7 +126,7 @@ export default {
|
||||
if (this.getAppliedConversationFilters.length) {
|
||||
this.appliedFilters = [];
|
||||
this.appliedFilters = [...this.getAppliedConversationFilters];
|
||||
} else if (!this.isFolderView) {
|
||||
} else {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
@@ -215,11 +177,11 @@ export default {
|
||||
if (key === 'created_at' || key === 'last_activity_at')
|
||||
if (operator === 'days_before') return 'plain_text';
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type?.inputType;
|
||||
return type.inputType;
|
||||
},
|
||||
getOperators(key) {
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type?.filterOperators;
|
||||
return type.filterOperators;
|
||||
},
|
||||
getDropdownValues(type) {
|
||||
const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
|
||||
@@ -305,30 +267,11 @@ export default {
|
||||
}
|
||||
},
|
||||
appendNewFilter() {
|
||||
if (this.isFolderView) {
|
||||
this.setQueryOperatorOnLastQuery();
|
||||
} else {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
});
|
||||
}
|
||||
},
|
||||
setQueryOperatorOnLastQuery() {
|
||||
const lastItemIndex = this.appliedFilters.length - 1;
|
||||
this.appliedFilters[lastItemIndex] = {
|
||||
...this.appliedFilters[lastItemIndex],
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
};
|
||||
this.$nextTick(() => {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
});
|
||||
});
|
||||
},
|
||||
removeFilter(index) {
|
||||
@@ -353,9 +296,6 @@ export default {
|
||||
})),
|
||||
});
|
||||
},
|
||||
updateSavedCustomViews() {
|
||||
this.$emit('updateFolder', this.appliedFilters, this.activeFolderNewName);
|
||||
},
|
||||
resetFilter(index, currentFilter) {
|
||||
this.appliedFilters[index].filter_operator = this.filterTypes.find(
|
||||
filter => filter.attributeKey === currentFilter.attribute_key
|
||||
@@ -382,12 +322,4 @@ export default {
|
||||
.filter-actions {
|
||||
margin-top: var(--space-normal);
|
||||
}
|
||||
|
||||
.input-label {
|
||||
margin-bottom: var(--space-smaller);
|
||||
|
||||
.name-input {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,11 +53,22 @@
|
||||
</span>
|
||||
<div v-if="!isPending && hasAttachments">
|
||||
<div v-for="attachment in data.attachments" :key="attachment.id">
|
||||
<bubble-image-audio-video
|
||||
v-if="isAttachmentImageVideoAudio(attachment.file_type)"
|
||||
:attachment="attachment"
|
||||
<bubble-image
|
||||
v-if="attachment.file_type === 'image' && !hasImageError"
|
||||
:url="attachment.data_url"
|
||||
@error="onImageLoadError"
|
||||
/>
|
||||
<audio
|
||||
v-else-if="attachment.file_type === 'audio'"
|
||||
controls
|
||||
class="skip-context-menu"
|
||||
>
|
||||
<source :src="`${attachment.data_url}?t=${Date.now()}`" />
|
||||
</audio>
|
||||
<bubble-video
|
||||
v-else-if="attachment.file_type === 'video'"
|
||||
:url="attachment.data_url"
|
||||
/>
|
||||
<bubble-location
|
||||
v-else-if="attachment.file_type === 'location'"
|
||||
:latitude="attachment.coordinates_lat"
|
||||
@@ -133,12 +144,11 @@ import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
|
||||
import BubbleActions from './bubble/Actions';
|
||||
import BubbleFile from './bubble/File';
|
||||
import BubbleImage from './bubble/Image';
|
||||
import BubbleVideo from './bubble/Video';
|
||||
import BubbleImageAudioVideo from './bubble/ImageAudioVideo';
|
||||
import BubbleIntegration from './bubble/Integration.vue';
|
||||
import BubbleLocation from './bubble/Location';
|
||||
import BubbleMailHead from './bubble/MailHead';
|
||||
import BubbleText from './bubble/Text';
|
||||
import BubbleVideo from './bubble/Video.vue';
|
||||
import BubbleContact from './bubble/Contact';
|
||||
import Spinner from 'shared/components/Spinner';
|
||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu';
|
||||
@@ -155,12 +165,11 @@ export default {
|
||||
BubbleActions,
|
||||
BubbleFile,
|
||||
BubbleImage,
|
||||
BubbleVideo,
|
||||
BubbleImageAudioVideo,
|
||||
BubbleIntegration,
|
||||
BubbleLocation,
|
||||
BubbleMailHead,
|
||||
BubbleText,
|
||||
BubbleVideo,
|
||||
BubbleContact,
|
||||
ContextMenu,
|
||||
Spinner,
|
||||
@@ -438,9 +447,6 @@ export default {
|
||||
clearTimeout(this.higlightTimeout);
|
||||
},
|
||||
methods: {
|
||||
isAttachmentImageVideoAudio(fileType) {
|
||||
return ['image', 'audio', 'video'].includes(fileType);
|
||||
},
|
||||
hasMediaAttachment(type) {
|
||||
if (this.hasAttachments && this.data.attachments.length > 0) {
|
||||
const { attachments = [{}] } = this.data;
|
||||
|
||||
@@ -279,7 +279,6 @@ export default {
|
||||
if (newChat.id === oldChat.id) {
|
||||
return;
|
||||
}
|
||||
this.fetchAllAttachmentsFromCurrentChat();
|
||||
this.selectedTweetId = null;
|
||||
},
|
||||
},
|
||||
@@ -291,7 +290,6 @@ export default {
|
||||
|
||||
mounted() {
|
||||
this.addScrollListener();
|
||||
this.fetchAllAttachmentsFromCurrentChat();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
@@ -300,9 +298,6 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
fetchAllAttachmentsFromCurrentChat() {
|
||||
this.$store.dispatch('fetchAllAttachments', this.currentChat.id);
|
||||
},
|
||||
removeBusListeners() {
|
||||
bus.$off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
|
||||
bus.$off(BUS_EVENTS.SET_TWEET_REPLY, this.setSelectedTweet);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<div class="reply-box" :class="replyBoxClass">
|
||||
<banner
|
||||
v-if="showSelfAssignBanner"
|
||||
action-button-variant="link"
|
||||
color-scheme="secondary"
|
||||
:banner-message="$t('CONVERSATION.NOT_ASSIGNED_TO_YOU')"
|
||||
:has-action-button="true"
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<div class="message-text__wrap" :class="attachmentTypeClasses">
|
||||
<img
|
||||
v-if="isImage && !isImageError"
|
||||
:src="attachment.data_url"
|
||||
@click="onClick"
|
||||
@error="onImgError()"
|
||||
/>
|
||||
<video
|
||||
v-if="isVideo"
|
||||
:src="attachment.data_url"
|
||||
muted
|
||||
playsInline
|
||||
@click="onClick"
|
||||
/>
|
||||
<audio v-else-if="isAudio" controls class="skip-context-menu">
|
||||
<source :src="`${attachment.data_url}?t=${Date.now()}`" />
|
||||
</audio>
|
||||
<gallery-view
|
||||
v-if="show"
|
||||
:show.sync="show"
|
||||
:attachment="attachment"
|
||||
:all-attachments="filteredCurrentChatAttachments"
|
||||
@error="onImgError()"
|
||||
@close="onClose"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { hasPressedCommand } from 'shared/helpers/KeyboardHelpers';
|
||||
|
||||
import GalleryView from '../components/GalleryView';
|
||||
|
||||
const ALLOWED_FILE_TYPES = {
|
||||
IMAGE: 'image',
|
||||
VIDEO: 'video',
|
||||
AUDIO: 'audio',
|
||||
};
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GalleryView,
|
||||
},
|
||||
props: {
|
||||
attachment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
isImageError: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChatAttachments: 'getSelectedChatAttachments',
|
||||
}),
|
||||
isImage() {
|
||||
return this.attachment.file_type === ALLOWED_FILE_TYPES.IMAGE;
|
||||
},
|
||||
isVideo() {
|
||||
return this.attachment.file_type === ALLOWED_FILE_TYPES.VIDEO;
|
||||
},
|
||||
isAudio() {
|
||||
return this.attachment.file_type === ALLOWED_FILE_TYPES.AUDIO;
|
||||
},
|
||||
attachmentTypeClasses() {
|
||||
return {
|
||||
image: this.isImage,
|
||||
video: this.isVideo,
|
||||
};
|
||||
},
|
||||
filteredCurrentChatAttachments() {
|
||||
const attachments = this.currentChatAttachments.filter(attachment =>
|
||||
['image', 'video', 'audio'].includes(attachment.file_type)
|
||||
);
|
||||
return attachments;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
attachment() {
|
||||
this.isImageError = false;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.show = false;
|
||||
},
|
||||
onClick(e) {
|
||||
if (hasPressedCommand(e)) {
|
||||
window.open(this.attachment.data_url, '_blank');
|
||||
return;
|
||||
}
|
||||
this.show = true;
|
||||
},
|
||||
onImgError() {
|
||||
this.isImageError = true;
|
||||
this.$emit('error');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,201 +0,0 @@
|
||||
<template>
|
||||
<woot-modal full-width :show.sync="show" :on-close="onClose">
|
||||
<div v-on-clickaway="onClose" class="gallery-modal--wrap" @click="onClose">
|
||||
<div class="attachment-toggle--button">
|
||||
<woot-button
|
||||
v-if="hasMoreThanOneAttachment"
|
||||
size="large"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
icon="chevron-left"
|
||||
:disabled="activeImageIndex === 0"
|
||||
@click.stop="
|
||||
onClickChangeAttachment(
|
||||
allAttachments[activeImageIndex - 1],
|
||||
activeImageIndex - 1
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="attachments-viewer">
|
||||
<div class="attachment-view">
|
||||
<img
|
||||
v-if="isImage"
|
||||
:key="attachmentSrc"
|
||||
:src="attachmentSrc"
|
||||
class="modal-image skip-context-menu"
|
||||
@click.stop
|
||||
/>
|
||||
<video
|
||||
v-if="isVideo"
|
||||
:key="attachmentSrc"
|
||||
:src="attachmentSrc"
|
||||
controls
|
||||
playsInline
|
||||
class="modal-video skip-context-menu"
|
||||
@click.stop
|
||||
/>
|
||||
<audio
|
||||
v-if="isAudio"
|
||||
:key="attachmentSrc"
|
||||
controls
|
||||
class="skip-context-menu"
|
||||
@click.stop
|
||||
>
|
||||
<source :src="`${attachmentSrc}?t=${Date.now()}`" />
|
||||
</audio>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attachment-toggle--button">
|
||||
<woot-button
|
||||
v-if="hasMoreThanOneAttachment"
|
||||
size="large"
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
:disabled="activeImageIndex === allAttachments.length - 1"
|
||||
icon="chevron-right"
|
||||
@click.stop="
|
||||
onClickChangeAttachment(
|
||||
allAttachments[activeImageIndex + 1],
|
||||
activeImageIndex + 1
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
import {
|
||||
isEscape,
|
||||
hasPressedArrowLeftKey,
|
||||
hasPressedArrowRightKey,
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
|
||||
|
||||
const ALLOWED_FILE_TYPES = {
|
||||
IMAGE: 'image',
|
||||
VIDEO: 'video',
|
||||
AUDIO: 'audio',
|
||||
};
|
||||
|
||||
export default {
|
||||
mixins: [eventListenerMixins, clickaway],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
attachment: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
allAttachments: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
attachmentSrc: '',
|
||||
activeFileType: '',
|
||||
activeImageIndex:
|
||||
this.allAttachments.findIndex(
|
||||
attachment => attachment.id === this.attachment.id
|
||||
) || 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasMoreThanOneAttachment() {
|
||||
return this.allAttachments.length > 1;
|
||||
},
|
||||
isImage() {
|
||||
return this.activeFileType === ALLOWED_FILE_TYPES.IMAGE;
|
||||
},
|
||||
isVideo() {
|
||||
return this.activeFileType === ALLOWED_FILE_TYPES.VIDEO;
|
||||
},
|
||||
isAudio() {
|
||||
return this.activeFileType === ALLOWED_FILE_TYPES.AUDIO;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setImageAndVideoSrc(this.attachment);
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.$emit('close');
|
||||
},
|
||||
onClickChangeAttachment(attachment, index) {
|
||||
if (!attachment) {
|
||||
return;
|
||||
}
|
||||
this.activeImageIndex = index;
|
||||
this.setImageAndVideoSrc(attachment);
|
||||
},
|
||||
setImageAndVideoSrc(attachment) {
|
||||
const { file_type: type } = attachment;
|
||||
if (!Object.values(ALLOWED_FILE_TYPES).includes(type)) {
|
||||
return;
|
||||
}
|
||||
this.attachmentSrc = attachment.data_url;
|
||||
this.activeFileType = type;
|
||||
},
|
||||
onKeyDownHandler(e) {
|
||||
if (isEscape(e)) {
|
||||
this.onClose();
|
||||
} else if (hasPressedArrowLeftKey(e)) {
|
||||
this.onClickChangeAttachment(
|
||||
this.allAttachments[this.activeImageIndex - 1],
|
||||
this.activeImageIndex - 1
|
||||
);
|
||||
} else if (hasPressedArrowRightKey(e)) {
|
||||
this.onClickChangeAttachment(
|
||||
this.allAttachments[this.activeImageIndex + 1],
|
||||
this.activeImageIndex + 1
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.gallery-modal--wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: inherit;
|
||||
height: inherit;
|
||||
|
||||
.attachments-viewer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.attachment-view {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
video {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment-toggle--button {
|
||||
width: var(--space-mega);
|
||||
min-width: var(--space-mega);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,119 +0,0 @@
|
||||
export const getInputType = (key, operator, filterTypes) => {
|
||||
if (key === 'created_at' || key === 'last_activity_at')
|
||||
if (operator === 'days_before') return 'plain_text';
|
||||
const type = filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type?.inputType;
|
||||
};
|
||||
|
||||
export const generateCustomAttributesInputType = type => {
|
||||
const filterInputTypes = {
|
||||
text: 'string',
|
||||
number: 'string',
|
||||
date: 'string',
|
||||
checkbox: 'multi_select',
|
||||
list: 'multi_select',
|
||||
link: 'string',
|
||||
};
|
||||
return filterInputTypes[type];
|
||||
};
|
||||
|
||||
export const getAttributeInputType = (key, allCustomAttributes) => {
|
||||
const customAttribute = allCustomAttributes.find(
|
||||
attr => attr.attribute_key === key
|
||||
);
|
||||
const { attribute_display_type } = customAttribute;
|
||||
const filterInputTypes = generateCustomAttributesInputType(
|
||||
attribute_display_type
|
||||
);
|
||||
return filterInputTypes;
|
||||
};
|
||||
|
||||
export const getValuesName = (values, list, idKey, nameKey) => {
|
||||
const item = list?.find(v => v[idKey] === values[0]);
|
||||
return {
|
||||
id: values[0],
|
||||
name: item ? item[nameKey] : values[0],
|
||||
};
|
||||
};
|
||||
|
||||
const getValuesForLabels = (values, labels) => {
|
||||
const selectedLabels = labels.filter(label => values.includes(label.title));
|
||||
return selectedLabels.map(({ title }) => ({
|
||||
id: title,
|
||||
name: title,
|
||||
}));
|
||||
};
|
||||
|
||||
const getValuesForLanguages = (values, languages) => {
|
||||
const selectedLanguages = languages.filter(language =>
|
||||
values.includes(language.id)
|
||||
);
|
||||
return selectedLanguages.map(({ id, name }) => ({
|
||||
id: id.toLowerCase(),
|
||||
name: name,
|
||||
}));
|
||||
};
|
||||
|
||||
const getValuesForCountries = (values, countries) => {
|
||||
const selectedCountries = countries.filter(country =>
|
||||
values.includes(country.id)
|
||||
);
|
||||
return selectedCountries.map(({ id, name }) => ({
|
||||
id: id,
|
||||
name: name,
|
||||
}));
|
||||
};
|
||||
|
||||
export const getValuesForFilter = (filter, params) => {
|
||||
const { attribute_key, values } = filter;
|
||||
const {
|
||||
languages,
|
||||
countries,
|
||||
agents,
|
||||
inboxes,
|
||||
teams,
|
||||
campaigns,
|
||||
labels,
|
||||
} = params;
|
||||
switch (attribute_key) {
|
||||
case 'assignee_id':
|
||||
return getValuesName(values, agents, 'id', 'name');
|
||||
case 'inbox_id':
|
||||
return getValuesName(values, inboxes, 'id', 'name');
|
||||
case 'team_id':
|
||||
return getValuesName(values, teams, 'id', 'name');
|
||||
case 'campaign_id':
|
||||
return getValuesName(values, campaigns, 'id', 'title');
|
||||
case 'labels': {
|
||||
return getValuesForLabels(values, labels);
|
||||
}
|
||||
case 'browser_language': {
|
||||
return getValuesForLanguages(values, languages);
|
||||
}
|
||||
case 'country_code': {
|
||||
return getValuesForCountries(values, countries);
|
||||
}
|
||||
default:
|
||||
return { id: values[0], name: values[0] };
|
||||
}
|
||||
};
|
||||
|
||||
export const generateValuesForEditCustomViews = (filter, params) => {
|
||||
const { attribute_key, filter_operator, values } = filter;
|
||||
const { filterTypes, allCustomAttributes } = params;
|
||||
const inboxType = getInputType(attribute_key, filter_operator, filterTypes);
|
||||
|
||||
if (inboxType === undefined) {
|
||||
const filterInputTypes = getAttributeInputType(
|
||||
attribute_key,
|
||||
allCustomAttributes
|
||||
);
|
||||
return filterInputTypes === 'string'
|
||||
? values[0].toString()
|
||||
: { id: values[0], name: values[0] };
|
||||
}
|
||||
|
||||
return inboxType === 'multi_select' || inboxType === 'search_select'
|
||||
? getValuesForFilter(filter, params)
|
||||
: values[0].toString();
|
||||
};
|
||||
@@ -1,12 +1,9 @@
|
||||
const setArrayValues = item => {
|
||||
return item.values[0]?.id ? item.values.map(val => val.id) : item.values;
|
||||
};
|
||||
const generatePayload = data => {
|
||||
// Make a copy of data to avoid vue data reactivity issues
|
||||
const filters = JSON.parse(JSON.stringify(data));
|
||||
let payload = filters.map(item => {
|
||||
if (Array.isArray(item.values)) {
|
||||
item.values = setArrayValues(item);
|
||||
item.values = item.values.map(val => val.id);
|
||||
} else if (typeof item.values === 'object') {
|
||||
item.values = [item.values.id];
|
||||
} else if (!item.values) {
|
||||
|
||||
@@ -18,18 +18,18 @@ export const initializeAnalyticsEvents = () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
window.bus.$on(ANALYTICS_RESET, () => {});
|
||||
};
|
||||
|
||||
const initializeAudioAlerts = user => {
|
||||
// InitializeAudioNotifications
|
||||
const { ui_settings: uiSettings } = user || {};
|
||||
const {
|
||||
always_play_audio_alert: alwaysPlayAudioAlert,
|
||||
enable_audio_alerts: audioAlertType,
|
||||
alert_if_unread_assigned_conversation_exist: alertIfUnreadConversationExist,
|
||||
notification_tone: audioAlertTone,
|
||||
// UI Settings can be undefined initally as we don't send the
|
||||
// entire payload for the user during the signup process.
|
||||
} = uiSettings || {};
|
||||
} = uiSettings;
|
||||
|
||||
DashboardAudioNotificationHelper.setInstanceValues({
|
||||
currentUserId: user.id,
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
import {
|
||||
getAttributeInputType,
|
||||
getInputType,
|
||||
getValuesName,
|
||||
getValuesForFilter,
|
||||
generateValuesForEditCustomViews,
|
||||
generateCustomAttributesInputType,
|
||||
} from '../customViewsHelper';
|
||||
import advancedFilterTypes from 'dashboard/components/widgets/conversation/advancedFilterItems/index';
|
||||
|
||||
describe('customViewsHelper', () => {
|
||||
describe('#getInputType', () => {
|
||||
it('should return plain_text if key is created_at or last_activity_at and operator is days_before', () => {
|
||||
const filterTypes = [
|
||||
{ attributeKey: 'created_at', inputType: 'date' },
|
||||
{ attributeKey: 'last_activity_at', inputType: 'date' },
|
||||
];
|
||||
expect(getInputType('created_at', 'days_before', filterTypes)).toEqual(
|
||||
'plain_text'
|
||||
);
|
||||
expect(
|
||||
getInputType('last_activity_at', 'days_before', filterTypes)
|
||||
).toEqual('plain_text');
|
||||
});
|
||||
|
||||
it('should return inputType if key is not created_at or last_activity_at', () => {
|
||||
const filterTypes = [
|
||||
{ attributeKey: 'created_at', inputType: 'date' },
|
||||
{ attributeKey: 'last_activity_at', inputType: 'date' },
|
||||
{ attributeKey: 'test', inputType: 'string' },
|
||||
];
|
||||
expect(getInputType('test', 'days_before', filterTypes)).toEqual(
|
||||
'string'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return undefined if key is not created_at or last_activity_at and inputType is not present', () => {
|
||||
const filterTypes = [
|
||||
{ attributeKey: 'created_at', inputType: 'date' },
|
||||
{ attributeKey: 'last_activity_at', inputType: 'date' },
|
||||
{ attributeKey: 'test', inputType: 'string' },
|
||||
];
|
||||
expect(getInputType('test', 'days_before', filterTypes)).toEqual(
|
||||
'string'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getAttributeInputType', () => {
|
||||
it('should return multi_select if attribute_display_type is checkbox or list', () => {
|
||||
const allCustomAttributes = [
|
||||
{ attribute_key: 'test', attribute_display_type: 'checkbox' },
|
||||
{ attribute_key: 'test2', attribute_display_type: 'list' },
|
||||
];
|
||||
expect(getAttributeInputType('test', allCustomAttributes)).toEqual(
|
||||
'multi_select'
|
||||
);
|
||||
expect(getAttributeInputType('test2', allCustomAttributes)).toEqual(
|
||||
'multi_select'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return string if attribute_display_type is text, number, date or link', () => {
|
||||
const allCustomAttributes = [
|
||||
{ attribute_key: 'test', attribute_display_type: 'text' },
|
||||
{ attribute_key: 'test2', attribute_display_type: 'number' },
|
||||
{ attribute_key: 'test3', attribute_display_type: 'date' },
|
||||
{ attribute_key: 'test4', attribute_display_type: 'link' },
|
||||
];
|
||||
expect(getAttributeInputType('test', allCustomAttributes)).toEqual(
|
||||
'string'
|
||||
);
|
||||
expect(getAttributeInputType('test2', allCustomAttributes)).toEqual(
|
||||
'string'
|
||||
);
|
||||
expect(getAttributeInputType('test3', allCustomAttributes)).toEqual(
|
||||
'string'
|
||||
);
|
||||
expect(getAttributeInputType('test4', allCustomAttributes)).toEqual(
|
||||
'string'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getValuesName', () => {
|
||||
it('should return id and name if item is present', () => {
|
||||
const list = [{ id: 1, name: 'test' }];
|
||||
const idKey = 'id';
|
||||
const nameKey = 'name';
|
||||
const values = [1];
|
||||
expect(getValuesName(values, list, idKey, nameKey)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and value if item is not present', () => {
|
||||
const list = [{ id: 1, name: 'test' }];
|
||||
const idKey = 'id';
|
||||
const nameKey = 'name';
|
||||
const values = [2];
|
||||
expect(getValuesName(values, list, idKey, nameKey)).toEqual({
|
||||
id: 2,
|
||||
name: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getValuesForFilter', () => {
|
||||
it('should return id and name if attribute_key is assignee_id', () => {
|
||||
const filter = { attribute_key: 'assignee_id', values: [1] };
|
||||
const params = { agents: [{ id: 1, name: 'test' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and name if attribute_key is inbox_id', () => {
|
||||
const filter = { attribute_key: 'inbox_id', values: [1] };
|
||||
const params = { inboxes: [{ id: 1, name: 'test' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and name if attribute_key is team_id', () => {
|
||||
const filter = { attribute_key: 'team_id', values: [1] };
|
||||
const params = { teams: [{ id: 1, name: 'test' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and title if attribute_key is campaign_id', () => {
|
||||
const filter = { attribute_key: 'campaign_id', values: [1] };
|
||||
const params = { campaigns: [{ id: 1, title: 'test' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and title if attribute_key is labels', () => {
|
||||
const filter = { attribute_key: 'labels', values: ['test'] };
|
||||
const params = { labels: [{ title: 'test' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual([
|
||||
{ id: 'test', name: 'test' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return id and name if attribute_key is browser_language', () => {
|
||||
const filter = { attribute_key: 'browser_language', values: ['en'] };
|
||||
const params = { languages: [{ id: 'en', name: 'English' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual([
|
||||
{ id: 'en', name: 'English' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return id and name if attribute_key is country_code', () => {
|
||||
const filter = { attribute_key: 'country_code', values: ['IN'] };
|
||||
const params = { countries: [{ id: 'IN', name: 'India' }] };
|
||||
expect(getValuesForFilter(filter, params)).toEqual([
|
||||
{ id: 'IN', name: 'India' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return id and name if attribute_key is not present', () => {
|
||||
const filter = { attribute_key: 'test', values: [1] };
|
||||
const params = {};
|
||||
expect(getValuesForFilter(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#generateValuesForEditCustomViews', () => {
|
||||
it('should return id and name if inboxType is multi_select or search_select', () => {
|
||||
const filter = {
|
||||
attribute_key: 'assignee_id',
|
||||
filter_operator: 'and',
|
||||
values: [1],
|
||||
};
|
||||
const params = {
|
||||
filterTypes: advancedFilterTypes,
|
||||
allCustomAttributes: [],
|
||||
agents: [{ id: 1, name: 'test' }],
|
||||
};
|
||||
expect(generateValuesForEditCustomViews(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and name if inboxType is not multi_select or search_select', () => {
|
||||
const filter = {
|
||||
attribute_key: 'assignee_id',
|
||||
filter_operator: 'and',
|
||||
values: [1],
|
||||
};
|
||||
const params = {
|
||||
filterTypes: advancedFilterTypes,
|
||||
allCustomAttributes: [],
|
||||
agents: [{ id: 1, name: 'test' }],
|
||||
};
|
||||
expect(generateValuesForEditCustomViews(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return id and name if inboxType is undefined', () => {
|
||||
const filter = {
|
||||
attribute_key: 'test2',
|
||||
filter_operator: 'and',
|
||||
values: [1],
|
||||
};
|
||||
const params = {
|
||||
filterTypes: advancedFilterTypes,
|
||||
allCustomAttributes: [
|
||||
{ attribute_key: 'test', attribute_display_type: 'checkbox' },
|
||||
{ attribute_key: 'test2', attribute_display_type: 'list' },
|
||||
],
|
||||
};
|
||||
expect(generateValuesForEditCustomViews(filter, params)).toEqual({
|
||||
id: 1,
|
||||
name: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return value as string if filterInputTypes is string', () => {
|
||||
const filter = {
|
||||
attribute_key: 'test',
|
||||
filter_operator: 'and',
|
||||
values: [1],
|
||||
};
|
||||
const params = {
|
||||
filterTypes: advancedFilterTypes,
|
||||
allCustomAttributes: [
|
||||
{ attribute_key: 'test', attribute_display_type: 'date' },
|
||||
{ attribute_key: 'test2', attribute_display_type: 'list' },
|
||||
],
|
||||
};
|
||||
expect(generateValuesForEditCustomViews(filter, params)).toEqual('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#generateCustomAttributesInputType', () => {
|
||||
it('should return string if type is text', () => {
|
||||
expect(generateCustomAttributesInputType('text')).toEqual('string');
|
||||
});
|
||||
|
||||
it('should return string if type is number', () => {
|
||||
expect(generateCustomAttributesInputType('number')).toEqual('string');
|
||||
});
|
||||
|
||||
it('should return string if type is date', () => {
|
||||
expect(generateCustomAttributesInputType('date')).toEqual('string');
|
||||
});
|
||||
|
||||
it('should return multi_select if type is checkbox', () => {
|
||||
expect(generateCustomAttributesInputType('checkbox')).toEqual(
|
||||
'multi_select'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return multi_select if type is list', () => {
|
||||
expect(generateCustomAttributesInputType('list')).toEqual('multi_select');
|
||||
});
|
||||
|
||||
it('should return string if type is link', () => {
|
||||
expect(generateCustomAttributesInputType('link')).toEqual('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,17 +2,12 @@
|
||||
"FILTER": {
|
||||
"TITLE": "Filter Conversations",
|
||||
"SUBTITLE": "Add filters below and hit 'Apply filters' to filter conversations.",
|
||||
"EDIT_CUSTOM_FILTER": "Edit Folder",
|
||||
"CUSTOM_VIEWS_SUBTITLE": "Add or remove filters and update your folder.",
|
||||
"ADD_NEW_FILTER": "Add Filter",
|
||||
"FILTER_DELETE_ERROR": "You should have atleast one filter to save",
|
||||
"SUBMIT_BUTTON_LABEL": "Apply filters",
|
||||
"UPDATE_BUTTON_LABEL": "Update folder",
|
||||
"CANCEL_BUTTON_LABEL": "Cancel",
|
||||
"CLEAR_BUTTON_LABEL": "Clear Filters",
|
||||
"EMPTY_VALUE_ERROR": "Value is required",
|
||||
"FOLDER_LABEL": "Folder Name",
|
||||
"FOLDER_QUERY_LABEL": "Folder Query",
|
||||
"TOOLTIP_LABEL": "Filter conversations",
|
||||
"QUERY_DROPDOWN_LABELS": {
|
||||
"AND": "AND",
|
||||
@@ -76,9 +71,6 @@
|
||||
"ERROR_MESSAGE": "Error while creating segment"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"EDIT_BUTTON": "Edit folder"
|
||||
},
|
||||
"DELETE": {
|
||||
"DELETE_BUTTON": "Delete filter",
|
||||
"MODAL": {
|
||||
|
||||
@@ -10,39 +10,15 @@
|
||||
"TITLE": "Manage Audit Logs",
|
||||
"DESC": "Audit Logs are trails for events and actions in a Chatwoot System.",
|
||||
"TABLE_HEADER": [
|
||||
"Activity",
|
||||
"Time",
|
||||
"IP Address"
|
||||
"User",
|
||||
"Action",
|
||||
"IP Address",
|
||||
"Time"
|
||||
]
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "AuditLogs retrieved successfully",
|
||||
"ERROR_MESSAGE": "Could not connect to Woot Server, Please try again later"
|
||||
},
|
||||
"DEFAULT_USER": "System",
|
||||
"AUTOMATION_RULE": {
|
||||
"ADD": "%{agentName} created a new automation rule (#%{id})",
|
||||
"EDIT": "%{agentName} updated an automation rule (#%{id})",
|
||||
"DELETE": "%{agentName} deleted an automation rule (#%{id})"
|
||||
},
|
||||
"INBOX": {
|
||||
"ADD": "%{agentName} created a new inbox (#%{id})",
|
||||
"EDIT": "%{agentName} updated an inbox (#%{id})",
|
||||
"DELETE": "%{agentName} deleted an inbox (#%{id})"
|
||||
},
|
||||
"WEBHOOK": {
|
||||
"ADD": "%{agentName} created a new webhook (#%{id})",
|
||||
"EDIT": "%{agentName} updated a webhook (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a webhook (#%{id})"
|
||||
},
|
||||
"USER_ACTION": {
|
||||
"SIGN_IN": "%{agentName} signed in",
|
||||
"SIGN_OUT": "%{agentName} signed out"
|
||||
},
|
||||
"TEAM": {
|
||||
"ADD": "%{agentName} created a new team (#%{id})",
|
||||
"EDIT": "%{agentName} updated a team (#%{id})",
|
||||
"DELETE": "%{agentName} deleted a team (#%{id})"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,13 +73,6 @@
|
||||
"SUCCESS_MESSAGE": "Contacts saved successfully",
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
},
|
||||
"EXPORT_CONTACTS": {
|
||||
"BUTTON_LABEL": "Export",
|
||||
"TITLE": "Export Contacts",
|
||||
"DESC": "Export contacts to a CSV file.",
|
||||
"SUCCESS_MESSAGE": "Export is in progress, You will be notified via email when export file is ready to dowanlod.",
|
||||
"ERROR_MESSAGE": "There was an error, please try again"
|
||||
},
|
||||
"DELETE_NOTE": {
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Deletion",
|
||||
@@ -218,7 +211,6 @@
|
||||
"FILTER_CONTACTS": "Filter",
|
||||
"FILTER_CONTACTS_SAVE": "Save filter",
|
||||
"FILTER_CONTACTS_DELETE": "Delete filter",
|
||||
"FILTER_CONTACTS_EDIT": "Edit segment",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading contacts...",
|
||||
"404": "No contacts matches your search 🔍",
|
||||
|
||||
@@ -1,55 +1,50 @@
|
||||
{
|
||||
"CONTACTS_FILTER": {
|
||||
"TITLE": "Filter Contacts",
|
||||
"SUBTITLE": "Add filters below and hit 'Submit' to filter contacts.",
|
||||
"EDIT_CUSTOM_SEGMENT": "Edit Segment",
|
||||
"CUSTOM_VIEWS_SUBTITLE": "Add or remove filters and update your segment.",
|
||||
"ADD_NEW_FILTER": "Add Filter",
|
||||
"CLEAR_ALL_FILTERS": "Clear All Filters",
|
||||
"FILTER_DELETE_ERROR": "You should have atleast one filter to save",
|
||||
"SUBMIT_BUTTON_LABEL": "Submit",
|
||||
"UPDATE_BUTTON_LABEL": "Update Segment",
|
||||
"CANCEL_BUTTON_LABEL": "Cancel",
|
||||
"CLEAR_BUTTON_LABEL": "Clear Filters",
|
||||
"EMPTY_VALUE_ERROR": "Value is required",
|
||||
"SEGMENT_LABEL": "Segment Name",
|
||||
"SEGMENT_QUERY_LABEL": "Segment Query",
|
||||
"TOOLTIP_LABEL": "Filter contacts",
|
||||
"QUERY_DROPDOWN_LABELS": {
|
||||
"AND": "AND",
|
||||
"OR": "OR"
|
||||
},
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Equal to",
|
||||
"not_equal_to": "Not equal to",
|
||||
"contains": "Contains",
|
||||
"does_not_contain": "Does not contain",
|
||||
"is_present": "Is present",
|
||||
"is_not_present": "Is not present",
|
||||
"is_greater_than": "Is greater than",
|
||||
"is_lesser_than": "Is lesser than",
|
||||
"days_before": "Is x days before"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"NAME": "Name",
|
||||
"EMAIL": "Email",
|
||||
"PHONE_NUMBER": "Phone number",
|
||||
"IDENTIFIER": "Identifier",
|
||||
"CITY": "City",
|
||||
"COUNTRY": "Country",
|
||||
"CUSTOM_ATTRIBUTE_LIST": "List",
|
||||
"CUSTOM_ATTRIBUTE_TEXT": "Text",
|
||||
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
|
||||
"CUSTOM_ATTRIBUTE_LINK": "Link",
|
||||
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
|
||||
"CREATED_AT": "Created At",
|
||||
"LAST_ACTIVITY": "Last Activity",
|
||||
"REFERER_LINK": "Referrer link"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard Filters",
|
||||
"ADDITIONAL_FILTERS": "Additional Filters",
|
||||
"CUSTOM_ATTRIBUTES": "Custom Attributes"
|
||||
"CONTACTS_FILTER": {
|
||||
"TITLE": "Filter Contacts",
|
||||
"SUBTITLE": "Add filters below and hit 'Submit' to filter contacts.",
|
||||
"ADD_NEW_FILTER": "Add Filter",
|
||||
"CLEAR_ALL_FILTERS": "Clear All Filters",
|
||||
"FILTER_DELETE_ERROR": "You should have atleast one filter to save",
|
||||
"SUBMIT_BUTTON_LABEL": "Submit",
|
||||
"CANCEL_BUTTON_LABEL": "Cancel",
|
||||
"CLEAR_BUTTON_LABEL": "Clear Filters",
|
||||
"EMPTY_VALUE_ERROR": "Value is required",
|
||||
"TOOLTIP_LABEL": "Filter contacts",
|
||||
"QUERY_DROPDOWN_LABELS": {
|
||||
"AND": "AND",
|
||||
"OR": "OR"
|
||||
},
|
||||
"OPERATOR_LABELS": {
|
||||
"equal_to": "Equal to",
|
||||
"not_equal_to": "Not equal to",
|
||||
"contains": "Contains",
|
||||
"does_not_contain": "Does not contain",
|
||||
"is_present": "Is present",
|
||||
"is_not_present": "Is not present",
|
||||
"is_greater_than": "Is greater than",
|
||||
"is_lesser_than": "Is lesser than",
|
||||
"days_before": "Is x days before"
|
||||
},
|
||||
"ATTRIBUTES": {
|
||||
"NAME": "Name",
|
||||
"EMAIL": "Email",
|
||||
"PHONE_NUMBER": "Phone number",
|
||||
"IDENTIFIER": "Identifier",
|
||||
"CITY": "City",
|
||||
"COUNTRY": "Country",
|
||||
"CUSTOM_ATTRIBUTE_LIST": "List",
|
||||
"CUSTOM_ATTRIBUTE_TEXT": "Text",
|
||||
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
|
||||
"CUSTOM_ATTRIBUTE_LINK": "Link",
|
||||
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
|
||||
"CREATED_AT": "Created At",
|
||||
"LAST_ACTIVITY": "Last Activity",
|
||||
"REFERER_LINK": "Referrer link"
|
||||
},
|
||||
"GROUPS": {
|
||||
"STANDARD_FILTERS": "Standard Filters",
|
||||
"ADDITIONAL_FILTERS": "Additional Filters",
|
||||
"CUSTOM_ATTRIBUTES": "Custom Attributes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,6 +535,7 @@
|
||||
"UPDATE": "Update business hours settings",
|
||||
"TOGGLE_AVAILABILITY": "Enable business availability for this inbox",
|
||||
"UNAVAILABLE_MESSAGE_LABEL": "Unavailable message for visitors",
|
||||
"UNAVAILABLE_MESSAGE_DEFAULT": "We are unavailable at the moment. Leave a message we will respond once we are back.",
|
||||
"TOGGLE_HELP": "Enabling business availability will show the available hours on live chat widget even if all the agents are offline. Outside available hours vistors can be warned with a message and a pre-chat form.",
|
||||
"DAY": {
|
||||
"ENABLE": "Enable availability for this day",
|
||||
|
||||
+13
-85
@@ -1,25 +1,9 @@
|
||||
<template>
|
||||
<div class="column">
|
||||
<woot-modal-header :header-title="filterModalHeaderTitle">
|
||||
<p>{{ filterModalSubTitle }}</p>
|
||||
<woot-modal-header :header-title="$t('CONTACTS_FILTER.TITLE')">
|
||||
<p>{{ $t('CONTACTS_FILTER.SUBTITLE') }}</p>
|
||||
</woot-modal-header>
|
||||
<div class="column modal-content">
|
||||
<div v-if="isSegmentsView" class="columns">
|
||||
<label class="input-label" :class="{ error: !activeSegmentNewName }">
|
||||
{{ $t('CONTACTS_FILTER.SEGMENT_LABEL') }}
|
||||
<input
|
||||
v-model="activeSegmentNewName"
|
||||
type="text"
|
||||
class="name-input"
|
||||
/>
|
||||
<span v-if="!activeSegmentNewName" class="message">
|
||||
{{ $t('CONTACTS_FILTER.EMPTY_VALUE_ERROR') }}
|
||||
</span>
|
||||
</label>
|
||||
<label class="input-label">
|
||||
{{ $t('CONTACTS_FILTER.SEGMENT_QUERY_LABEL') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="row modal-content">
|
||||
<div class="medium-12 columns filters-wrap">
|
||||
<filter-input-box
|
||||
v-for="(filter, i) in appliedFilters"
|
||||
@@ -52,7 +36,7 @@
|
||||
{{ $t('CONTACTS_FILTER.ADD_NEW_FILTER') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="hasAppliedFilters && !isSegmentsView"
|
||||
v-if="hasAppliedFilters"
|
||||
icon="subtract"
|
||||
color-scheme="alert"
|
||||
variant="smooth"
|
||||
@@ -68,14 +52,7 @@
|
||||
<woot-button class="button clear" @click.prevent="onClose">
|
||||
{{ $t('CONTACTS_FILTER.CANCEL_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-if="isSegmentsView"
|
||||
:disabled="!activeSegmentNewName"
|
||||
@click="updateSegment"
|
||||
>
|
||||
{{ $t('CONTACTS_FILTER.UPDATE_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
<woot-button v-else @click="submitFilterQuery">
|
||||
<woot-button @click="submitFilterQuery">
|
||||
{{ $t('CONTACTS_FILTER.SUBMIT_BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
@@ -108,18 +85,6 @@ export default {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
initialAppliedFilters: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isSegmentsView: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
activeSegmentName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
validations: {
|
||||
appliedFilters: {
|
||||
@@ -140,8 +105,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
show: true,
|
||||
appliedFilters: this.initialAppliedFilters,
|
||||
activeSegmentNewName: this.activeSegmentName,
|
||||
appliedFilters: [],
|
||||
filterTypes: this.initialFilterTypes,
|
||||
filterGroups: [],
|
||||
allCustomAttributes: [],
|
||||
@@ -157,22 +121,12 @@ export default {
|
||||
hasAppliedFilters() {
|
||||
return this.getAppliedContactFilters.length;
|
||||
},
|
||||
filterModalHeaderTitle() {
|
||||
return !this.isSegmentsView
|
||||
? this.$t('CONTACTS_FILTER.TITLE')
|
||||
: this.$t('CONTACTS_FILTER.EDIT_CUSTOM_SEGMENT');
|
||||
},
|
||||
filterModalSubTitle() {
|
||||
return !this.isSegmentsView
|
||||
? this.$t('CONTACTS_FILTER.SUBTITLE')
|
||||
: this.$t('CONTACTS_FILTER.CUSTOM_VIEWS_SUBTITLE');
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setFilterAttributes();
|
||||
if (this.getAppliedContactFilters.length) {
|
||||
this.appliedFilters = [...this.getAppliedContactFilters];
|
||||
} else if (!this.isSegmentsView) {
|
||||
} else {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'name',
|
||||
filter_operator: 'equal_to',
|
||||
@@ -223,11 +177,11 @@ export default {
|
||||
if (key === 'created_at' || key === 'last_activity_at')
|
||||
if (operator === 'days_before') return 'plain_text';
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type?.inputType;
|
||||
return type.inputType;
|
||||
},
|
||||
getOperators(key) {
|
||||
const type = this.filterTypes.find(filter => filter.attributeKey === key);
|
||||
return type?.filterOperators;
|
||||
return type.filterOperators;
|
||||
},
|
||||
getDropdownValues(type) {
|
||||
const allCustomAttributes = this.$store.getters[
|
||||
@@ -276,30 +230,11 @@ export default {
|
||||
}
|
||||
},
|
||||
appendNewFilter() {
|
||||
if (this.isSegmentsView) {
|
||||
this.setQueryOperatorOnLastQuery();
|
||||
} else {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'name',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
});
|
||||
}
|
||||
},
|
||||
setQueryOperatorOnLastQuery() {
|
||||
const lastItemIndex = this.appliedFilters.length - 1;
|
||||
this.appliedFilters[lastItemIndex] = {
|
||||
...this.appliedFilters[lastItemIndex],
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'name',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
};
|
||||
this.$nextTick(() => {
|
||||
this.appliedFilters.push({
|
||||
attribute_key: 'name',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
});
|
||||
});
|
||||
},
|
||||
removeFilter(index) {
|
||||
@@ -324,13 +259,6 @@ export default {
|
||||
})),
|
||||
});
|
||||
},
|
||||
updateSegment() {
|
||||
this.$emit(
|
||||
'updateSegment',
|
||||
this.appliedFilters,
|
||||
this.activeSegmentNewName
|
||||
);
|
||||
},
|
||||
resetFilter(index, currentFilter) {
|
||||
this.appliedFilters[index].filter_operator = this.filterTypes.find(
|
||||
filter => filter.attributeKey === currentFilter.attribute_key
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<div class="contacts-page row">
|
||||
<div class="left-wrap" :class="wrapClass">
|
||||
<div class="left-wrap" :class="wrapClas">
|
||||
<contacts-header
|
||||
:search-query="searchQuery"
|
||||
:segments-id="segmentsId"
|
||||
:on-search-submit="onSearchSubmit"
|
||||
:on-export-submit="onExportSubmit"
|
||||
this-selected-contact-id=""
|
||||
:on-input-search="onInputSearch"
|
||||
:on-toggle-create="onToggleCreate"
|
||||
@@ -14,7 +13,6 @@
|
||||
:header-title="pageTitle"
|
||||
@on-toggle-save-filter="onToggleSaveFilters"
|
||||
@on-toggle-delete-filter="onToggleDeleteFilters"
|
||||
@on-toggle-edit-filter="onToggleFilters"
|
||||
/>
|
||||
<contacts-table
|
||||
:contacts="records"
|
||||
@@ -60,18 +58,14 @@
|
||||
</woot-modal>
|
||||
<woot-modal
|
||||
:show.sync="showFiltersModal"
|
||||
:on-close="closeAdvanceFiltersModal"
|
||||
:on-close="onToggleFilters"
|
||||
size="medium"
|
||||
>
|
||||
<contacts-advanced-filters
|
||||
v-if="showFiltersModal"
|
||||
:on-close="closeAdvanceFiltersModal"
|
||||
:on-close="onToggleFilters"
|
||||
:initial-filter-types="contactFilterItems"
|
||||
:initial-applied-filters="appliedFilter"
|
||||
:active-segment-name="activeSegmentName"
|
||||
:is-segments-view="hasActiveSegments"
|
||||
@applyFilter="onApplyFilter"
|
||||
@updateSegment="onUpdateSegment"
|
||||
@clearFilters="clearFilters"
|
||||
/>
|
||||
</woot-modal>
|
||||
@@ -93,9 +87,6 @@ import filterQueryGenerator from '../../../../helper/filterQueryGenerator';
|
||||
import AddCustomViews from 'dashboard/routes/dashboard/customviews/AddCustomViews';
|
||||
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews';
|
||||
import { CONTACTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import countries from 'shared/constants/countries.js';
|
||||
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
||||
|
||||
const DEFAULT_PAGE = 1;
|
||||
const FILTER_TYPE_CONTACT = 1;
|
||||
@@ -112,7 +103,6 @@ export default {
|
||||
AddCustomViews,
|
||||
DeleteCustomViews,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
label: { type: String, default: '' },
|
||||
segmentsId: {
|
||||
@@ -138,7 +128,6 @@ export default {
|
||||
filterType: FILTER_TYPE_CONTACT,
|
||||
showAddSegmentsModal: false,
|
||||
showDeleteSegmentsModal: false,
|
||||
appliedFilter: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -186,7 +175,7 @@ export default {
|
||||
showContactViewPane() {
|
||||
return this.selectedContactId !== '';
|
||||
},
|
||||
wrapClass() {
|
||||
wrapClas() {
|
||||
return this.showContactViewPane ? 'medium-9' : 'medium-12';
|
||||
},
|
||||
pageParameter() {
|
||||
@@ -205,9 +194,6 @@ export default {
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
activeSegmentName() {
|
||||
return this.activeSegment?.name;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
label() {
|
||||
@@ -359,14 +345,7 @@ export default {
|
||||
});
|
||||
},
|
||||
onToggleFilters() {
|
||||
if (this.hasActiveSegments) {
|
||||
this.initializeSegmentToFilterModal(this.activeSegment);
|
||||
}
|
||||
this.showFiltersModal = true;
|
||||
},
|
||||
closeAdvanceFiltersModal() {
|
||||
this.showFiltersModal = false;
|
||||
this.appliedFilter = [];
|
||||
this.showFiltersModal = !this.showFiltersModal;
|
||||
},
|
||||
onApplyFilter(payload) {
|
||||
this.closeContactInfoPanel();
|
||||
@@ -376,69 +355,10 @@ export default {
|
||||
});
|
||||
this.showFiltersModal = false;
|
||||
},
|
||||
onUpdateSegment(payload, segmentName) {
|
||||
const payloadData = {
|
||||
...this.activeSegment,
|
||||
name: segmentName,
|
||||
query: filterQueryGenerator(payload),
|
||||
};
|
||||
this.$store.dispatch('customViews/update', payloadData);
|
||||
this.closeAdvanceFiltersModal();
|
||||
},
|
||||
clearFilters() {
|
||||
this.$store.dispatch('contacts/clearContactFilters');
|
||||
this.fetchContacts(this.pageParameter);
|
||||
},
|
||||
onExportSubmit() {
|
||||
try {
|
||||
this.$store.dispatch('contacts/export');
|
||||
this.showAlert(this.$t('EXPORT_CONTACTS.SUCCESS_MESSAGE'));
|
||||
} catch (error) {
|
||||
this.showAlert(
|
||||
error.message || this.$t('EXPORT_CONTACTS.ERROR_MESSAGE')
|
||||
);
|
||||
}
|
||||
},
|
||||
setParamsForEditSegmentModal() {
|
||||
// Here we are setting the params for edit segment modal to show the existing values.
|
||||
|
||||
// For custom attributes we get only attribute key.
|
||||
// So we are mapping it to find the input type of the attribute to show in the edit segment modal.
|
||||
const params = {
|
||||
countries: countries,
|
||||
filterTypes: contactFilterItems,
|
||||
allCustomAttributes: this.$store.getters[
|
||||
'attributes/getAttributesByModel'
|
||||
]('contact_attribute'),
|
||||
};
|
||||
return params;
|
||||
},
|
||||
initializeSegmentToFilterModal(activeSegment) {
|
||||
// Here we are setting the params for edit segment modal.
|
||||
// To show the existing values. when we click on edit segment button.
|
||||
|
||||
// Here we get the query from the active segment.
|
||||
// And we are mapping the query to the actual values.
|
||||
// To show in the edit segment modal by the help of generateValuesForEditCustomViews helper.
|
||||
const query = activeSegment?.query?.payload;
|
||||
if (!Array.isArray(query)) return;
|
||||
|
||||
this.appliedFilter.push(
|
||||
...query.map(filter => ({
|
||||
attribute_key: filter.attribute_key,
|
||||
attribute_model: filter.attribute_model,
|
||||
filter_operator: filter.filter_operator,
|
||||
values: Array.isArray(filter.values)
|
||||
? generateValuesForEditCustomViews(
|
||||
filter,
|
||||
this.setParamsForEditSegmentModal()
|
||||
)
|
||||
: [],
|
||||
query_operator: filter.query_operator,
|
||||
custom_attribute_type: filter.custom_attribute_type,
|
||||
}))
|
||||
);
|
||||
},
|
||||
openSavedItemInSegment() {
|
||||
const lastItemInSegments = this.segments[this.segments.length - 1];
|
||||
const lastItemId = lastItemInSegments.id;
|
||||
|
||||
@@ -27,24 +27,15 @@
|
||||
{{ $t('CONTACTS_PAGE.SEARCH_BUTTON') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div v-if="hasActiveSegments">
|
||||
<woot-button
|
||||
class="margin-right-1 clear"
|
||||
color-scheme="secondary"
|
||||
icon="edit"
|
||||
@click="onToggleEditSegmentsModal"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_EDIT') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
class="margin-right-1 clear"
|
||||
color-scheme="alert"
|
||||
icon="delete"
|
||||
@click="onToggleDeleteSegmentsModal"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_DELETE') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<woot-button
|
||||
v-if="hasActiveSegments"
|
||||
class="margin-right-1 clear"
|
||||
color-scheme="alert"
|
||||
icon="delete"
|
||||
@click="onToggleDeleteSegmentsModal"
|
||||
>
|
||||
{{ $t('CONTACTS_PAGE.FILTER_CONTACTS_DELETE') }}
|
||||
</woot-button>
|
||||
<div v-if="!hasActiveSegments" class="filters__button-wrap">
|
||||
<div v-if="hasAppliedFilters" class="filters__applied-indicator" />
|
||||
<woot-button
|
||||
@@ -87,16 +78,6 @@
|
||||
>
|
||||
{{ $t('IMPORT_CONTACTS.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
|
||||
<woot-button
|
||||
v-if="isAdmin"
|
||||
color-scheme="info"
|
||||
icon="upload"
|
||||
class="clear"
|
||||
@click="onExportSubmit"
|
||||
>
|
||||
{{ $t('EXPORT_CONTACTS.BUTTON_LABEL') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -137,10 +118,6 @@ export default {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onExportSubmit: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onToggleFilter: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
@@ -170,9 +147,6 @@ export default {
|
||||
onToggleSegmentsModal() {
|
||||
this.$emit('on-toggle-save-filter');
|
||||
},
|
||||
onToggleEditSegmentsModal() {
|
||||
this.$emit('on-toggle-edit-filter');
|
||||
},
|
||||
onToggleDeleteSegmentsModal() {
|
||||
this.$emit('on-toggle-delete-filter');
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="column content-box audit-log--settings">
|
||||
<div class="column content-box">
|
||||
<!-- List Audit Logs -->
|
||||
<div>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="small-8 columns with-right-space ">
|
||||
<p
|
||||
v-if="!uiFlags.fetchingList && !records.length"
|
||||
class="no-items-error-message"
|
||||
@@ -16,13 +16,8 @@
|
||||
|
||||
<table
|
||||
v-if="!uiFlags.fetchingList && records.length"
|
||||
class="woot-table width-100"
|
||||
class="woot-table"
|
||||
>
|
||||
<colgroup>
|
||||
<col class="column-activity" />
|
||||
<col />
|
||||
<col />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<!-- Header -->
|
||||
<th
|
||||
@@ -34,8 +29,12 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="auditLogItem in records" :key="auditLogItem.id">
|
||||
<td class="wrap-break-words">{{ auditLogItem.username }}</td>
|
||||
<td class="wrap-break-words">
|
||||
{{ generateLogText(auditLogItem) }}
|
||||
{{ auditLogItem.auditable_type }}.{{ auditLogItem.action }}
|
||||
</td>
|
||||
<td class="remote-address">
|
||||
{{ auditLogItem.remote_address }}
|
||||
</td>
|
||||
<td class="wrap-break-words">
|
||||
{{
|
||||
@@ -45,9 +44,6 @@
|
||||
)
|
||||
}}
|
||||
</td>
|
||||
<td class="remote-address">
|
||||
{{ auditLogItem.remote_address }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -85,55 +81,13 @@ export default {
|
||||
records: 'auditlogs/getAuditLogs',
|
||||
uiFlags: 'auditlogs/getUIFlags',
|
||||
meta: 'auditlogs/getMeta',
|
||||
agentList: 'agents/getAgents',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
// Fetch API Call
|
||||
this.$store.dispatch('auditlogs/fetch', { page: 1 });
|
||||
this.$store.dispatch('agents/get');
|
||||
},
|
||||
methods: {
|
||||
getAgentName(email) {
|
||||
if (email === null) {
|
||||
return this.$t('AUDIT_LOGS.DEFAULT_USER');
|
||||
}
|
||||
const agentName = this.agentList.find(agent => agent.email === email)
|
||||
?.name;
|
||||
// If agent does not exist(removed/deleted), return email from audit log
|
||||
return agentName || email;
|
||||
},
|
||||
generateLogText(auditLogItem) {
|
||||
const agentName = this.getAgentName(auditLogItem.username);
|
||||
const auditableType = auditLogItem.auditable_type.toLowerCase();
|
||||
const action = auditLogItem.action.toLowerCase();
|
||||
const auditId = auditLogItem.auditable_id;
|
||||
const logActionKey = `${auditableType}:${action}`;
|
||||
|
||||
const translationPayload = {
|
||||
agentName,
|
||||
id: auditId,
|
||||
};
|
||||
|
||||
const translationKeys = {
|
||||
'automationrule:create': `AUDIT_LOGS.AUTOMATION_RULE.ADD`,
|
||||
'automationrule:update': `AUDIT_LOGS.AUTOMATION_RULE.EDIT`,
|
||||
'automationrule:destroy': `AUDIT_LOGS.AUTOMATION_RULE.DELETE`,
|
||||
'webhook:create': `AUDIT_LOGS.WEBHOOK.ADD`,
|
||||
'webhook:update': `AUDIT_LOGS.WEBHOOK.EDIT`,
|
||||
'webhook:destroy': `AUDIT_LOGS.WEBHOOK.DELETE`,
|
||||
'inbox:create': `AUDIT_LOGS.INBOX.ADD`,
|
||||
'inbox:update': `AUDIT_LOGS.INBOX.EDIT`,
|
||||
'inbox:destroy': `AUDIT_LOGS.INBOX.DELETE`,
|
||||
'user:sign_in': `AUDIT_LOGS.USER_ACTION.SIGN_IN`,
|
||||
'user:sign_out': `AUDIT_LOGS.USER_ACTION.SIGN_OUT`,
|
||||
'team:create': `AUDIT_LOGS.TEAM.ADD`,
|
||||
'team:update': `AUDIT_LOGS.TEAM.EDIT`,
|
||||
'team:destroy': `AUDIT_LOGS.TEAM.DELETE`,
|
||||
};
|
||||
|
||||
return this.$t(translationKeys[logActionKey] || '', translationPayload);
|
||||
},
|
||||
onPageChange(page) {
|
||||
window.history.pushState({}, null, `${this.$route.path}?page=${page}`);
|
||||
try {
|
||||
@@ -147,24 +101,12 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.audit-log--settings {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
|
||||
.remote-address {
|
||||
width: 14rem;
|
||||
}
|
||||
|
||||
.wrap-break-words {
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.column-activity {
|
||||
width: 60%;
|
||||
}
|
||||
<style scoped>
|
||||
.remote-address {
|
||||
width: 14rem;
|
||||
}
|
||||
.wrap-break-words {
|
||||
word-break: break-all;
|
||||
white-space: normal;
|
||||
}
|
||||
</style>
|
||||
|
||||
+6
-2
@@ -102,7 +102,9 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
isBusinessHoursEnabled: false,
|
||||
unavailableMessage: '',
|
||||
unavailableMessage: this.$t(
|
||||
'INBOX_MGMT.BUSINESS_HOURS.UNAVAILABLE_MESSAGE_DEFAULT'
|
||||
),
|
||||
timeZone: DEFAULT_TIMEZONE,
|
||||
dayNames: {
|
||||
0: 'Sunday',
|
||||
@@ -155,7 +157,9 @@ export default {
|
||||
? timeSlotParse(timeSlots)
|
||||
: defaultTimeSlot;
|
||||
this.isBusinessHoursEnabled = isEnabled;
|
||||
this.unavailableMessage = unavailableMessage || '';
|
||||
this.unavailableMessage =
|
||||
unavailableMessage ||
|
||||
this.$t('INBOX_MGMT.BUSINESS_HOURS.UNAVAILABLE_MESSAGE_DEFAULT');
|
||||
this.timeSlots = slots;
|
||||
this.timeZone =
|
||||
this.timeZones.find(item => timeZone === item.value) ||
|
||||
|
||||
@@ -140,20 +140,6 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
export: async ({ commit }) => {
|
||||
try {
|
||||
await ContactAPI.exportContacts();
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isCreating: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isCreating: false });
|
||||
if (error.response?.data?.message) {
|
||||
throw new Error(error.response.data.message);
|
||||
} else {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ commit }, id) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
|
||||
@@ -86,18 +86,6 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
fetchAllAttachments: async ({ commit }, conversationId) => {
|
||||
try {
|
||||
const { data } = await ConversationApi.getAllAttachments(conversationId);
|
||||
commit(types.SET_ALL_ATTACHMENTS, {
|
||||
id: conversationId,
|
||||
data: data.payload,
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle error
|
||||
}
|
||||
},
|
||||
|
||||
syncActiveConversationMessages: async (
|
||||
{ commit, state, dispatch },
|
||||
{ conversationId }
|
||||
@@ -259,10 +247,6 @@ const actions = {
|
||||
...response.data,
|
||||
status: MESSAGE_STATUS.SENT,
|
||||
});
|
||||
commit(types.ADD_CONVERSATION_ATTACHMENTS, {
|
||||
...response.data,
|
||||
status: MESSAGE_STATUS.SENT,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error.response
|
||||
? error.response.data.error
|
||||
@@ -285,7 +269,6 @@ const actions = {
|
||||
conversationId: message.conversation_id,
|
||||
canReply: true,
|
||||
});
|
||||
commit(types.ADD_CONVERSATION_ATTACHMENTS, message);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -300,7 +283,6 @@ const actions = {
|
||||
try {
|
||||
const { data } = await MessageApi.delete(conversationId, messageId);
|
||||
commit(types.ADD_MESSAGE, data);
|
||||
commit(types.DELETE_CONVERSATION_ATTACHMENTS, data);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
@@ -32,11 +32,6 @@ const getters = {
|
||||
);
|
||||
return selectedChat || {};
|
||||
},
|
||||
getSelectedChatAttachments: (_state, _getters) => {
|
||||
const selectedChat = _getters.getSelectedChat;
|
||||
const { attachments } = selectedChat;
|
||||
return attachments;
|
||||
},
|
||||
getLastEmailInSelectedChat: (stage, _getters) => {
|
||||
const selectedChat = _getters.getSelectedChat;
|
||||
const { messages = [] } = selectedChat;
|
||||
|
||||
@@ -3,7 +3,6 @@ import types from '../../mutation-types';
|
||||
import getters, { getSelectedChatConversation } from './getters';
|
||||
import actions from './actions';
|
||||
import { findPendingMessageIndex } from './helpers';
|
||||
import { MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { BUS_EVENTS } from '../../../../shared/constants/busEvents';
|
||||
|
||||
@@ -57,12 +56,6 @@ export const mutations = {
|
||||
chat.messages.unshift(...data);
|
||||
}
|
||||
},
|
||||
[types.SET_ALL_ATTACHMENTS](_state, { id, data }) {
|
||||
const [chat] = _state.allConversations.filter(c => c.id === id);
|
||||
if (!chat) return;
|
||||
Vue.set(chat, 'attachments', []);
|
||||
chat.attachments.push(...data);
|
||||
},
|
||||
[types.SET_MISSING_MESSAGES](_state, { id, data }) {
|
||||
const [chat] = _state.allConversations.filter(c => c.id === id);
|
||||
if (!chat) return;
|
||||
@@ -122,44 +115,6 @@ export const mutations = {
|
||||
Vue.set(chat, 'muted', false);
|
||||
},
|
||||
|
||||
[types.ADD_CONVERSATION_ATTACHMENTS]({ allConversations }, message) {
|
||||
const { conversation_id: conversationId } = message;
|
||||
const [chat] = getSelectedChatConversation({
|
||||
allConversations,
|
||||
selectedChatId: conversationId,
|
||||
});
|
||||
|
||||
if (!chat) return;
|
||||
|
||||
const isMessageSent =
|
||||
message.status === MESSAGE_STATUS.SENT && message.attachments;
|
||||
if (isMessageSent) {
|
||||
message.attachments.forEach(attachment => {
|
||||
if (!chat.attachments.some(a => a.id === attachment.id)) {
|
||||
chat.attachments.push(attachment);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
[types.DELETE_CONVERSATION_ATTACHMENTS]({ allConversations }, message) {
|
||||
const { conversation_id: conversationId } = message;
|
||||
const [chat] = getSelectedChatConversation({
|
||||
allConversations,
|
||||
selectedChatId: conversationId,
|
||||
});
|
||||
|
||||
if (!chat) return;
|
||||
|
||||
const isMessageSent = message.status === MESSAGE_STATUS.SENT;
|
||||
if (isMessageSent) {
|
||||
const attachmentIndex = chat.attachments.findIndex(
|
||||
a => a.message_id === message.id
|
||||
);
|
||||
if (attachmentIndex !== -1) chat.attachments.splice(attachmentIndex, 1);
|
||||
}
|
||||
},
|
||||
|
||||
[types.ADD_MESSAGE]({ allConversations, selectedChatId }, message) {
|
||||
const { conversation_id: conversationId } = message;
|
||||
const [chat] = getSelectedChatConversation({
|
||||
|
||||
@@ -49,18 +49,6 @@ export const actions = {
|
||||
commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: false });
|
||||
}
|
||||
},
|
||||
update: async function updateCustomViews({ commit }, obj) {
|
||||
commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
const response = await CustomViewsAPI.update(obj.id, obj);
|
||||
commit(types.UPDATE_CUSTOM_VIEW, response.data);
|
||||
} catch (error) {
|
||||
const errorMessage = error?.response?.data?.message;
|
||||
throw new Error(errorMessage);
|
||||
} finally {
|
||||
commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: false });
|
||||
}
|
||||
},
|
||||
delete: async ({ commit }, { id, filterType }) => {
|
||||
commit(types.SET_CUSTOM_VIEW_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
@@ -84,7 +72,6 @@ export const mutations = {
|
||||
|
||||
[types.ADD_CUSTOM_VIEW]: MutationHelpers.create,
|
||||
[types.SET_CUSTOM_VIEW]: MutationHelpers.set,
|
||||
[types.UPDATE_CUSTOM_VIEW]: MutationHelpers.update,
|
||||
[types.DELETE_CUSTOM_VIEW]: MutationHelpers.destroy,
|
||||
};
|
||||
|
||||
|
||||
@@ -204,7 +204,6 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#addMessage', () => {
|
||||
it('sends correct mutations if message is incoming', () => {
|
||||
const message = {
|
||||
@@ -219,7 +218,6 @@ describe('#actions', () => {
|
||||
types.SET_CONVERSATION_CAN_REPLY,
|
||||
{ conversationId: 1, canReply: true },
|
||||
],
|
||||
[types.ADD_CONVERSATION_ATTACHMENTS, message],
|
||||
]);
|
||||
});
|
||||
it('sends correct mutations if message is not an incoming message', () => {
|
||||
@@ -438,13 +436,10 @@ describe('#actions', () => {
|
||||
describe('#deleteMessage', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const [conversationId, messageId] = [1, 1];
|
||||
axios.delete.mockResolvedValue({
|
||||
data: { id: 1, content: 'deleted' },
|
||||
});
|
||||
axios.delete.mockResolvedValue({ data: { id: 1, content: 'deleted' } });
|
||||
await actions.deleteMessage({ commit }, { conversationId, messageId });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.ADD_MESSAGE, { id: 1, content: 'deleted' }],
|
||||
[types.DELETE_CONVERSATION_ATTACHMENTS, { id: 1, content: 'deleted' }],
|
||||
]);
|
||||
});
|
||||
it('sends no actions if API is error', async () => {
|
||||
@@ -559,40 +554,4 @@ describe('#addMentions', () => {
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
describe('#fetchAllAttachments', () => {
|
||||
it('fetches all attachments', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: {
|
||||
payload: [
|
||||
{
|
||||
id: 1,
|
||||
message_id: 1,
|
||||
file_type: 'image',
|
||||
data_url: '',
|
||||
thumb_url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
await actions.fetchAllAttachments({ commit }, 1);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[
|
||||
types.SET_ALL_ATTACHMENTS,
|
||||
{
|
||||
id: 1,
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
message_id: 1,
|
||||
file_type: 'image',
|
||||
data_url: '',
|
||||
thumb_url: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -305,34 +305,4 @@ describe('#getters', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getSelectedChatAttachments', () => {
|
||||
it('Returns attachments in selected chat', () => {
|
||||
const state = {};
|
||||
const getSelectedChat = {
|
||||
attachments: [
|
||||
{
|
||||
id: 1,
|
||||
file_name: 'test1',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
file_name: 'test2',
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(
|
||||
getters.getSelectedChatAttachments(state, { getSelectedChat })
|
||||
).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
file_name: 'test1',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
file_name: 'test2',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -278,129 +278,4 @@ describe('#mutations', () => {
|
||||
expect(state.appliedFilters).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ALL_ATTACHMENTS', () => {
|
||||
it('set all attachments', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1 }],
|
||||
};
|
||||
const data = [{ id: 1, name: 'test' }];
|
||||
mutations[types.SET_ALL_ATTACHMENTS](state, { id: 1, data });
|
||||
expect(state.allConversations[0].attachments).toEqual(data);
|
||||
});
|
||||
it('set attachments key even if the attachments are empty', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1 }],
|
||||
};
|
||||
const data = [];
|
||||
mutations[types.SET_ALL_ATTACHMENTS](state, { id: 1, data });
|
||||
expect(state.allConversations[0].attachments).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ADD_CONVERSATION_ATTACHMENTS', () => {
|
||||
it('add conversation attachments', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, attachments: [] }],
|
||||
};
|
||||
const message = {
|
||||
conversation_id: 1,
|
||||
status: 'sent',
|
||||
attachments: [{ id: 1, name: 'test' }],
|
||||
};
|
||||
|
||||
mutations[types.ADD_CONVERSATION_ATTACHMENTS](state, message);
|
||||
expect(state.allConversations[0].attachments).toEqual(
|
||||
message.attachments
|
||||
);
|
||||
});
|
||||
|
||||
it('should not add duplicate attachments', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{
|
||||
id: 1,
|
||||
attachments: [{ id: 1, name: 'existing' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const message = {
|
||||
conversation_id: 1,
|
||||
status: 'sent',
|
||||
attachments: [
|
||||
{ id: 1, name: 'existing' },
|
||||
{ id: 2, name: 'new' },
|
||||
],
|
||||
};
|
||||
|
||||
mutations[types.ADD_CONVERSATION_ATTACHMENTS](state, message);
|
||||
expect(state.allConversations[0].attachments).toHaveLength(2);
|
||||
expect(state.allConversations[0].attachments).toContainEqual({
|
||||
id: 1,
|
||||
name: 'existing',
|
||||
});
|
||||
expect(state.allConversations[0].attachments).toContainEqual({
|
||||
id: 2,
|
||||
name: 'new',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not add attachments if chat not found', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, attachments: [] }],
|
||||
};
|
||||
const message = {
|
||||
conversation_id: 2,
|
||||
status: 'sent',
|
||||
attachments: [{ id: 1, name: 'test' }],
|
||||
};
|
||||
|
||||
mutations[types.ADD_CONVERSATION_ATTACHMENTS](state, message);
|
||||
expect(state.allConversations[0].attachments).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#DELETE_CONVERSATION_ATTACHMENTS', () => {
|
||||
it('delete conversation attachments', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, attachments: [{ id: 1, message_id: 1 }] }],
|
||||
};
|
||||
const message = {
|
||||
conversation_id: 1,
|
||||
status: 'sent',
|
||||
id: 1,
|
||||
};
|
||||
|
||||
mutations[types.DELETE_CONVERSATION_ATTACHMENTS](state, message);
|
||||
expect(state.allConversations[0].attachments).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not delete attachments for non-matching message id', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, attachments: [{ id: 1, message_id: 1 }] }],
|
||||
};
|
||||
const message = {
|
||||
conversation_id: 1,
|
||||
status: 'sent',
|
||||
id: 2,
|
||||
};
|
||||
|
||||
mutations[types.DELETE_CONVERSATION_ATTACHMENTS](state, message);
|
||||
expect(state.allConversations[0].attachments).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not delete attachments if chat not found', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, attachments: [{ id: 1, message_id: 1 }] }],
|
||||
};
|
||||
const message = {
|
||||
conversation_id: 2,
|
||||
status: 'sent',
|
||||
id: 1,
|
||||
};
|
||||
|
||||
mutations[types.DELETE_CONVERSATION_ATTACHMENTS](state, message);
|
||||
expect(state.allConversations[0].attachments).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import { actions } from '../../customViews';
|
||||
import * as types from '../../../mutation-types';
|
||||
import { customViewList, updateCustomViewList } from './fixtures';
|
||||
import customViewList from './fixtures';
|
||||
|
||||
const commit = jest.fn();
|
||||
global.axios = axios;
|
||||
@@ -67,28 +67,4 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.patch.mockResolvedValue({ data: updateCustomViewList[0] });
|
||||
await actions.update(
|
||||
{ commit },
|
||||
updateCustomViewList[0].id,
|
||||
updateCustomViewList[0]
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: true }],
|
||||
[types.default.UPDATE_CUSTOM_VIEW, updateCustomViewList[0]],
|
||||
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(actions.update({ commit }, 1)).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: true }],
|
||||
[types.default.SET_CUSTOM_VIEW_UI_FLAG, { isCreating: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const customViewList = [
|
||||
export default [
|
||||
{
|
||||
name: 'Custom view',
|
||||
filter_type: 0,
|
||||
@@ -34,31 +34,3 @@ export const customViewList = [
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const updateCustomViewList = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Open',
|
||||
filter_type: 'conversation',
|
||||
query: {
|
||||
payload: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
attribute_model: 'standard',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['open'],
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
{
|
||||
attribute_key: 'assignee_id',
|
||||
filter_operator: 'equal_to',
|
||||
values: [52],
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
created_at: '2022-02-08T03:17:38.761Z',
|
||||
updated_at: '2023-06-05T13:57:48.478Z',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getters } from '../../customViews';
|
||||
import { customViewList } from './fixtures';
|
||||
import customViewList from './fixtures';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getCustomViews', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import types from '../../../mutation-types';
|
||||
import { mutations } from '../../customViews';
|
||||
import { customViewList, updateCustomViewList } from './fixtures';
|
||||
import customViewList from './fixtures';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#SET_CUSTOM_VIEW', () => {
|
||||
@@ -26,12 +26,4 @@ describe('#mutations', () => {
|
||||
expect(state.records).toEqual([customViewList[0]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UPDATE_CUSTOM_VIEW', () => {
|
||||
it('update custom view record', () => {
|
||||
const state = { records: [updateCustomViewList[0]] };
|
||||
mutations[types.UPDATE_CUSTOM_VIEW](state, updateCustomViewList[0]);
|
||||
expect(state.records).toEqual(updateCustomViewList);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,10 +49,6 @@ export default {
|
||||
UPDATE_CONVERSATION_LAST_ACTIVITY: 'UPDATE_CONVERSATION_LAST_ACTIVITY',
|
||||
SET_MISSING_MESSAGES: 'SET_MISSING_MESSAGES',
|
||||
|
||||
SET_ALL_ATTACHMENTS: 'SET_ALL_ATTACHMENTS',
|
||||
ADD_CONVERSATION_ATTACHMENTS: 'ADD_CONVERSATION_ATTACHMENTS',
|
||||
DELETE_CONVERSATION_ATTACHMENTS: 'DELETE_CONVERSATION_ATTACHMENTS',
|
||||
|
||||
SET_CONVERSATION_CAN_REPLY: 'SET_CONVERSATION_CAN_REPLY',
|
||||
|
||||
// Inboxes
|
||||
@@ -221,7 +217,6 @@ export default {
|
||||
SET_CUSTOM_VIEW_UI_FLAG: 'SET_CUSTOM_VIEW_UI_FLAG',
|
||||
SET_CUSTOM_VIEW: 'SET_CUSTOM_VIEW',
|
||||
ADD_CUSTOM_VIEW: 'ADD_CUSTOM_VIEW',
|
||||
UPDATE_CUSTOM_VIEW: 'UPDATE_CUSTOM_VIEW',
|
||||
DELETE_CUSTOM_VIEW: 'DELETE_CUSTOM_VIEW',
|
||||
|
||||
// Bulk Actions
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
hasUserKeys,
|
||||
} from '../sdk/cookieHelpers';
|
||||
import { addClasses, removeClasses } from '../sdk/DOMHelpers';
|
||||
import { setCookieWithDomain } from '../sdk/cookieHelpers';
|
||||
import { SDK_SET_BUBBLE_VISIBILITY } from 'shared/constants/sharedFrameEvents';
|
||||
const runSDK = ({ baseUrl, websiteToken }) => {
|
||||
if (window.$chatwoot) {
|
||||
@@ -20,7 +19,6 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
||||
|
||||
const chatwootSettings = window.chatwootSettings || {};
|
||||
let locale = chatwootSettings.locale;
|
||||
let baseDomain = chatwootSettings.baseDomain;
|
||||
|
||||
if (chatwootSettings.useBrowserLanguage) {
|
||||
locale = window.navigator.language.replace('-', '_');
|
||||
@@ -28,7 +26,6 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
||||
|
||||
window.$chatwoot = {
|
||||
baseUrl,
|
||||
baseDomain,
|
||||
hasLoaded: false,
|
||||
hideMessageBubble: chatwootSettings.hideMessageBubble || false,
|
||||
isOpen: false,
|
||||
@@ -93,9 +90,9 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
||||
window.$chatwoot.identifier = identifier;
|
||||
window.$chatwoot.user = user;
|
||||
IFrameHelper.sendMessage('set-user', { identifier, user });
|
||||
|
||||
setCookieWithDomain(userCookieName, hashToBeStored, {
|
||||
baseDomain,
|
||||
Cookies.set(userCookieName, hashToBeStored, {
|
||||
expires: 365,
|
||||
sameSite: 'Lax',
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
CHATWOOT_READY,
|
||||
} from '../widget/constants/sdkEvents';
|
||||
import { SET_USER_ERROR } from '../widget/constants/errorTypes';
|
||||
import { getUserCookieName, setCookieWithDomain } from './cookieHelpers';
|
||||
import { getUserCookieName } from './cookieHelpers';
|
||||
import {
|
||||
getAlertAudio,
|
||||
initOnEvents,
|
||||
@@ -38,16 +38,17 @@ import { isFlatWidgetStyle } from './settingsHelper';
|
||||
import { popoutChatWindow } from '../widget/helpers/popoutHelper';
|
||||
import addHours from 'date-fns/addHours';
|
||||
|
||||
const updateAuthCookie = (cookieContent, baseDomain = '') =>
|
||||
setCookieWithDomain('cw_conversation', cookieContent, {
|
||||
baseDomain,
|
||||
const updateAuthCookie = cookieContent =>
|
||||
Cookies.set('cw_conversation', cookieContent, {
|
||||
expires: 365,
|
||||
sameSite: 'Lax',
|
||||
});
|
||||
|
||||
const updateCampaignReadStatus = baseDomain => {
|
||||
const updateCampaignReadStatus = () => {
|
||||
const expireBy = addHours(new Date(), 1);
|
||||
setCookieWithDomain('cw_snooze_campaigns_till', Number(expireBy), {
|
||||
Cookies.set('cw_snooze_campaigns_till', Number(expireBy), {
|
||||
expires: expireBy,
|
||||
baseDomain,
|
||||
sameSite: 'Lax',
|
||||
});
|
||||
};
|
||||
|
||||
@@ -153,7 +154,7 @@ export const IFrameHelper = {
|
||||
|
||||
events: {
|
||||
loaded: message => {
|
||||
updateAuthCookie(message.config.authToken, window.$chatwoot.baseDomain);
|
||||
updateAuthCookie(message.config.authToken);
|
||||
window.$chatwoot.hasLoaded = true;
|
||||
const campaignsSnoozedTill = Cookies.get('cw_snooze_campaigns_till');
|
||||
IFrameHelper.sendMessage('config-set', {
|
||||
@@ -199,11 +200,11 @@ export const IFrameHelper = {
|
||||
},
|
||||
|
||||
setAuthCookie({ data: { widgetAuthToken } }) {
|
||||
updateAuthCookie(widgetAuthToken, window.$chatwoot.baseDomain);
|
||||
updateAuthCookie(widgetAuthToken);
|
||||
},
|
||||
|
||||
setCampaignReadOn() {
|
||||
updateCampaignReadStatus(window.$chatwoot.baseDomain);
|
||||
updateCampaignReadStatus();
|
||||
},
|
||||
|
||||
toggleBubble: state => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import md5 from 'md5';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
const REQUIRED_USER_KEYS = ['avatar_url', 'email', 'name'];
|
||||
const ALLOWED_USER_ATTRIBUTES = [...REQUIRED_USER_KEYS, 'identifier_hash'];
|
||||
@@ -22,17 +21,3 @@ export const computeHashForUserData = (...args) => md5(getUserString(...args));
|
||||
|
||||
export const hasUserKeys = user =>
|
||||
REQUIRED_USER_KEYS.reduce((acc, key) => acc || !!user[key], false);
|
||||
|
||||
export const setCookieWithDomain = (
|
||||
name,
|
||||
value,
|
||||
{ expires = 365, baseDomain = undefined } = {}
|
||||
) => {
|
||||
const cookieOptions = {
|
||||
expires,
|
||||
sameSite: 'Lax',
|
||||
domain: baseDomain,
|
||||
};
|
||||
|
||||
Cookies.set(name, value, cookieOptions);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import Cookies from 'js-cookie';
|
||||
import {
|
||||
getUserCookieName,
|
||||
getUserString,
|
||||
hasUserKeys,
|
||||
setCookieWithDomain,
|
||||
} from '../cookieHelpers';
|
||||
|
||||
describe('#getUserCookieName', () => {
|
||||
@@ -49,78 +47,3 @@ describe('#hasUserKeys', () => {
|
||||
expect(hasUserKeys({ avatar_url: 'randomValue' })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Mock the 'set' method of the 'Cookies' object
|
||||
jest.mock('js-cookie', () => ({
|
||||
set: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('setCookieWithDomain', () => {
|
||||
afterEach(() => {
|
||||
// Clear mock calls after each test
|
||||
Cookies.set.mockClear();
|
||||
});
|
||||
|
||||
it('should set a cookie with default parameters', () => {
|
||||
setCookieWithDomain('myCookie', 'cookieValue');
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith(
|
||||
'myCookie',
|
||||
'cookieValue',
|
||||
expect.objectContaining({
|
||||
expires: 365,
|
||||
sameSite: 'Lax',
|
||||
domain: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should set a cookie with custom expiration and sameSite attribute', () => {
|
||||
setCookieWithDomain('myCookie', 'cookieValue', {
|
||||
expires: 30,
|
||||
});
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith(
|
||||
'myCookie',
|
||||
'cookieValue',
|
||||
expect.objectContaining({
|
||||
expires: 30,
|
||||
sameSite: 'Lax',
|
||||
domain: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should set a cookie with a specific base domain', () => {
|
||||
setCookieWithDomain('myCookie', 'cookieValue', {
|
||||
baseDomain: 'example.com',
|
||||
});
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith(
|
||||
'myCookie',
|
||||
'cookieValue',
|
||||
expect.objectContaining({
|
||||
expires: 365,
|
||||
sameSite: 'Lax',
|
||||
domain: 'example.com',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should set a cookie with custom expiration, sameSite attribute, and specific base domain', () => {
|
||||
setCookieWithDomain('myCookie', 'cookieValue', {
|
||||
expires: 7,
|
||||
baseDomain: 'example.com',
|
||||
});
|
||||
|
||||
expect(Cookies.set).toHaveBeenCalledWith(
|
||||
'myCookie',
|
||||
'cookieValue',
|
||||
expect.objectContaining({
|
||||
expires: 7,
|
||||
sameSite: 'Lax',
|
||||
domain: 'example.com',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,14 +94,6 @@ export const hasPressedArrowDownKey = e => {
|
||||
return e.keyCode === 40;
|
||||
};
|
||||
|
||||
export const hasPressedArrowLeftKey = e => {
|
||||
return e.keyCode === 37;
|
||||
};
|
||||
|
||||
export const hasPressedArrowRightKey = e => {
|
||||
return e.keyCode === 39;
|
||||
};
|
||||
|
||||
export const hasPressedCommandPlusKKey = e => {
|
||||
return e.metaKey && e.keyCode === 75;
|
||||
};
|
||||
|
||||
@@ -4,9 +4,8 @@ export const LocalStorage = {
|
||||
},
|
||||
|
||||
get(key) {
|
||||
let value = null;
|
||||
const value = window.localStorage.getItem(key);
|
||||
try {
|
||||
value = window.localStorage.getItem(key);
|
||||
return typeof value === 'string' ? JSON.parse(value) : value;
|
||||
} catch (error) {
|
||||
return value;
|
||||
|
||||
@@ -42,10 +42,11 @@
|
||||
</div>
|
||||
<p
|
||||
v-if="message.showAvatar || hasRecordedResponse"
|
||||
v-dompurify-html="agentName"
|
||||
class="agent-name"
|
||||
:class="$dm('text-slate-700', 'dark:text-slate-200')"
|
||||
/>
|
||||
>
|
||||
{{ agentName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
class Account::ContactsExportJob < ApplicationJob
|
||||
queue_as :low
|
||||
|
||||
def perform(account_id, column_names)
|
||||
account = Account.find(account_id)
|
||||
headers = valid_headers(column_names)
|
||||
generate_csv(account, headers)
|
||||
file_url = account_contact_export_url(account)
|
||||
|
||||
AdministratorNotifications::ChannelNotificationsMailer.with(account: account).contact_export_complete(file_url)&.deliver_later
|
||||
end
|
||||
|
||||
def generate_csv(account, headers)
|
||||
csv_data = CSV.generate do |csv|
|
||||
csv << headers
|
||||
account.contacts.each do |contact|
|
||||
csv << headers.map { |header| contact.send(header) }
|
||||
end
|
||||
end
|
||||
|
||||
attach_export_file(account, csv_data)
|
||||
end
|
||||
|
||||
def valid_headers(column_names)
|
||||
columns = (column_names.presence || default_columns)
|
||||
headers = columns.map { |column| column if Contact.column_names.include?(column) }
|
||||
headers.compact
|
||||
end
|
||||
|
||||
def attach_export_file(account, csv_data)
|
||||
return if csv_data.blank?
|
||||
|
||||
account.contacts_export.attach(
|
||||
io: StringIO.new(csv_data),
|
||||
filename: "#{account.name}_#{account.id}_contacts.csv",
|
||||
content_type: 'text/csv'
|
||||
)
|
||||
end
|
||||
|
||||
def account_contact_export_url(account)
|
||||
Rails.application.routes.url_helpers.rails_blob_url(account.contacts_export)
|
||||
end
|
||||
|
||||
def default_columns
|
||||
%w[id name email phone_number]
|
||||
end
|
||||
end
|
||||
@@ -3,8 +3,10 @@ class Channels::Whatsapp::TemplatesSyncSchedulerJob < ApplicationJob
|
||||
|
||||
def perform
|
||||
Channel::Whatsapp.where('message_templates_last_updated <= ? OR message_templates_last_updated IS NULL',
|
||||
3.hours.ago).limit(Limits::BULK_EXTERNAL_HTTP_CALLS_LIMIT).all.each do |channel|
|
||||
Channels::Whatsapp::TemplatesSyncJob.perform_later(channel)
|
||||
15.minutes.ago).find_in_batches do |channels_batch|
|
||||
channels_batch.each do |channel|
|
||||
Channels::Whatsapp::TemplatesSyncJob.perform_later(channel)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,16 +40,14 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
|
||||
received_mails(imap_inbox).each do |message_id|
|
||||
inbound_mail = Mail.read_from_string imap_inbox.fetch(message_id, 'RFC822')[0].attr['RFC822']
|
||||
|
||||
mail_info_logger(channel, inbound_mail, message_id)
|
||||
|
||||
next if email_already_present?(channel, inbound_mail, last_email_time)
|
||||
|
||||
process_mail(inbound_mail, channel)
|
||||
end
|
||||
end
|
||||
|
||||
def email_already_present?(channel, inbound_mail, _last_email_time)
|
||||
channel.inbox.messages.find_by(source_id: inbound_mail.message_id).present?
|
||||
def email_already_present?(channel, inbound_mail, last_email_time)
|
||||
processed_email?(inbound_mail, last_email_time) || channel.inbox.messages.find_by(source_id: inbound_mail.message_id).present?
|
||||
end
|
||||
|
||||
def received_mails(imap_inbox)
|
||||
@@ -78,21 +76,12 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
|
||||
received_mails(imap_inbox).each do |message_id|
|
||||
inbound_mail = Mail.read_from_string imap_inbox.fetch(message_id, 'RFC822')[0].attr['RFC822']
|
||||
|
||||
mail_info_logger(channel, inbound_mail, message_id)
|
||||
|
||||
next if channel.inbox.messages.find_by(source_id: inbound_mail.message_id).present?
|
||||
|
||||
process_mail(inbound_mail, channel)
|
||||
end
|
||||
end
|
||||
|
||||
def mail_info_logger(channel, inbound_mail, message_id)
|
||||
return if Rails.env.test?
|
||||
|
||||
Rails.logger.info("
|
||||
#{channel.provider} Email id: #{inbound_mail.from} and message_source_id: #{inbound_mail.message_id}, message_id: #{message_id}")
|
||||
end
|
||||
|
||||
def authenticated_imap_inbox(channel, access_token, auth_method)
|
||||
imap = Net::IMAP.new(channel.imap_address, channel.imap_port, true)
|
||||
imap.authenticate(auth_method, channel.imap_login, access_token)
|
||||
@@ -124,8 +113,6 @@ class Inboxes::FetchImapEmailsJob < ApplicationJob
|
||||
Imap::ImapMailbox.new.process(inbound_mail, channel)
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e, account: channel.account).capture_exception
|
||||
Rails.logger.error("
|
||||
#{channel.provider} Email dropped: #{inbound_mail.from} and message_source_id: #{inbound_mail.message_id}")
|
||||
end
|
||||
|
||||
# Making sure the access token is valid for microsoft provider
|
||||
|
||||
@@ -45,49 +45,23 @@ class Imap::ImapMailbox
|
||||
end
|
||||
end
|
||||
|
||||
def find_conversation_by_reference_ids
|
||||
return if @inbound_mail.references.blank? && in_reply_to.present?
|
||||
|
||||
message = find_message_by_references
|
||||
|
||||
return if message.nil?
|
||||
|
||||
@inbox.conversations.find(message.conversation_id)
|
||||
end
|
||||
|
||||
def in_reply_to
|
||||
@inbound_mail.in_reply_to
|
||||
end
|
||||
|
||||
def find_message_by_references
|
||||
message_to_return = nil
|
||||
|
||||
references = Array.wrap(@inbound_mail.references)
|
||||
|
||||
references.each do |message_id|
|
||||
message = @inbox.messages.find_by(source_id: message_id)
|
||||
message_to_return = message if message.present?
|
||||
end
|
||||
message_to_return
|
||||
end
|
||||
|
||||
def find_or_create_conversation
|
||||
@conversation = find_conversation_by_in_reply_to || find_conversation_by_reference_ids || ::Conversation.create!(
|
||||
{
|
||||
account_id: @account.id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: @contact.id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: {
|
||||
source: 'email',
|
||||
in_reply_to: in_reply_to,
|
||||
mail_subject: @processed_mail.subject,
|
||||
initiated_at: {
|
||||
timestamp: Time.now.utc
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@conversation = find_conversation_by_in_reply_to || ::Conversation.create!({ account_id: @account.id,
|
||||
inbox_id: @inbox.id,
|
||||
contact_id: @contact.id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: {
|
||||
source: 'email',
|
||||
in_reply_to: in_reply_to,
|
||||
mail_subject: @processed_mail.subject,
|
||||
initiated_at: {
|
||||
timestamp: Time.now.utc
|
||||
}
|
||||
} })
|
||||
end
|
||||
|
||||
def find_or_create_contact
|
||||
|
||||
@@ -24,53 +24,15 @@ module MailboxHelper
|
||||
return if @message.blank?
|
||||
|
||||
processed_mail.attachments.last(Message::NUMBER_OF_PERMITTED_ATTACHMENTS).each do |mail_attachment|
|
||||
if inline_attachment?(mail_attachment)
|
||||
embed_inline_image_source(mail_attachment)
|
||||
else
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @conversation.account_id,
|
||||
file_type: 'file'
|
||||
)
|
||||
attachment.file.attach(mail_attachment[:blob])
|
||||
end
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @conversation.account_id,
|
||||
file_type: 'file'
|
||||
)
|
||||
attachment.file.attach(mail_attachment[:blob])
|
||||
end
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def embed_inline_image_source(mail_attachment)
|
||||
if processed_mail.serialized_data[:html_content].present?
|
||||
upload_inline_image(mail_attachment)
|
||||
elsif processed_mail.serialized_data[:text_content].present?
|
||||
embed_plain_text_email_with_inline_image(mail_attachment)
|
||||
end
|
||||
end
|
||||
|
||||
def upload_inline_image(mail_attachment)
|
||||
content_id = mail_attachment[:original].cid
|
||||
|
||||
@message.content_attributes[:email][:html_content][:full] = processed_mail.serialized_data[:html_content][:full].gsub(
|
||||
"cid:#{content_id}", inline_image_url(mail_attachment[:blob]).to_s
|
||||
)
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def inline_attachment?(mail_attachment)
|
||||
mail_attachment[:original].inline?
|
||||
end
|
||||
|
||||
def embed_plain_text_email_with_inline_image(mail_attachment)
|
||||
attachment_name = mail_attachment[:original].filename
|
||||
|
||||
@message.content_attributes[:email][:text_content][:full] = processed_mail.serialized_data[:text_content][:reply].gsub(
|
||||
"[image: #{attachment_name}]", "<img src=\"#{inline_image_url(mail_attachment[:blob])}\" alt=\"#{attachment_name}>\""
|
||||
)
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def inline_image_url(blob)
|
||||
Rails.application.routes.url_helpers.url_for(blob)
|
||||
end
|
||||
|
||||
def create_contact
|
||||
@contact_inbox = ::ContactInboxWithContactBuilder.new(
|
||||
source_id: processed_mail.original_sender,
|
||||
|
||||
@@ -43,14 +43,6 @@ class AdministratorNotifications::ChannelNotificationsMailer < ApplicationMailer
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
end
|
||||
|
||||
def contact_export_complete(file_url)
|
||||
return unless smtp_config_set_or_development?
|
||||
|
||||
@action_url = file_url
|
||||
subject = "Your contact's export file is available to download."
|
||||
send_mail_with_liquid(to: admin_emails, subject: subject) and return
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def admin_emails
|
||||
|
||||
@@ -79,12 +79,8 @@ class ConversationReplyMailer < ApplicationMailer
|
||||
@conversation.messages.chat.where.not(message_type: :incoming)&.last
|
||||
end
|
||||
|
||||
def sender_name
|
||||
@sender_name ||= current_message&.sender&.available_name || @agent&.available_name || 'Notifications'
|
||||
end
|
||||
|
||||
def current_message
|
||||
@message || @conversation.messages.outgoing.last
|
||||
def assignee_name
|
||||
@assignee_name ||= @agent&.available_name || 'Notifications'
|
||||
end
|
||||
|
||||
def mail_subject
|
||||
@@ -101,7 +97,7 @@ class ConversationReplyMailer < ApplicationMailer
|
||||
|
||||
def reply_email
|
||||
if should_use_conversation_email_address?
|
||||
I18n.t('conversations.reply.email.header.reply_with_name', assignee_name: sender_name, inbox_name: @inbox.name,
|
||||
I18n.t('conversations.reply.email.header.reply_with_name', assignee_name: assignee_name, inbox_name: @inbox.name,
|
||||
reply_email: "#{@conversation.uuid}@#{@account.inbound_email_domain}")
|
||||
else
|
||||
@inbox.email_address || @agent&.email
|
||||
@@ -110,17 +106,21 @@ class ConversationReplyMailer < ApplicationMailer
|
||||
|
||||
def from_email_with_name
|
||||
if should_use_conversation_email_address?
|
||||
I18n.t('conversations.reply.email.header.from_with_name', assignee_name: sender_name, inbox_name: @inbox.name,
|
||||
I18n.t('conversations.reply.email.header.from_with_name', assignee_name: assignee_name, inbox_name: @inbox.name,
|
||||
from_email: parse_email(@account.support_email))
|
||||
else
|
||||
I18n.t('conversations.reply.email.header.from_with_name', assignee_name: sender_name, inbox_name: @inbox.name,
|
||||
I18n.t('conversations.reply.email.header.from_with_name', assignee_name: assignee_name, inbox_name: @inbox.name,
|
||||
from_email: parse_email(inbox_from_email_address))
|
||||
end
|
||||
end
|
||||
|
||||
def channel_email_with_name
|
||||
I18n.t('conversations.reply.channel_email.header.reply_with_name', assignee_name: sender_name, inbox_name: @inbox.name,
|
||||
from_email: @channel.email)
|
||||
if @conversation.assignee.present?
|
||||
I18n.t('conversations.reply.channel_email.header.reply_with_name', assignee_name: assignee_name, inbox_name: @inbox.name,
|
||||
from_email: @channel.email)
|
||||
else
|
||||
I18n.t('conversations.reply.channel_email.header.reply_with_inbox_name', inbox_name: @inbox.name, from_email: @channel.email)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_email(email_string)
|
||||
|
||||
@@ -77,8 +77,6 @@ class Account < ApplicationRecord
|
||||
has_many :whatsapp_channels, dependent: :destroy_async, class_name: '::Channel::Whatsapp'
|
||||
has_many :working_hours, dependent: :destroy_async
|
||||
|
||||
has_one_attached :contacts_export
|
||||
|
||||
enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h
|
||||
enum status: { active: 0, suspended: 1 }
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ class AgentBot < ApplicationRecord
|
||||
enum bot_type: { webhook: 0, csml: 1 }
|
||||
|
||||
validate :validate_agent_bot_config
|
||||
validates :outgoing_url, length: { maximum: Limits::URL_LENGTH_LIMIT }
|
||||
|
||||
def available_name
|
||||
name
|
||||
|
||||
@@ -37,7 +37,7 @@ class Attachment < ApplicationRecord
|
||||
belongs_to :message
|
||||
has_one_attached :file
|
||||
validate :acceptable_file
|
||||
validates :external_url, length: { maximum: Limits::URL_LENGTH_LIMIT }
|
||||
validates :external_url, length: { maximum: 1000 }
|
||||
enum file_type: [:image, :audio, :video, :file, :location, :fallback, :share, :story_mention, :contact]
|
||||
|
||||
def push_event_data
|
||||
|
||||
@@ -27,7 +27,6 @@ class Channel::Api < ApplicationRecord
|
||||
has_secure_token :identifier
|
||||
has_secure_token :hmac_token
|
||||
validate :ensure_valid_agent_reply_time_window
|
||||
validates :webhook_url, length: { maximum: Limits::URL_LENGTH_LIMIT }
|
||||
|
||||
def name
|
||||
'API'
|
||||
|
||||
@@ -50,12 +50,6 @@ class Channel::Whatsapp < ApplicationRecord
|
||||
true
|
||||
end
|
||||
|
||||
def mark_message_templates_updated
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
update_column(:message_templates_last_updated, Time.zone.now)
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
delegate :send_message, to: :provider_service
|
||||
delegate :send_template, to: :provider_service
|
||||
delegate :sync_templates, to: :provider_service
|
||||
|
||||
@@ -4,41 +4,29 @@ module CacheKeys
|
||||
include CacheKeysHelper
|
||||
include Events::Types
|
||||
|
||||
included do
|
||||
class_attribute :cacheable_models
|
||||
self.cacheable_models = [Label, Inbox, Team]
|
||||
end
|
||||
|
||||
def cache_keys
|
||||
keys = {}
|
||||
self.class.cacheable_models.each do |model|
|
||||
keys[model.name.underscore.to_sym] = fetch_value_for_key(id, model.name.underscore)
|
||||
end
|
||||
|
||||
keys
|
||||
{
|
||||
label: fetch_value_for_key(id, Label.name.underscore),
|
||||
inbox: fetch_value_for_key(id, Inbox.name.underscore),
|
||||
team: fetch_value_for_key(id, Team.name.underscore)
|
||||
}
|
||||
end
|
||||
|
||||
def invalidate_cache_key_for(key)
|
||||
prefixed_cache_key = get_prefixed_cache_key(id, key)
|
||||
Redis::Alfred.delete(prefixed_cache_key)
|
||||
dispatch_cache_update_event
|
||||
Redis::Alfred.del(prefixed_cache_key)
|
||||
dispatch_cache_udpate_event
|
||||
end
|
||||
|
||||
def update_cache_key(key)
|
||||
prefixed_cache_key = get_prefixed_cache_key(id, key)
|
||||
Redis::Alfred.set(prefixed_cache_key, Time.now.utc.to_i)
|
||||
dispatch_cache_update_event
|
||||
end
|
||||
|
||||
def reset_cache_keys
|
||||
self.class.cacheable_models.each do |model|
|
||||
invalidate_cache_key_for(model.name.underscore)
|
||||
end
|
||||
dispatch_cache_udpate_event
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def dispatch_cache_update_event
|
||||
def dispatch_cache_udpate_event
|
||||
Rails.configuration.dispatcher.dispatch(ACCOUNT_CACHE_INVALIDATED, Time.zone.now, cache_keys: cache_keys, account: self)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,6 +2,7 @@ module Liquidable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
acts_as_taggable_on :labels
|
||||
before_create :process_liquid_in_content
|
||||
end
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ module MessageFilterHelpers
|
||||
end
|
||||
|
||||
def webhook_sendable?
|
||||
incoming? || outgoing? || template?
|
||||
incoming? || outgoing?
|
||||
end
|
||||
|
||||
def slack_hook_sendable?
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
# index_contacts_on_account_id (account_id)
|
||||
# index_contacts_on_lower_email_account_id (lower((email)::text), account_id)
|
||||
# index_contacts_on_name_email_phone_number_identifier (name,email,phone_number,identifier) USING gin
|
||||
# index_contacts_on_nonempty_fields (account_id,email,phone_number,identifier) WHERE (((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text)) # rubocop:disable Layout/LineLength
|
||||
# index_contacts_on_identifiable_fields (account_id,email,phone_number,identifier) WHERE (((email)::text <> ''::text) OR ((phone_number)::text <> ''::text) OR ((identifier)::text <> ''::text)) # rubocop:disable Layout/LineLength
|
||||
# index_contacts_on_phone_number_and_account_id (phone_number,account_id)
|
||||
# uniq_email_per_account_contact (email,account_id) UNIQUE
|
||||
# uniq_identifier_per_account_contact (identifier,account_id) UNIQUE
|
||||
|
||||
@@ -69,7 +69,6 @@ class Conversation < ApplicationRecord
|
||||
scope :unassigned, -> { where(assignee_id: nil) }
|
||||
scope :assigned, -> { where.not(assignee_id: nil) }
|
||||
scope :assigned_to, ->(agent) { where(assignee_id: agent.id) }
|
||||
scope :unattended, -> { where(first_reply_created_at: nil) }
|
||||
scope :resolvable, lambda { |auto_resolve_duration|
|
||||
return none if auto_resolve_duration.to_i.zero?
|
||||
|
||||
|
||||
+17
-28
@@ -2,24 +2,23 @@
|
||||
#
|
||||
# Table name: messages
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# additional_attributes :jsonb
|
||||
# content :text
|
||||
# content_attributes :json
|
||||
# content_type :integer default("text"), not null
|
||||
# external_source_ids :jsonb
|
||||
# message_type :integer not null
|
||||
# private :boolean default(FALSE)
|
||||
# processed_message_content :text
|
||||
# sender_type :string
|
||||
# status :integer default("sent")
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer not null
|
||||
# conversation_id :integer not null
|
||||
# inbox_id :integer not null
|
||||
# sender_id :bigint
|
||||
# source_id :string
|
||||
# id :integer not null, primary key
|
||||
# additional_attributes :jsonb
|
||||
# content :text
|
||||
# content_attributes :json
|
||||
# content_type :integer default("text"), not null
|
||||
# external_source_ids :jsonb
|
||||
# message_type :integer not null
|
||||
# private :boolean default(FALSE)
|
||||
# sender_type :string
|
||||
# status :integer default("sent")
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer not null
|
||||
# conversation_id :integer not null
|
||||
# inbox_id :integer not null
|
||||
# sender_id :bigint
|
||||
# source_id :string
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
@@ -58,7 +57,6 @@ class Message < ApplicationRecord
|
||||
}.to_json.freeze
|
||||
|
||||
before_validation :ensure_content_type
|
||||
before_save :ensure_processed_message_content
|
||||
|
||||
validates :account_id, presence: true
|
||||
validates :inbox_id, presence: true
|
||||
@@ -70,7 +68,6 @@ class Message < ApplicationRecord
|
||||
|
||||
validates :content_type, presence: true
|
||||
validates :content, length: { maximum: 150_000 }
|
||||
validates :processed_message_content, length: { maximum: 150_000 }
|
||||
|
||||
# when you have a temperory id in your frontend and want it echoed back via action cable
|
||||
attr_accessor :echo_id
|
||||
@@ -217,14 +214,6 @@ class Message < ApplicationRecord
|
||||
|
||||
private
|
||||
|
||||
def ensure_processed_message_content
|
||||
text_content_quoted = content_attributes.dig(:email, :text_content, :quoted)
|
||||
html_content_quoted = content_attributes.dig(:email, :html_content, :quoted)
|
||||
|
||||
message_content = text_content_quoted || html_content_quoted || content
|
||||
self.processed_message_content = message_content&.truncate(150_000)
|
||||
end
|
||||
|
||||
def ensure_content_type
|
||||
self.content_type ||= Message.content_types[:text]
|
||||
end
|
||||
|
||||
@@ -17,13 +17,12 @@
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_reporting_events_on_account_id (account_id)
|
||||
# index_reporting_events_on_conversation_id (conversation_id)
|
||||
# index_reporting_events_on_created_at (created_at)
|
||||
# index_reporting_events_on_inbox_id (inbox_id)
|
||||
# index_reporting_events_on_name (name)
|
||||
# index_reporting_events_on_user_id (user_id)
|
||||
# reporting_events__account_id__name__created_at (account_id,name,created_at)
|
||||
# index_reporting_events_on_account_id (account_id)
|
||||
# index_reporting_events_on_conversation_id (conversation_id)
|
||||
# index_reporting_events_on_created_at (created_at)
|
||||
# index_reporting_events_on_inbox_id (inbox_id)
|
||||
# index_reporting_events_on_name (name)
|
||||
# index_reporting_events_on_user_id (user_id)
|
||||
#
|
||||
|
||||
class ReportingEvent < ApplicationRecord
|
||||
|
||||
@@ -54,5 +54,3 @@ class Team < ApplicationRecord
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
Team.include_mod_with('Audit::Team')
|
||||
|
||||
@@ -11,10 +11,6 @@ class ContactPolicy < ApplicationPolicy
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def export?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def search?
|
||||
true
|
||||
end
|
||||
|
||||
@@ -70,8 +70,6 @@ class MailPresenter < SimpleDelegator
|
||||
}
|
||||
end
|
||||
|
||||
# check content disposition check
|
||||
# if inline, upload to AWS and and take the URL
|
||||
def attachments
|
||||
# ref : https://github.com/gorails-screencasts/action-mailbox-action-text/blob/master/app/mailboxes/posts_mailbox.rb
|
||||
mail.attachments.map do |attachment|
|
||||
|
||||
@@ -27,7 +27,6 @@ class AutomationRules::ConditionsFilterService < FilterService
|
||||
end
|
||||
|
||||
records = base_relation.where(@query_string, @filter_values.with_indifferent_access)
|
||||
|
||||
records = perform_attribute_changed_filter(records) if @attribute_changed_query_filter.any?
|
||||
|
||||
records.any?
|
||||
@@ -94,8 +93,6 @@ class AutomationRules::ConditionsFilterService < FilterService
|
||||
attribute_key = query_hash['attribute_key']
|
||||
query_operator = query_hash['query_operator']
|
||||
|
||||
attribute_key = 'processed_message_content' if attribute_key == 'content'
|
||||
|
||||
filter_operator_value = filter_operation(query_hash, current_index)
|
||||
|
||||
case current_filter['attribute_type']
|
||||
|
||||
@@ -26,54 +26,4 @@ class Whatsapp::Providers::BaseService
|
||||
def validate_provider_config
|
||||
raise 'Overwrite this method in child class'
|
||||
end
|
||||
|
||||
def create_buttons(items)
|
||||
buttons = []
|
||||
items.each do |item|
|
||||
button = { :type => 'reply', 'reply' => { 'id' => item['value'], 'title' => item['title'] } }
|
||||
buttons << button
|
||||
end
|
||||
buttons
|
||||
end
|
||||
|
||||
def create_rows(items)
|
||||
rows = []
|
||||
items.each do |item|
|
||||
row = { 'id' => item['value'], 'title' => item['title'] }
|
||||
rows << row
|
||||
end
|
||||
rows
|
||||
end
|
||||
|
||||
def create_payload(type, message_content, action)
|
||||
{
|
||||
'type': type,
|
||||
'body': {
|
||||
'text': message_content
|
||||
},
|
||||
'action': action
|
||||
}
|
||||
end
|
||||
|
||||
def create_payload_based_on_items(message)
|
||||
if message.content_attributes['items'].length <= 3
|
||||
create_button_payload(message)
|
||||
else
|
||||
create_list_payload(message)
|
||||
end
|
||||
end
|
||||
|
||||
def create_button_payload(message)
|
||||
buttons = create_buttons(message.content_attributes['items'])
|
||||
json_hash = { 'buttons' => buttons }
|
||||
create_payload('button', message.content, JSON.generate(json_hash))
|
||||
end
|
||||
|
||||
def create_list_payload(message)
|
||||
rows = create_rows(message.content_attributes['items'])
|
||||
section1 = { 'rows' => rows }
|
||||
sections = [section1]
|
||||
json_hash = { :button => 'Choose an item', 'sections' => sections }
|
||||
create_payload('list', message.content, JSON.generate(json_hash))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,8 +2,6 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS
|
||||
def send_message(phone_number, message)
|
||||
if message.attachments.present?
|
||||
send_attachment_message(phone_number, message)
|
||||
elsif message.content_type == 'input_select'
|
||||
send_interactive_text_message(phone_number, message)
|
||||
else
|
||||
send_text_message(phone_number, message)
|
||||
end
|
||||
@@ -24,8 +22,6 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS
|
||||
end
|
||||
|
||||
def sync_templates
|
||||
# ensuring that channels with wrong provider config wouldn't keep trying to sync templates
|
||||
whatsapp_channel.mark_message_templates_updated
|
||||
response = HTTParty.get("#{api_base_path}/configs/templates", headers: api_headers)
|
||||
whatsapp_channel.update(message_templates: response['waba_templates'], message_templates_last_updated: Time.now.utc) if response.success?
|
||||
end
|
||||
@@ -114,20 +110,4 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS
|
||||
}]
|
||||
}
|
||||
end
|
||||
|
||||
def send_interactive_text_message(phone_number, message)
|
||||
payload = create_payload_based_on_items(message)
|
||||
|
||||
response = HTTParty.post(
|
||||
"#{api_base_path}/messages",
|
||||
headers: api_headers,
|
||||
body: {
|
||||
to: phone_number,
|
||||
interactive: payload,
|
||||
type: 'interactive'
|
||||
}.to_json
|
||||
)
|
||||
|
||||
process_response(response)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,8 +2,6 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
||||
def send_message(phone_number, message)
|
||||
if message.attachments.present?
|
||||
send_attachment_message(phone_number, message)
|
||||
elsif message.content_type == 'input_select'
|
||||
send_interactive_text_message(phone_number, message)
|
||||
else
|
||||
send_text_message(phone_number, message)
|
||||
end
|
||||
@@ -25,9 +23,8 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
||||
end
|
||||
|
||||
def sync_templates
|
||||
# ensuring that channels with wrong provider config wouldn't keep trying to sync templates
|
||||
whatsapp_channel.mark_message_templates_updated
|
||||
templates = fetch_whatsapp_templates("#{business_account_path}/message_templates?access_token=#{whatsapp_channel.provider_config['api_key']}")
|
||||
|
||||
whatsapp_channel.update(message_templates: templates, message_templates_last_updated: Time.now.utc) if templates.present?
|
||||
end
|
||||
|
||||
@@ -131,21 +128,4 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
|
||||
}]
|
||||
}
|
||||
end
|
||||
|
||||
def send_interactive_text_message(phone_number, message)
|
||||
payload = create_payload_based_on_items(message)
|
||||
|
||||
response = HTTParty.post(
|
||||
"#{phone_id_path}/messages",
|
||||
headers: api_headers,
|
||||
body: {
|
||||
messaging_product: 'whatsapp',
|
||||
to: phone_number,
|
||||
interactive: payload,
|
||||
type: 'interactive'
|
||||
}.to_json
|
||||
)
|
||||
|
||||
process_response(response)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1 @@
|
||||
json.payload @attachments do |attachment|
|
||||
json.message_id attachment.push_event_data[:message_id]
|
||||
json.thumb_url attachment.push_event_data[:thumb_url]
|
||||
json.data_url attachment.push_event_data[:data_url]
|
||||
json.file_size attachment.push_event_data[:file_size]
|
||||
json.file_type attachment.push_event_data[:file_type]
|
||||
json.extension attachment.push_event_data[:extension]
|
||||
json.created_at attachment.message.created_at.to_i
|
||||
json.sender attachment.message.sender.push_event_data if attachment.message.sender
|
||||
end
|
||||
json.payload @attachments.map(&:push_event_data)
|
||||
|
||||
@@ -6,7 +6,6 @@ json.data do
|
||||
json.display_name resource.display_name
|
||||
json.email resource.email
|
||||
json.account_id @account.id
|
||||
json.created_at resource.created_at
|
||||
json.pubsub_token resource.pubsub_token
|
||||
json.role resource.active_account_user&.role
|
||||
json.inviter_id resource.active_account_user&.inviter_id
|
||||
|
||||
@@ -26,7 +26,6 @@ else
|
||||
end
|
||||
|
||||
json.account_id conversation.account_id
|
||||
json.uuid conversation.uuid
|
||||
json.additional_attributes conversation.additional_attributes
|
||||
json.agent_last_seen_at conversation.agent_last_seen_at.to_i
|
||||
json.assignee_last_seen_at conversation.assignee_last_seen_at.to_i
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user