diff --git a/Gemfile.lock b/Gemfile.lock
index 5e2abbac6..2222403f1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -9,63 +9,63 @@ GIT
GEM
remote: https://rubygems.org/
specs:
- actioncable (6.1.4.6)
- actionpack (= 6.1.4.6)
- activesupport (= 6.1.4.6)
+ actioncable (6.1.4.7)
+ actionpack (= 6.1.4.7)
+ activesupport (= 6.1.4.7)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (6.1.4.6)
- actionpack (= 6.1.4.6)
- activejob (= 6.1.4.6)
- activerecord (= 6.1.4.6)
- activestorage (= 6.1.4.6)
- activesupport (= 6.1.4.6)
+ actionmailbox (6.1.4.7)
+ actionpack (= 6.1.4.7)
+ activejob (= 6.1.4.7)
+ activerecord (= 6.1.4.7)
+ activestorage (= 6.1.4.7)
+ activesupport (= 6.1.4.7)
mail (>= 2.7.1)
- actionmailer (6.1.4.6)
- actionpack (= 6.1.4.6)
- actionview (= 6.1.4.6)
- activejob (= 6.1.4.6)
- activesupport (= 6.1.4.6)
+ actionmailer (6.1.4.7)
+ actionpack (= 6.1.4.7)
+ actionview (= 6.1.4.7)
+ activejob (= 6.1.4.7)
+ activesupport (= 6.1.4.7)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (6.1.4.6)
- actionview (= 6.1.4.6)
- activesupport (= 6.1.4.6)
+ actionpack (6.1.4.7)
+ actionview (= 6.1.4.7)
+ activesupport (= 6.1.4.7)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
- actiontext (6.1.4.6)
- actionpack (= 6.1.4.6)
- activerecord (= 6.1.4.6)
- activestorage (= 6.1.4.6)
- activesupport (= 6.1.4.6)
+ actiontext (6.1.4.7)
+ actionpack (= 6.1.4.7)
+ activerecord (= 6.1.4.7)
+ activestorage (= 6.1.4.7)
+ activesupport (= 6.1.4.7)
nokogiri (>= 1.8.5)
- actionview (6.1.4.6)
- activesupport (= 6.1.4.6)
+ actionview (6.1.4.7)
+ activesupport (= 6.1.4.7)
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 (6.1.4.6)
- activesupport (= 6.1.4.6)
+ activejob (6.1.4.7)
+ activesupport (= 6.1.4.7)
globalid (>= 0.3.6)
- activemodel (6.1.4.6)
- activesupport (= 6.1.4.6)
- activerecord (6.1.4.6)
- activemodel (= 6.1.4.6)
- activesupport (= 6.1.4.6)
+ activemodel (6.1.4.7)
+ activesupport (= 6.1.4.7)
+ activerecord (6.1.4.7)
+ activemodel (= 6.1.4.7)
+ activesupport (= 6.1.4.7)
activerecord-import (1.3.0)
activerecord (>= 4.2)
- activestorage (6.1.4.6)
- actionpack (= 6.1.4.6)
- activejob (= 6.1.4.6)
- activerecord (= 6.1.4.6)
- activesupport (= 6.1.4.6)
+ activestorage (6.1.4.7)
+ actionpack (= 6.1.4.7)
+ activejob (= 6.1.4.7)
+ activerecord (= 6.1.4.7)
+ activesupport (= 6.1.4.7)
marcel (~> 1.0.0)
mini_mime (>= 1.1.0)
- activesupport (6.1.4.6)
+ activesupport (6.1.4.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -135,7 +135,7 @@ GEM
byebug (11.1.3)
climate_control (1.0.1)
coderay (1.1.3)
- commonmarker (0.23.2)
+ commonmarker (0.23.4)
concurrent-ruby (1.1.9)
connection_pool (2.2.5)
crack (0.4.5)
@@ -303,7 +303,7 @@ GEM
httpclient (2.8.3)
i18n (1.10.0)
concurrent-ruby (~> 1.0)
- image_processing (1.12.1)
+ image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
jbuilder (2.11.5)
@@ -419,29 +419,29 @@ GEM
rack-test (1.1.0)
rack (>= 1.0, < 3)
rack-timeout (0.6.0)
- rails (6.1.4.6)
- actioncable (= 6.1.4.6)
- actionmailbox (= 6.1.4.6)
- actionmailer (= 6.1.4.6)
- actionpack (= 6.1.4.6)
- actiontext (= 6.1.4.6)
- actionview (= 6.1.4.6)
- activejob (= 6.1.4.6)
- activemodel (= 6.1.4.6)
- activerecord (= 6.1.4.6)
- activestorage (= 6.1.4.6)
- activesupport (= 6.1.4.6)
+ rails (6.1.4.7)
+ actioncable (= 6.1.4.7)
+ actionmailbox (= 6.1.4.7)
+ actionmailer (= 6.1.4.7)
+ actionpack (= 6.1.4.7)
+ actiontext (= 6.1.4.7)
+ actionview (= 6.1.4.7)
+ activejob (= 6.1.4.7)
+ activemodel (= 6.1.4.7)
+ activerecord (= 6.1.4.7)
+ activestorage (= 6.1.4.7)
+ activesupport (= 6.1.4.7)
bundler (>= 1.15.0)
- railties (= 6.1.4.6)
+ railties (= 6.1.4.7)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.4.2)
loofah (~> 2.3)
- railties (6.1.4.6)
- actionpack (= 6.1.4.6)
- activesupport (= 6.1.4.6)
+ railties (6.1.4.7)
+ actionpack (= 6.1.4.7)
+ activesupport (= 6.1.4.7)
method_source
rake (>= 0.13)
thor (~> 1.0)
@@ -574,7 +574,7 @@ GEM
spring-watcher-listen (2.0.1)
listen (>= 2.7, < 4.0)
spring (>= 1.2, < 3.0)
- sprockets (4.0.2)
+ sprockets (4.0.3)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.4.2)
@@ -751,4 +751,4 @@ RUBY VERSION
ruby 3.0.2p107
BUNDLED WITH
- 2.2.25
+ 2.3.8
diff --git a/app/actions/contact_identify_action.rb b/app/actions/contact_identify_action.rb
index 655c6bc1c..746d1bfec 100644
--- a/app/actions/contact_identify_action.rb
+++ b/app/actions/contact_identify_action.rb
@@ -52,12 +52,11 @@ class ContactIdentifyAction
end
def update_contact
- custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes
# blank identifier or email will throw unique index error
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
@contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v|
v.blank?
- end.merge({ custom_attributes: custom_attributes }))
+ end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes }))
ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end
@@ -68,4 +67,16 @@ class ContactIdentifyAction
mergee_contact: merge_contact
).perform
end
+
+ def custom_attributes
+ params[:custom_attributes] ? @contact.custom_attributes.deep_merge(params[:custom_attributes].stringify_keys) : @contact.custom_attributes
+ end
+
+ def additional_attributes
+ if params[:additional_attributes]
+ @contact.additional_attributes.deep_merge(params[:additional_attributes].stringify_keys)
+ else
+ @contact.additional_attributes
+ end
+ end
end
diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb
index 08aa58be0..2e58825f4 100644
--- a/app/builders/messages/messenger/message_builder.rb
+++ b/app/builders/messages/messenger/message_builder.rb
@@ -1,10 +1,14 @@
class Messages::Messenger::MessageBuilder
+ include ::FileTypeHelper
+
def process_attachment(attachment)
return if attachment['type'].to_sym == :template
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
attachment_obj.save!
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
+ fetch_story_link(attachment_obj) if attachment_obj.file_type == 'story_mention'
+ update_attachment_file_type(attachment_obj)
end
def attach_file(attachment, file_url)
@@ -22,7 +26,7 @@ class Messages::Messenger::MessageBuilder
file_type = attachment['type'].to_sym
params = { file_type: file_type, account_id: @message.account_id }
- if [:image, :file, :audio, :video].include? file_type
+ if [:image, :file, :audio, :video, :share, :story_mention].include? file_type
params.merge!(file_type_params(attachment))
elsif file_type == :location
params.merge!(location_params(attachment))
@@ -39,4 +43,31 @@ class Messages::Messenger::MessageBuilder
remote_file_url: attachment['payload']['url']
}
end
+
+ def update_attachment_file_type(attachment)
+ return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
+
+ attachment.file_type = file_type(attachment.file&.content_type)
+ attachment.save!
+ end
+
+ def fetch_story_link(attachment)
+ message = attachment.message
+ begin
+ k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
+ result = k.get_object(message.source_id, fields: %w[story from]) || {}
+ rescue Koala::Facebook::AuthenticationError
+ @inbox.channel.authorization_error!
+ raise
+ rescue StandardError => e
+ result = {}
+ Sentry.capture_exception(e)
+ end
+ story_id = result['story']['mention']['id']
+ story_sender = result['from']['username']
+ message.content_attributes[:story_sender] = story_sender
+ message.content_attributes[:story_id] = story_id
+ message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
+ message.save!
+ end
end
diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb
index 7f0dd470c..1f17dbb08 100644
--- a/app/builders/v2/report_builder.rb
+++ b/app/builders/v2/report_builder.rb
@@ -95,15 +95,15 @@ class V2::ReportBuilder
end
def avg_first_response_time
- (get_grouped_values scope.events.where(name: 'first_response')).average(:value)
+ (get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value)
end
def avg_resolution_time
- (get_grouped_values scope.events.where(name: 'conversation_resolved')).average(:value)
+ (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value)
end
def avg_resolution_time_summary
- avg_rt = scope.events
+ avg_rt = scope.reporting_events
.where(name: 'conversation_resolved', created_at: range)
.average(:value)
@@ -113,7 +113,7 @@ class V2::ReportBuilder
end
def avg_first_response_time_summary
- avg_frt = scope.events
+ avg_frt = scope.reporting_events
.where(name: 'first_response', created_at: range)
.average(:value)
diff --git a/app/channels/room_channel.rb b/app/channels/room_channel.rb
index b3c597770..05bff5d86 100644
--- a/app/channels/room_channel.rb
+++ b/app/channels/room_channel.rb
@@ -1,5 +1,8 @@
class RoomChannel < ApplicationCable::Channel
def subscribed
+ # TODO: should we only do ensure stream if current account is present?
+ # for now going ahead with guard clauses in update_subscription and broadcast_presence
+
ensure_stream
current_user
current_account
@@ -15,6 +18,8 @@ class RoomChannel < ApplicationCable::Channel
private
def broadcast_presence
+ return if @current_account.blank?
+
data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) }
data[:contacts] = ::OnlineStatusTracker.get_available_contacts(@current_account.id) if @current_user.is_a? User
ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data })
@@ -26,6 +31,8 @@ class RoomChannel < ApplicationCable::Channel
end
def update_subscription
+ return if @current_account.blank?
+
::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id)
end
diff --git a/app/controllers/api/v1/accounts/automation_rules_controller.rb b/app/controllers/api/v1/accounts/automation_rules_controller.rb
index 12b7b5957..2dc71bcec 100644
--- a/app/controllers/api/v1/accounts/automation_rules_controller.rb
+++ b/app/controllers/api/v1/accounts/automation_rules_controller.rb
@@ -3,7 +3,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
before_action :fetch_automation_rule, only: [:show, :update, :destroy, :clone]
def index
- @automation_rules = Current.account.automation_rules.active
+ @automation_rules = Current.account.automation_rules
end
def create
@@ -32,7 +32,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
def automation_rules_permit
params.permit(
- :name, :description, :event_name, :account_id,
+ :name, :description, :event_name, :account_id, :active,
conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }],
actions: [:action_name, { action_params: [] }]
)
diff --git a/app/controllers/api/v1/accounts/conversations/messages_controller.rb b/app/controllers/api/v1/accounts/conversations/messages_controller.rb
index ffd00461b..77a3a7081 100644
--- a/app/controllers/api/v1/accounts/conversations/messages_controller.rb
+++ b/app/controllers/api/v1/accounts/conversations/messages_controller.rb
@@ -13,7 +13,7 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
def destroy
ActiveRecord::Base.transaction do
- message.update!(content: I18n.t('conversations.messages.deleted'), deleted: true)
+ message.update!(content: I18n.t('conversations.messages.deleted'), content_attributes: { deleted: true })
message.attachments.destroy_all
end
end
diff --git a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb
index 26de6d406..347f028f7 100644
--- a/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb
+++ b/app/controllers/api/v1/accounts/csat_survey_responses_controller.rb
@@ -30,8 +30,7 @@ class Api::V1::Accounts::CsatSurveyResponsesController < Api::V1::Accounts::Base
def set_csat_survey_responses
@csat_survey_responses = filtrate(
Current.account.csat_survey_responses.includes([:conversation, :assigned_agent, :contact])
- )
- @csat_survey_responses = @csat_survey_responses.where(created_at: range) if range.present?
+ ).filter_by_created_at(range).filter_by_assigned_agent_id(params[:user_ids])
end
def set_current_page_surveys
diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb
index 38c880526..0a50835bc 100644
--- a/app/controllers/api/v1/widget/base_controller.rb
+++ b/app/controllers/api/v1/widget/base_controller.rb
@@ -34,6 +34,8 @@ class Api::V1::Widget::BaseController < ApplicationController
)
@contact = @contact_inbox&.contact
raise ActiveRecord::RecordNotFound unless @contact
+
+ Current.contact = @contact
end
def create_conversation
diff --git a/app/controllers/api/v1/widget/contacts_controller.rb b/app/controllers/api/v1/widget/contacts_controller.rb
index d745c4153..fbc303a4f 100644
--- a/app/controllers/api/v1/widget/contacts_controller.rb
+++ b/app/controllers/api/v1/widget/contacts_controller.rb
@@ -46,6 +46,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
end
def permitted_params
- params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {})
+ params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {},
+ additional_attributes: {})
end
end
diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb
index 8d28345d4..e2ee61424 100644
--- a/app/controllers/api/v1/widget/conversations_controller.rb
+++ b/app/controllers/api/v1/widget/conversations_controller.rb
@@ -44,6 +44,15 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
head :ok
end
+ def toggle_status
+ head :not_found && return if conversation.nil?
+ unless conversation.resolved?
+ conversation.status = 'resolved'
+ conversation.save
+ end
+ head :ok
+ end
+
private
def trigger_typing_event(event)
diff --git a/app/controllers/api/v2/accounts/reports_controller.rb b/app/controllers/api/v2/accounts/reports_controller.rb
index a83611676..efa09a43c 100644
--- a/app/controllers/api/v2/accounts/reports_controller.rb
+++ b/app/controllers/api/v2/accounts/reports_controller.rb
@@ -41,12 +41,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
end
- def summary_params
+ def current_summary_params
{
type: params[:type].to_sym,
- since: params[:since],
- until: params[:until],
id: params[:id],
+ since: range[:current][:since],
+ until: range[:current][:until],
+ group_by: params[:group_by]
+ }
+ end
+
+ def previous_summary_params
+ {
+ type: params[:type].to_sym,
+ id: params[:id],
+ since: range[:previous][:since],
+ until: range[:previous][:until],
group_by: params[:group_by]
}
end
@@ -63,8 +73,22 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
}
end
+ def range
+ {
+ current: {
+ since: params[:since],
+ until: params[:until]
+ },
+ previous: {
+ since: (params[:since].to_i - (params[:until].to_i - params[:since].to_i)).to_s,
+ until: params[:since]
+ }
+ }
+ end
+
def summary_metrics
- builder = V2::ReportBuilder.new(Current.account, summary_params)
- builder.summary
+ summary = V2::ReportBuilder.new(Current.account, current_summary_params).summary
+ summary[:previous] = V2::ReportBuilder.new(Current.account, previous_summary_params).summary
+ summary
end
end
diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb
index 3f95405f9..f2f238cd4 100644
--- a/app/dispatchers/async_dispatcher.rb
+++ b/app/dispatchers/async_dispatcher.rb
@@ -12,10 +12,10 @@ class AsyncDispatcher < BaseDispatcher
[
CampaignListener.instance,
CsatSurveyListener.instance,
- EventListener.instance,
HookListener.instance,
InstallationWebhookListener.instance,
NotificationListener.instance,
+ ReportingEventListener.instance,
WebhookListener.instance,
AutomationRuleListener.instance
]
diff --git a/app/helpers/api/v1/reports_helper.rb b/app/helpers/api/v1/reports_helper.rb
deleted file mode 100644
index 308bab3e6..000000000
--- a/app/helpers/api/v1/reports_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module Api::V1::ReportsHelper
-end
diff --git a/app/helpers/file_type_helper.rb b/app/helpers/file_type_helper.rb
index 503c7f25b..db3f6249d 100644
--- a/app/helpers/file_type_helper.rb
+++ b/app/helpers/file_type_helper.rb
@@ -3,7 +3,7 @@ module FileTypeHelper
def file_type(content_type)
return :image if image_file?(content_type)
return :video if video_file?(content_type)
- return :audio if content_type.include?('audio/')
+ return :audio if content_type&.include?('audio/')
:file
end
@@ -14,7 +14,8 @@ module FileTypeHelper
'image/png',
'image/gif',
'image/bmp',
- 'image/webp'
+ 'image/webp',
+ 'image'
].include?(content_type)
end
@@ -23,7 +24,8 @@ module FileTypeHelper
'video/ogg',
'video/mp4',
'video/webm',
- 'video/quicktime'
+ 'video/quicktime',
+ 'video'
].include?(content_type)
end
end
diff --git a/app/javascript/dashboard/App.vue b/app/javascript/dashboard/App.vue
index 502d8bda1..2c91de16f 100644
--- a/app/javascript/dashboard/App.vue
+++ b/app/javascript/dashboard/App.vue
@@ -1,5 +1,6 @@
+
@@ -13,24 +14,27 @@
diff --git a/app/javascript/dashboard/components/index.js b/app/javascript/dashboard/components/index.js
index f737bd313..d2614421c 100644
--- a/app/javascript/dashboard/components/index.js
+++ b/app/javascript/dashboard/components/index.js
@@ -21,6 +21,7 @@ import SubmitButton from './buttons/FormSubmitButton';
import Tabs from './ui/Tabs/Tabs';
import TabsItem from './ui/Tabs/TabsItem';
import Thumbnail from './widgets/Thumbnail.vue';
+import ConfirmModal from './widgets/modal/ConfirmationModal.vue';
const WootUIKit = {
AvatarUploader,
@@ -45,6 +46,7 @@ const WootUIKit = {
Tabs,
TabsItem,
Thumbnail,
+ ConfirmModal,
install(Vue) {
const keys = Object.keys(this);
keys.pop(); // remove 'install' from keys
diff --git a/app/javascript/dashboard/components/layout/Sidebar.vue b/app/javascript/dashboard/components/layout/Sidebar.vue
index ee64f4949..826d766df 100644
--- a/app/javascript/dashboard/components/layout/Sidebar.vue
+++ b/app/javascript/dashboard/components/layout/Sidebar.vue
@@ -19,23 +19,6 @@
:current-role="currentRole"
@add-label="showAddLabelPopup"
/>
-
-
-
-
-
-
@@ -46,12 +29,8 @@ import adminMixin from '../../mixins/isAdmin';
import { getSidebarItems } from './config/default-sidebar';
import alertMixin from 'shared/mixins/alertMixin';
-import AccountSelector from './sidebarComponents/AccountSelector.vue';
-import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
-import AddLabelModal from '../../routes/dashboard/settings/labels/AddLabel';
import PrimarySidebar from './sidebarComponents/Primary';
import SecondarySidebar from './sidebarComponents/Secondary';
-import WootKeyShortcutModal from 'components/widgets/modal/WootKeyShortcutModal';
import {
hasPressedAltAndCKey,
hasPressedAltAndRKey,
@@ -65,21 +44,13 @@ import router from '../../routes';
export default {
components: {
- AccountSelector,
- AddAccountModal,
- AddLabelModal,
PrimarySidebar,
SecondarySidebar,
- WootKeyShortcutModal,
},
mixins: [adminMixin, alertMixin, eventListenerMixins],
data() {
return {
showOptionsMenu: false,
- showAccountModal: false,
- showCreateAccountModal: false,
- showAddLabelModal: false,
- showShortcutModal: false,
};
},
@@ -162,10 +133,10 @@ export default {
}
},
toggleKeyShortcutModal() {
- this.showShortcutModal = true;
+ this.$emit('open-key-shortcut-modal');
},
closeKeyShortcutModal() {
- this.showShortcutModal = false;
+ this.$emit('close-key-shortcut-modal');
},
handleKeyEvents(e) {
if (hasPressedCommandAndForwardSlash(e)) {
@@ -200,20 +171,10 @@ export default {
window.$chatwoot.toggle();
},
toggleAccountModal() {
- this.showAccountModal = !this.showAccountModal;
- },
- openCreateAccountModal() {
- this.showAccountModal = false;
- this.showCreateAccountModal = true;
- },
- closeCreateAccountModal() {
- this.showCreateAccountModal = false;
+ this.$emit('toggle-account-modal');
},
showAddLabelPopup() {
- this.showAddLabelModal = true;
- },
- hideAddLabelPopup() {
- this.showAddLabelModal = false;
+ this.$emit('show-add-label-popup');
},
},
};
@@ -223,6 +184,8 @@ export default {
.woot-sidebar {
background: var(--white);
display: flex;
+ min-height: 0;
+ height: 100%;
}
diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/Primary.vue b/app/javascript/dashboard/components/layout/sidebarComponents/Primary.vue
index 0f568ab75..002f371bf 100644
--- a/app/javascript/dashboard/components/layout/sidebarComponents/Primary.vue
+++ b/app/javascript/dashboard/components/layout/sidebarComponents/Primary.vue
@@ -93,7 +93,7 @@ export default {
width: var(--space-jumbo);
border-right: 1px solid var(--s-50);
box-sizing: content-box;
- height: 100vh;
+ height: 100%;
flex-shrink: 0;
}
diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue
index 6222490e4..1f98305a1 100644
--- a/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue
+++ b/app/javascript/dashboard/components/layout/sidebarComponents/Secondary.vue
@@ -225,7 +225,7 @@ export default {
.secondary-menu {
background: var(--white);
border-right: 1px solid var(--s-50);
- height: 100vh;
+ height: 100%;
width: 19rem;
flex-shrink: 0;
overflow: hidden;
diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue
index 3a344d985..ffd8018d5 100644
--- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue
+++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue
@@ -20,7 +20,8 @@
data-view-component="true"
label="Beta"
class="beta"
- >Beta
+ >
+ {{ $t('SIDEBAR.BETA') }}
@@ -233,7 +234,7 @@ export default {
padding-left: var(--space-smaller) !important;
margin-left: var(--space-half) !important;
display: inline-block;
- font-size: var(--font-size-mini);
+ font-size: var(--font-size-micro);
font-weight: var(--font-weight-medium);
line-height: 18px;
border: 1px solid transparent;
diff --git a/app/javascript/dashboard/components/ui/Banner.vue b/app/javascript/dashboard/components/ui/Banner.vue
index 52d7c5740..c9d71bfaf 100644
--- a/app/javascript/dashboard/components/ui/Banner.vue
+++ b/app/javascript/dashboard/components/ui/Banner.vue
@@ -1,6 +1,6 @@
onRemoveAttachment(index)"
@@ -58,6 +59,10 @@ export default {
const type = file.content_type || file.type;
return type.includes('image');
},
+ isTypeAudio(file) {
+ const type = file.content_type || file.type;
+ return type.includes('audio');
+ },
fileName(file) {
return file.filename || file.name;
},
diff --git a/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js b/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js
index 9e9e3a844..07792c5d5 100644
--- a/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js
+++ b/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.js
@@ -69,7 +69,22 @@ export const OPERATOR_TYPES_4 = [
label: 'Is greater than',
},
{
- value: 'is_lesser_than',
- label: 'Is lesser than',
+ value: 'is_less_than',
+ label: 'Is less than',
+ },
+];
+
+export const OPERATOR_TYPES_5 = [
+ {
+ value: 'is_greater_than',
+ label: 'Is greater than',
+ },
+ {
+ value: 'is_less_than',
+ label: 'Is less than',
+ },
+ {
+ value: 'days_before',
+ label: 'Is x days before',
},
];
diff --git a/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.spec.js b/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.spec.js
new file mode 100644
index 000000000..4e8e57b16
--- /dev/null
+++ b/app/javascript/dashboard/components/widgets/FilterInput/FilterOperatorTypes.spec.js
@@ -0,0 +1,15 @@
+import {
+ OPERATOR_TYPES_1,
+ OPERATOR_TYPES_2,
+ OPERATOR_TYPES_3,
+ OPERATOR_TYPES_4,
+} from './FilterOperatorTypes';
+
+describe('#filterOperators', () => {
+ it('Matches the correct Operators', () => {
+ expect(OPERATOR_TYPES_1).toMatchObject(OPERATOR_TYPES_1);
+ expect(OPERATOR_TYPES_2).toMatchObject(OPERATOR_TYPES_2);
+ expect(OPERATOR_TYPES_3).toMatchObject(OPERATOR_TYPES_3);
+ expect(OPERATOR_TYPES_4).toMatchObject(OPERATOR_TYPES_4);
+ });
+});
diff --git a/app/javascript/dashboard/components/widgets/FilterInput/Index.vue b/app/javascript/dashboard/components/widgets/FilterInput/Index.vue
index d26467ab6..25203642a 100644
--- a/app/javascript/dashboard/components/widgets/FilterInput/Index.vue
+++ b/app/javascript/dashboard/components/widgets/FilterInput/Index.vue
@@ -141,6 +141,10 @@ export default {
type: String,
default: 'plain_text',
},
+ dataType: {
+ type: String,
+ default: 'plain_text',
+ },
operators: {
type: Array,
default: () => [],
diff --git a/app/javascript/dashboard/components/widgets/ReportStatsCard.vue b/app/javascript/dashboard/components/widgets/ReportStatsCard.vue
index 309627220..9bc46d021 100644
--- a/app/javascript/dashboard/components/widgets/ReportStatsCard.vue
+++ b/app/javascript/dashboard/components/widgets/ReportStatsCard.vue
@@ -7,9 +7,12 @@
{{ heading }}
-
- {{ point }}
-
+
+
+ {{ point }}
+
+ {{ trendValue }}
+
{{ desc }}
@@ -20,10 +23,27 @@ export default {
props: {
heading: { type: String, default: '' },
point: { type: [Number, String], default: '' },
+ trend: { type: Number, default: null },
index: { type: Number, default: null },
desc: { type: String, default: '' },
selected: Boolean,
onClick: { type: Function, default: () => {} },
},
+ computed: {
+ trendClass() {
+ if (this.trend > 0) {
+ return 'metric-trend metric-up';
+ }
+
+ return 'metric-trend metric-down';
+ },
+ trendValue() {
+ if (this.trend > 0) {
+ return `+${this.trend}%`;
+ }
+
+ return `${this.trend}%`;
+ },
+ },
};
diff --git a/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue b/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue
new file mode 100644
index 000000000..4d6df7793
--- /dev/null
+++ b/app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue
index 6df1b2858..6dcb47532 100644
--- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue
+++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue
@@ -11,7 +11,6 @@
size="small"
@click="toggleEmojiPicker"
/>
-
+
+
+ {{ recordingAudioDurationText }}
+
({}),
@@ -134,6 +158,10 @@ export default {
type: Boolean,
default: false,
},
+ showAudioRecorder: {
+ type: Boolean,
+ default: false,
+ },
onFileUpload: {
type: Function,
default: () => {},
@@ -146,6 +174,22 @@ export default {
type: Function,
default: () => {},
},
+ toggleAudioRecorder: {
+ type: Function,
+ default: () => {},
+ },
+ toggleAudioRecorderPlayPause: {
+ type: Function,
+ default: () => {},
+ },
+ isRecordingAudio: {
+ type: Boolean,
+ default: false,
+ },
+ recordingAudioState: {
+ type: String,
+ default: '',
+ },
isSendDisabled: {
type: Boolean,
default: false,
@@ -192,9 +236,28 @@ export default {
showAttachButton() {
return this.showFileUpload || this.isNote;
},
+ showAudioRecorderButton() {
+ return this.showAudioRecorder;
+ },
+ showAudioPlayStopButton() {
+ return this.showAudioRecorder && this.isRecordingAudio;
+ },
allowedFileTypes() {
return ALLOWED_FILE_TYPES;
},
+ audioRecorderPlayStopIcon() {
+ switch (this.recordingAudioState) {
+ // playing paused recording stopped inactive destroyed
+ case 'playing':
+ return 'microphone-pause';
+ case 'paused':
+ return 'microphone-play';
+ case 'stopped':
+ return 'microphone-play';
+ default:
+ return 'microphone-stop';
+ }
+ },
showMessageSignatureButton() {
return !this.isPrivate && this.isAnEmailChannel;
},
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationAdvancedFilter.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationAdvancedFilter.vue
index de65bb042..34a349fff 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ConversationAdvancedFilter.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ConversationAdvancedFilter.vue
@@ -10,7 +10,12 @@
:key="i"
v-model="appliedFilters[i]"
:filter-groups="filterGroups"
- :input-type="getInputType(appliedFilters[i].attribute_key)"
+ :input-type="
+ getInputType(
+ appliedFilters[i].attribute_key,
+ appliedFilters[i].filter_operator
+ )
+ "
:operators="getOperators(appliedFilters[i].attribute_key)"
:dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)"
:show-query-operator="i !== appliedFilters.length - 1"
@@ -56,6 +61,7 @@ import { mapGetters } from 'vuex';
import { filterAttributeGroups } from './advancedFilterItems';
import filterMixin from 'shared/mixins/filterMixin';
import * as OPERATORS from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
+
export default {
components: {
FilterInputBox,
@@ -76,6 +82,12 @@ export default {
required,
$each: {
values: {
+ ensureBetween0to999(value, prop) {
+ if (prop.filter_operator === 'days_before') {
+ return parseInt(value, 10) > 0 && parseInt(value, 10) < 999;
+ }
+ return true;
+ },
required: requiredIf(prop => {
return !(
prop.filter_operator === 'is_present' ||
@@ -141,6 +153,12 @@ export default {
switch (key) {
case 'date':
return 'date';
+ case 'text':
+ return 'plain_text';
+ case 'list':
+ return 'search_select';
+ case 'checkbox':
+ return 'search_select';
default:
return 'plain_text';
}
@@ -149,7 +167,9 @@ export default {
const type = this.filterTypes.find(filter => filter.attributeKey === key);
return type.attributeModel;
},
- getInputType(key) {
+ getInputType(key, operator) {
+ 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;
},
@@ -159,6 +179,47 @@ export default {
},
getDropdownValues(type) {
const statusFilters = this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS');
+ const allCustomAttributes = this.$store.getters[
+ 'attributes/getAttributesByModel'
+ ](this.attributeModel);
+
+ const isCustomAttributeCheckbox = allCustomAttributes.find(attr => {
+ return (
+ attr.attribute_key === type &&
+ attr.attribute_display_type === 'checkbox'
+ );
+ });
+
+ if (isCustomAttributeCheckbox) {
+ return [
+ {
+ id: true,
+ name: this.$t('FILTER.ATTRIBUTE_LABELS.TRUE'),
+ },
+ {
+ id: false,
+ name: this.$t('FILTER.ATTRIBUTE_LABELS.FALSE'),
+ },
+ ];
+ }
+
+ const isCustomAttributeList = allCustomAttributes.find(attr => {
+ return (
+ attr.attribute_key === type && attr.attribute_display_type === 'list'
+ );
+ });
+
+ if (isCustomAttributeList) {
+ return allCustomAttributes
+ .find(attr => attr.attribute_key === type)
+ .attribute_values.map(item => {
+ return {
+ id: item,
+ name: item,
+ };
+ });
+ }
+
switch (type) {
case 'status':
return [
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue
index ea4055d08..4849c2e97 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue
@@ -100,7 +100,8 @@ export default {
display: flex;
background: var(--color-background-light);
margin: 0;
- height: calc(100vh - var(--space-jumbo));
+ height: 100%;
+ min-height: 0;
}
.conversation-sidebar-wrap {
diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue
index 145660c07..51d82ff8c 100644
--- a/app/javascript/dashboard/components/widgets/conversation/Message.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue
@@ -50,7 +50,10 @@
@@ -64,6 +65,7 @@
class="message--unread"
:data="message"
:is-a-tweet="isATweet"
+ :has-instagram-story="hasInstagramStory"
/>
+
this.$refs.messageInput.focus());
@@ -530,10 +568,26 @@ export default {
this.attachedFiles = [];
this.ccEmails = '';
this.bccEmails = '';
+ this.isRecordingAudio = false;
},
toggleEmojiPicker() {
this.showEmojiPicker = !this.showEmojiPicker;
},
+ toggleAudioRecorder() {
+ this.isRecordingAudio = !this.isRecordingAudio;
+ this.isRecorderAudioStopped = !this.isRecordingAudio;
+ if (!this.isRecordingAudio) {
+ this.clearMessage();
+ }
+ },
+ toggleAudioRecorderPlayPause() {
+ if (this.isRecordingAudio && !this.isRecorderAudioStopped) {
+ this.isRecorderAudioStopped = true;
+ this.$refs.audioRecorderInput.stopAudioRecording();
+ } else if (this.isRecordingAudio && this.isRecorderAudioStopped) {
+ this.$refs.audioRecorderInput.playPause();
+ }
+ },
hideEmojiPicker() {
if (this.showEmojiPicker) {
this.toggleEmojiPicker();
@@ -554,6 +608,20 @@ export default {
onFocus() {
this.isFocused = true;
},
+ onStateRecorderTimerChanged(time) {
+ this.recordingAudioDuration = time;
+ },
+ onStateRecorderChanged(state) {
+ this.recordingAudioState = state;
+ if (state.includes('notallowederror')) {
+ this.toggleAudioRecorder();
+ }
+ },
+ onRecorderBlob(file) {
+ if (file) {
+ this.onFileUpload(file);
+ }
+ },
toggleTyping(status) {
const conversationId = this.currentChat.id;
const isPrivate = this.isPrivate;
@@ -650,11 +718,11 @@ export default {
});
}
- if (this.ccEmails) {
+ if (this.ccEmails && !this.isOnPrivateNote) {
messagePayload.ccEmails = this.ccEmails;
}
- if (this.bccEmails) {
+ if (this.bccEmails && !this.isOnPrivateNote) {
messagePayload.bccEmails = this.bccEmails;
}
@@ -667,6 +735,17 @@ export default {
this.bccEmails = value.bccEmails;
this.ccEmails = value.ccEmails;
},
+ setCCEmailFromLastChat() {
+ if (this.lastEmail) {
+ const {
+ content_attributes: { email: emailAttributes = {} },
+ } = this.lastEmail;
+ const cc = emailAttributes.cc || [];
+ const bcc = emailAttributes.bcc || [];
+ this.ccEmails = cc.join(', ');
+ this.bccEmails = bcc.join(', ');
+ }
+ },
},
};
@@ -684,6 +763,8 @@ export default {
justify-content: space-between;
border: 1px dashed var(--s-100);
border-radius: var(--border-radius-small);
+ max-height: 8vh;
+ overflow: auto;
&:hover {
background: var(--s-25);
diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyEmailHead.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyEmailHead.vue
index e82fb2ecc..b2c7dd589 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ReplyEmailHead.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ReplyEmailHead.vue
@@ -83,6 +83,10 @@ export default {
}
},
},
+ mounted() {
+ this.ccEmailsVal = this.ccEmails;
+ this.bccEmailsVal = this.bccEmails;
+ },
validations: {
ccEmailsVal: {
hasValidEmails(value) {
diff --git a/app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/advancedFilterItems.spec.js b/app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/advancedFilterItems.spec.js
new file mode 100644
index 000000000..69eafa994
--- /dev/null
+++ b/app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/advancedFilterItems.spec.js
@@ -0,0 +1,9 @@
+import defaultFilters from './index';
+import { filterAttributeGroups } from './index';
+
+describe('#filterItems', () => {
+ it('Matches the correct filterItems', () => {
+ expect(defaultFilters).toMatchObject(defaultFilters);
+ expect(filterAttributeGroups).toMatchObject(filterAttributeGroups);
+ });
+});
diff --git a/app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js b/app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js
index 98e27c5bb..a3e8e2c10 100644
--- a/app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js
+++ b/app/javascript/dashboard/components/widgets/conversation/advancedFilterItems/index.js
@@ -2,6 +2,7 @@ import {
OPERATOR_TYPES_1,
OPERATOR_TYPES_2,
OPERATOR_TYPES_3,
+ OPERATOR_TYPES_5,
} from '../../FilterInput/FilterOperatorTypes';
const filterTypes = [
@@ -85,6 +86,30 @@ const filterTypes = [
filterOperators: OPERATOR_TYPES_3,
attributeModel: 'additional',
},
+ {
+ attributeKey: 'created_at',
+ attributeI18nKey: 'CREATED_AT',
+ inputType: 'date',
+ dataType: 'text',
+ filterOperators: OPERATOR_TYPES_5,
+ attributeModel: 'standard',
+ },
+ {
+ attributeKey: 'last_activity_at',
+ attributeI18nKey: 'LAST_ACTIVITY',
+ inputType: 'date',
+ dataType: 'text',
+ filterOperators: OPERATOR_TYPES_5,
+ attributeModel: 'standard',
+ },
+ {
+ attributeKey: 'referer',
+ attributeI18nKey: 'REFERER_LINK',
+ inputType: 'plain_text',
+ dataType: 'text',
+ filterOperators: OPERATOR_TYPES_5,
+ attributeModel: 'standard',
+ },
];
export const filterAttributeGroups = [
@@ -120,6 +145,14 @@ export const filterAttributeGroups = [
key: 'labels',
i18nKey: 'LABELS',
},
+ {
+ key: 'created_at',
+ i18nKey: 'CREATED_AT',
+ },
+ {
+ key: 'last_activity_at',
+ i18nKey: 'LAST_ACTIVITY',
+ },
],
},
{
diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue
index 957402f78..a20f3d34c 100644
--- a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue
@@ -35,6 +35,19 @@
size="16"
/>
+
+
+
+
+
+
+
+
+
+ {{ description }}
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/helper/URLHelper.js b/app/javascript/dashboard/helper/URLHelper.js
index e229ce7e4..583a99c7c 100644
--- a/app/javascript/dashboard/helper/URLHelper.js
+++ b/app/javascript/dashboard/helper/URLHelper.js
@@ -1,10 +1,22 @@
import queryString from 'query-string';
+import { DEFAULT_REDIRECT_URL } from '../constants';
export const frontendURL = (path, params) => {
const stringifiedParams = params ? `?${queryString.stringify(params)}` : '';
return `/app/${path}${stringifiedParams}`;
};
+export const getLoginRedirectURL = (ssoAccountId, user) => {
+ const { accounts = [] } = user || {};
+ const ssoAccount = accounts.find(
+ account => account.id === Number(ssoAccountId)
+ );
+ if (ssoAccount) {
+ return frontendURL(`accounts/${ssoAccountId}/dashboard`);
+ }
+ return DEFAULT_REDIRECT_URL;
+};
+
export const conversationUrl = ({
accountId,
activeInbox,
diff --git a/app/javascript/dashboard/helper/localStorage.js b/app/javascript/dashboard/helper/localStorage.js
new file mode 100644
index 000000000..cc963aa48
--- /dev/null
+++ b/app/javascript/dashboard/helper/localStorage.js
@@ -0,0 +1,17 @@
+class LocalStorage {
+ constructor(key) {
+ this.key = key;
+ }
+
+ store(allItems) {
+ localStorage.setItem(this.key, JSON.stringify(allItems));
+ localStorage.setItem(this.key + ':ts', Date.now());
+ }
+
+ get() {
+ let stored = localStorage.getItem(this.key);
+ return JSON.parse(stored) || [];
+ }
+}
+
+export default LocalStorage;
diff --git a/app/javascript/dashboard/helper/specs/URLHelper.spec.js b/app/javascript/dashboard/helper/specs/URLHelper.spec.js
index ade735642..832d90577 100644
--- a/app/javascript/dashboard/helper/specs/URLHelper.spec.js
+++ b/app/javascript/dashboard/helper/specs/URLHelper.spec.js
@@ -3,6 +3,7 @@ import {
conversationUrl,
accountIdFromPathname,
isValidURL,
+ getLoginRedirectURL,
} from '../URLHelper';
describe('#URL Helpers', () => {
@@ -58,4 +59,24 @@ describe('#URL Helpers', () => {
expect(isValidURL('alert.window')).toBe(false);
});
});
+
+ describe('getLoginRedirectURL', () => {
+ it('should return correct Account URL if account id is present', () => {
+ expect(
+ getLoginRedirectURL('7500', {
+ accounts: [{ id: 7500, name: 'Test Account 7500' }],
+ })
+ ).toBe('/app/accounts/7500/dashboard');
+ });
+
+ it('should return default URL if account id is not present', () => {
+ expect(getLoginRedirectURL('7500', {})).toBe('/app/');
+ expect(
+ getLoginRedirectURL('7500', {
+ accounts: [{ id: '7501', name: 'Test Account 7501' }],
+ })
+ ).toBe('/app/');
+ expect(getLoginRedirectURL('7500', null)).toBe('/app/');
+ });
+ });
});
diff --git a/app/javascript/dashboard/i18n/locale/en/advancedFilters.json b/app/javascript/dashboard/i18n/locale/en/advancedFilters.json
index f00973f29..46a573c42 100644
--- a/app/javascript/dashboard/i18n/locale/en/advancedFilters.json
+++ b/app/javascript/dashboard/i18n/locale/en/advancedFilters.json
@@ -21,7 +21,12 @@
"is_present": "Is present",
"is_not_present": "Is not present",
"is_greater_than": "Is greater than",
- "is_lesser_than": "Is lesser than"
+ "is_less_than": "Is lesser than",
+ "days_before": "Is x days before"
+ },
+ "ATTRIBUTE_LABELS": {
+ "TRUE": "True",
+ "FALSE": "False"
},
"ATTRIBUTES": {
"STATUS": "Status",
@@ -38,7 +43,9 @@
"CUSTOM_ATTRIBUTE_TEXT": "Text",
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
"CUSTOM_ATTRIBUTE_LINK": "Link",
- "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox"
+ "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
+ "CREATED_AT": "Created At",
+ "LAST_ACTIVITY": "Last Activity"
},
"GROUPS": {
"STANDARD_FILTERS": "Standard Filters",
diff --git a/app/javascript/dashboard/i18n/locale/en/automation.json b/app/javascript/dashboard/i18n/locale/en/automation.json
index ce95b861c..8c92467bd 100644
--- a/app/javascript/dashboard/i18n/locale/en/automation.json
+++ b/app/javascript/dashboard/i18n/locale/en/automation.json
@@ -90,6 +90,18 @@
},
"ACTION": {
"DELETE_MESSAGE": "You need to have atleast one action to save"
+ },
+ "TOGGLE": {
+ "ACTIVATION_TITLE": "Activate Automation Rule",
+ "DEACTIVATION_TITLE": "Deactivate Automation Rule",
+ "ACTIVATION_DESCRIPTION": "This action will activate the automation rule '{automationName}'. Are you sure you want to proceed?",
+ "DEACTIVATION_DESCRIPTION": "This action will deactivate the automation rule '{automationName}'. Are you sure you want to proceed?",
+ "ACTIVATION_SUCCESFUL": "Automation Rule Activated Successfully",
+ "DEACTIVATION_SUCCESFUL": "Automation Rule Deactivated Successfully",
+ "ACTIVATION_ERROR": "Could not Activate Automation, Please try again later",
+ "DEACTIVATION_ERROR": "Could not Deactivate Automation, Please try again later",
+ "CONFIRMATION_LABEL": "Yes",
+ "CANCEL_LABEL": "No"
}
}
}
diff --git a/app/javascript/dashboard/i18n/locale/en/chatlist.json b/app/javascript/dashboard/i18n/locale/en/chatlist.json
index c1ff81c2d..ccff2c33b 100644
--- a/app/javascript/dashboard/i18n/locale/en/chatlist.json
+++ b/app/javascript/dashboard/i18n/locale/en/chatlist.json
@@ -76,6 +76,7 @@
"RECEIVED_VIA_EMAIL": "Received via email",
"VIEW_TWEET_IN_TWITTER": "View tweet in Twitter",
"REPLY_TO_TWEET": "Reply to this tweet",
+ "LINK_TO_STORY": "Go to instagram story",
"SENT": "Sent successfully",
"NO_MESSAGES": "No Messages",
"NO_CONTENT": "No content available",
diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json
index 3824d9593..037f6f769 100644
--- a/app/javascript/dashboard/i18n/locale/en/contact.json
+++ b/app/javascript/dashboard/i18n/locale/en/contact.json
@@ -77,9 +77,8 @@
"CONFIRM": {
"TITLE": "Confirm Deletion",
"MESSAGE": "Are you sure to delete ",
- "PLACE_HOLDER": "Please type {contactName} to confirm",
- "YES": "Yes, Delete ",
- "NO": "No, Keep "
+ "YES": "Yes, Delete",
+ "NO": "No, Keep"
},
"API": {
"SUCCESS_MESSAGE": "Contact deleted successfully",
diff --git a/app/javascript/dashboard/i18n/locale/en/contactFilters.json b/app/javascript/dashboard/i18n/locale/en/contactFilters.json
index a752d537c..bd58ccf3e 100644
--- a/app/javascript/dashboard/i18n/locale/en/contactFilters.json
+++ b/app/javascript/dashboard/i18n/locale/en/contactFilters.json
@@ -22,7 +22,8 @@
"is_present": "Is present",
"is_not_present": "Is not present",
"is_greater_than": "Is greater than",
- "is_lesser_than": "Is lesser than"
+ "is_lesser_than": "Is lesser than",
+ "days_before": "Is x days before"
},
"ATTRIBUTES": {
"NAME": "Name",
@@ -35,7 +36,9 @@
"CUSTOM_ATTRIBUTE_TEXT": "Text",
"CUSTOM_ATTRIBUTE_NUMBER": "Number",
"CUSTOM_ATTRIBUTE_LINK": "Link",
- "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox"
+ "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
+ "CREATED_AT": "Created At",
+ "LAST_ACTIVITY": "Last Activity"
},
"GROUPS": {
"STANDARD_FILTERS": "Standard Filters",
diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json
index 96ba4b429..ac25a5aed 100644
--- a/app/javascript/dashboard/i18n/locale/en/conversation.json
+++ b/app/javascript/dashboard/i18n/locale/en/conversation.json
@@ -74,8 +74,14 @@
"TIP_FORMAT_ICON": "Show rich text editor",
"TIP_EMOJI_ICON": "Show emoji selector",
"TIP_ATTACH_ICON": "Attach files",
+ "TIP_AUDIORECORDER_ICON": "Record audio",
+ "TIP_AUDIORECORDER_PERMISSION": "Allow access to audio",
+ "TIP_AUDIORECORDER_ERROR": "Could not open the audio",
"ENTER_TO_SEND": "Enter to send",
"DRAG_DROP": "Drag and drop here to attach",
+ "START_AUDIO_RECORDING": "Start audio recording",
+ "STOP_AUDIO_RECORDING": "Stop audio recording",
+ "": "",
"EMAIL_HEAD": {
"ADD_BCC": "Add bcc",
"CC": {
diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json
index f365a3248..c22d9ee3f 100644
--- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json
+++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json
@@ -47,7 +47,8 @@
"CUSTOM_EMAIL_DOMAIN_ENABLED": "You can receive emails in your custom domain now."
}
},
- "UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance."
+ "UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance.",
+ "LEARN_MORE":"Learn more"
},
"FORMS": {
"MULTISELECT": {
diff --git a/app/javascript/dashboard/i18n/locale/en/report.json b/app/javascript/dashboard/i18n/locale/en/report.json
index 229c7b068..053c730e1 100644
--- a/app/javascript/dashboard/i18n/locale/en/report.json
+++ b/app/javascript/dashboard/i18n/locale/en/report.json
@@ -333,6 +333,11 @@
"CSAT_REPORTS": {
"HEADER": "CSAT Reports",
"NO_RECORDS": "There are no CSAT survey responses available.",
+ "FILTERS": {
+ "AGENTS": {
+ "PLACEHOLDER": "Choose Agents"
+ }
+ },
"TABLE": {
"HEADER": {
"CONTACT_NAME": "Contact",
diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json
index d353e6b6e..5727e09d0 100644
--- a/app/javascript/dashboard/i18n/locale/en/settings.json
+++ b/app/javascript/dashboard/i18n/locale/en/settings.json
@@ -178,7 +178,8 @@
"REPORTS_LABEL": "Labels",
"REPORTS_INBOX": "Inbox",
"REPORTS_TEAM": "Team",
- "SET_AVAILABILITY_TITLE": "Set yourself as"
+ "SET_AVAILABILITY_TITLE": "Set yourself as",
+ "BETA": "Beta"
},
"CREATE_ACCOUNT": {
"NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.",
diff --git a/app/javascript/dashboard/mixins/reportMixin.js b/app/javascript/dashboard/mixins/reportMixin.js
new file mode 100644
index 000000000..8ef7fdfe5
--- /dev/null
+++ b/app/javascript/dashboard/mixins/reportMixin.js
@@ -0,0 +1,33 @@
+import { mapGetters } from 'vuex';
+import { formatTime } from '@chatwoot/utils';
+
+export default {
+ computed: {
+ ...mapGetters({
+ accountSummary: 'getAccountSummary',
+ }),
+ calculateTrend() {
+ return metric_key => {
+ if (!this.accountSummary.previous[metric_key]) return 0;
+ return Math.round(
+ ((this.accountSummary[metric_key] -
+ this.accountSummary.previous[metric_key]) /
+ this.accountSummary.previous[metric_key]) *
+ 100
+ );
+ };
+ },
+ displayMetric() {
+ return metric_key => {
+ if (
+ ['avg_first_response_time', 'avg_resolution_time'].includes(
+ metric_key
+ )
+ ) {
+ return formatTime(this.accountSummary[metric_key]);
+ }
+ return this.accountSummary[metric_key];
+ };
+ },
+ },
+};
diff --git a/app/javascript/dashboard/mixins/specs/reportMixin.spec.js b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js
new file mode 100644
index 000000000..003295f95
--- /dev/null
+++ b/app/javascript/dashboard/mixins/specs/reportMixin.spec.js
@@ -0,0 +1,41 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import reportMixin from '../reportMixin';
+import reportFixtures from './reportMixinFixtures';
+import Vuex from 'vuex';
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('reportMixin', () => {
+ let getters;
+ let store;
+ beforeEach(() => {
+ getters = {
+ getAccountSummary: () => reportFixtures.summary,
+ };
+ store = new Vuex.Store({ getters });
+ });
+
+ it('display the metric', () => {
+ const Component = {
+ render() {},
+ title: 'TestComponent',
+ mixins: [reportMixin],
+ };
+ const wrapper = shallowMount(Component, { store, localVue });
+ expect(wrapper.vm.displayMetric('conversations_count')).toEqual(5);
+ expect(wrapper.vm.displayMetric('avg_first_response_time')).toEqual(
+ '3 Min'
+ );
+ });
+
+ it('calculate the trend', () => {
+ const Component = {
+ render() {},
+ title: 'TestComponent',
+ mixins: [reportMixin],
+ };
+ const wrapper = shallowMount(Component, { store, localVue });
+ expect(wrapper.vm.calculateTrend('conversations_count')).toEqual(25);
+ expect(wrapper.vm.calculateTrend('resolutions_count')).toEqual(0);
+ });
+});
diff --git a/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js
new file mode 100644
index 000000000..5c8315ab1
--- /dev/null
+++ b/app/javascript/dashboard/mixins/specs/reportMixinFixtures.js
@@ -0,0 +1,18 @@
+export default {
+ summary: {
+ avg_first_response_time: '198.6666666666667',
+ avg_resolution_time: '208.3333333333333',
+ conversations_count: 5,
+ incoming_messages_count: 5,
+ outgoing_messages_count: 3,
+ previous: {
+ avg_first_response_time: '89.0',
+ avg_resolution_time: '145.0',
+ conversations_count: 4,
+ incoming_messages_count: 5,
+ outgoing_messages_count: 4,
+ resolutions_count: 0,
+ },
+ resolutions_count: 3,
+ },
+};
diff --git a/app/javascript/dashboard/routes/dashboard/Dashboard.vue b/app/javascript/dashboard/routes/dashboard/Dashboard.vue
index 670c93d5d..11903c9ed 100644
--- a/app/javascript/dashboard/routes/dashboard/Dashboard.vue
+++ b/app/javascript/dashboard/routes/dashboard/Dashboard.vue
@@ -1,9 +1,33 @@
@@ -12,16 +36,28 @@
import Sidebar from '../../components/layout/Sidebar';
import CommandBar from './commands/commandbar.vue';
import { BUS_EVENTS } from 'shared/constants/busEvents';
+import WootKeyShortcutModal from 'dashboard/components/widgets/modal/WootKeyShortcutModal';
+import AddAccountModal from 'dashboard/components/layout/sidebarComponents/AddAccountModal';
+import AccountSelector from 'dashboard/components/layout/sidebarComponents/AccountSelector';
+import AddLabelModal from 'dashboard/routes/dashboard/settings/labels/AddLabel.vue';
export default {
components: {
Sidebar,
CommandBar,
+ WootKeyShortcutModal,
+ AddAccountModal,
+ AccountSelector,
+ AddLabelModal,
},
data() {
return {
isSidebarOpen: false,
isOnDesktop: true,
+ showAccountModal: false,
+ showCreateAccountModal: false,
+ showAddLabelModal: false,
+ showShortcutModal: false,
};
},
computed: {
@@ -68,6 +104,28 @@ export default {
toggleSidebar() {
this.isSidebarOpen = !this.isSidebarOpen;
},
+ openCreateAccountModal() {
+ this.showAccountModal = false;
+ this.showCreateAccountModal = true;
+ },
+ closeCreateAccountModal() {
+ this.showCreateAccountModal = false;
+ },
+ toggleAccountModal() {
+ this.showAccountModal = !this.showAccountModal;
+ },
+ toggleKeyShortcutModal() {
+ this.showShortcutModal = true;
+ },
+ closeKeyShortcutModal() {
+ this.showShortcutModal = false;
+ },
+ showAddLabelPopup() {
+ this.showAddLabelModal = true;
+ },
+ hideAddLabelPopup() {
+ this.showAddLabelModal = false;
+ },
},
};
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue
index 04ba42f33..95c9ae61c 100644
--- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue
+++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue
@@ -11,7 +11,12 @@
v-model="appliedFilters[i]"
:filter-groups="filterGroups"
:grouped-filters="true"
- :input-type="getInputType(appliedFilters[i].attribute_key)"
+ :input-type="
+ getInputType(
+ appliedFilters[i].attribute_key,
+ appliedFilters[i].filter_operator
+ )
+ "
:operators="getOperators(appliedFilters[i].attribute_key)"
:dropdown-values="getDropdownValues(appliedFilters[i].attribute_key)"
:show-query-operator="i !== appliedFilters.length - 1"
@@ -86,6 +91,12 @@ export default {
$each: {
values: {
required,
+ ensureBetween0to999(value, prop) {
+ if (prop.filter_operator === 'days_before') {
+ return parseInt(value, 10) > 0 && parseInt(value, 10) < 999;
+ }
+ return true;
+ },
},
},
},
@@ -147,6 +158,12 @@ export default {
switch (key) {
case 'date':
return 'date';
+ case 'text':
+ return 'plain_text';
+ case 'list':
+ return 'search_select';
+ case 'checkbox':
+ return 'search_select';
default:
return 'plain_text';
}
@@ -155,7 +172,9 @@ export default {
const type = this.filterTypes.find(filter => filter.attributeKey === key);
return type.attributeModel;
},
- getInputType(key) {
+ getInputType(key, operator) {
+ 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;
},
@@ -164,6 +183,44 @@ export default {
return type.filterOperators;
},
getDropdownValues(type) {
+ const allCustomAttributes = this.$store.getters[
+ 'attributes/getAttributesByModel'
+ ](this.attributeModel);
+ const isCustomAttributeCheckbox = allCustomAttributes.find(attr => {
+ return (
+ attr.attribute_key === type &&
+ attr.attribute_display_type === 'checkbox'
+ );
+ });
+ if (isCustomAttributeCheckbox) {
+ return [
+ {
+ id: true,
+ name: this.$t('FILTER.ATTRIBUTE_LABELS.TRUE'),
+ },
+ {
+ id: false,
+ name: this.$t('FILTER.ATTRIBUTE_LABELS.FALSE'),
+ },
+ ];
+ }
+
+ const isCustomAttributeList = allCustomAttributes.find(attr => {
+ return (
+ attr.attribute_key === type && attr.attribute_display_type === 'list'
+ );
+ });
+
+ if (isCustomAttributeList) {
+ return allCustomAttributes
+ .find(attr => attr.attribute_key === type)
+ .attribute_values.map(item => {
+ return {
+ id: item,
+ name: item,
+ };
+ });
+ }
switch (type) {
case 'country_code':
return countries;
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue
index 7536c0d24..dbac2f0bc 100644
--- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue
+++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue
@@ -253,7 +253,7 @@ export default {
this.$store.dispatch('contacts/get', requestParams);
} else {
this.$store.dispatch('contacts/search', {
- search: value,
+ search: encodeURIComponent(value),
...requestParams,
});
}
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js b/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js
index dc12d2640..2d54c37bc 100644
--- a/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js
+++ b/app/javascript/dashboard/routes/dashboard/contacts/contactFilterItems/index.js
@@ -1,6 +1,7 @@
import {
OPERATOR_TYPES_1,
OPERATOR_TYPES_3,
+ OPERATOR_TYPES_5,
} from 'dashboard/components/widgets/FilterInput/FilterOperatorTypes.js';
const filterTypes = [
{
@@ -51,6 +52,30 @@ const filterTypes = [
filterOperators: OPERATOR_TYPES_3,
attribute_type: 'standard',
},
+ {
+ attributeKey: 'created_at',
+ attributeI18nKey: 'CREATED_AT',
+ inputType: 'date',
+ dataType: 'text',
+ filterOperators: OPERATOR_TYPES_5,
+ attributeModel: 'standard',
+ },
+ {
+ attributeKey: 'last_activity_at',
+ attributeI18nKey: 'LAST_ACTIVITY',
+ inputType: 'date',
+ dataType: 'text',
+ filterOperators: OPERATOR_TYPES_5,
+ attributeModel: 'standard',
+ },
+ {
+ attributeKey: 'referer',
+ attributeI18nKey: 'REFERER_LINK',
+ inputType: 'plain_text',
+ dataType: 'text',
+ filterOperators: OPERATOR_TYPES_5,
+ attributeModel: 'standard',
+ },
];
export const filterAttributeGroups = [
@@ -82,6 +107,14 @@ export const filterAttributeGroups = [
key: 'city',
i18nKey: 'CITY',
},
+ {
+ key: 'created_at',
+ i18nKey: 'CREATED_AT',
+ },
+ {
+ key: 'last_activity_at',
+ i18nKey: 'LAST_ACTIVITY',
+ },
],
},
];
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue
index 2b3dae832..11e0fb675 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue
@@ -130,11 +130,13 @@ export default {
facebook: '',
twitter: '',
linkedin: '',
+ github: '',
},
socialProfileKeys: [
{ key: 'facebook', prefixURL: 'https://facebook.com/' },
{ key: 'twitter', prefixURL: 'https://twitter.com/' },
{ key: 'linkedin', prefixURL: 'https://linkedin.com/' },
+ { key: 'github', prefixURL: 'https://github.com/' },
],
};
},
@@ -183,6 +185,7 @@ export default {
twitter: socialProfiles.twitter || twitterScreenName || '',
facebook: socialProfiles.facebook || '',
linkedin: socialProfiles.linkedin || '',
+ github: socialProfiles.github || '',
};
},
getContactObject() {
diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
index 4a58d8a54..24aeeff88 100644
--- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
+++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue
@@ -126,17 +126,15 @@
@close="toggleMergeModal"
/>
-
@@ -210,8 +208,7 @@ export default {
if (!cityAndCountry) {
return '';
}
- const countryFlag = countryCode ? flag(countryCode) : '🌎';
- return `${cityAndCountry} ${countryFlag}`;
+ return this.findCountryFlag(countryCode, cityAndCountry);
},
socialProfiles() {
const {
@@ -222,22 +219,11 @@ export default {
return { twitter: twitterScreenName, ...(socialProfiles || {}) };
},
// Delete Modal
- deleteConfirmText() {
- return `${this.$t('DELETE_CONTACT.CONFIRM.YES')} ${this.contact.name}`;
- },
- deleteRejectText() {
- return `${this.$t('DELETE_CONTACT.CONFIRM.NO')} ${this.contact.name}`;
- },
confirmDeleteMessage() {
return `${this.$t('DELETE_CONTACT.CONFIRM.MESSAGE')} ${
this.contact.name
} ?`;
},
- confirmPlaceHolderText() {
- return `${this.$t('DELETE_CONTACT.CONFIRM.PLACE_HOLDER', {
- contactName: this.contact.name,
- })}`;
- },
},
methods: {
toggleMergeModal() {
@@ -261,6 +247,14 @@ export default {
this.showConversationModal = false;
this.showEditModal = false;
},
+ findCountryFlag(countryCode, cityAndCountry) {
+ try {
+ const countryFlag = countryCode ? flag(countryCode) : '🌎';
+ return `${cityAndCountry} ${countryFlag}`;
+ } catch (error) {
+ return '';
+ }
+ },
async deleteContact({ id }) {
try {
await this.$store.dispatch('contacts/delete', id);
diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue
index be79dd4f5..811fc95c6 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue
@@ -34,12 +34,19 @@
{{ automation.name }} |
{{ automation.description }} |
-
-
+
|
{{ readableTime(automation.created_on) }} |
@@ -120,6 +127,11 @@
@saveAutomation="submitAutomation"
/>
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue
index 1f2839048..fe0240d74 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue
@@ -10,6 +10,7 @@
@@ -48,6 +50,7 @@ import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format';
import ReportFilterSelector from './components/FilterSelector';
import { GROUP_BY_FILTER } from './constants';
+import reportMixin from '../../../../mixins/reportMixin';
const REPORTS_KEYS = {
CONVERSATIONS: 'conversations_count',
@@ -62,6 +65,7 @@ export default {
components: {
ReportFilterSelector,
},
+ mixins: [reportMixin],
data() {
return {
from: 0,
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue
index 8df8beec3..885b76283 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue
@@ -24,7 +24,7 @@
@change="onChange"
/>
@@ -41,6 +41,26 @@
@input="changeFilterSelection"
/>
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue
index 2fd32ee69..857a74363 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue
@@ -255,7 +255,6 @@ export default {
},
methods: {
onDateRangeChange() {
- console.log(this.from, this.to);
this.$emit('date-range-change', {
from: this.from,
to: this.to,
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue
index 449b0383f..8ce264080 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue
@@ -27,7 +27,8 @@
:heading="metric.NAME"
:index="index"
:on-click="changeSelection"
- :point="accountSummary[metric.KEY]"
+ :point="displayMetric(metric.KEY)"
+ :trend="calculateTrend(metric.KEY)"
:selected="index === currentSelection"
/>
@@ -55,6 +56,7 @@ import ReportFilters from './ReportFilters';
import fromUnixTime from 'date-fns/fromUnixTime';
import format from 'date-fns/format';
import { GROUP_BY_FILTER } from '../constants';
+import reportMixin from '../../../../../mixins/reportMixin';
const REPORTS_KEYS = {
CONVERSATIONS: 'conversations_count',
@@ -68,6 +70,7 @@ export default {
components: {
ReportFilters,
},
+ mixins: [reportMixin],
props: {
type: {
type: String,
diff --git a/app/javascript/dashboard/routes/index.js b/app/javascript/dashboard/routes/index.js
index b2d19cf7f..14ef77bc3 100644
--- a/app/javascript/dashboard/routes/index.js
+++ b/app/javascript/dashboard/routes/index.js
@@ -5,6 +5,7 @@ import login from './login/login.routes';
import dashboard from './dashboard/dashboard.routes';
import authRoute from './auth/auth.routes';
import { frontendURL } from '../helper/URLHelper';
+import { clearBrowserSessionCookies } from '../store/utils/api';
const routes = [
...login.routes,
@@ -101,6 +102,13 @@ export const validateAuthenticateRoutePermission = (to, from, next) => {
return nextRoute ? next(frontendURL(nextRoute)) : next();
};
+const validateSSOLoginParams = to => {
+ const isLoginRoute = to.name === 'login';
+ const { email, sso_auth_token: ssoAuthToken } = to.query || {};
+ const hasValidSSOParams = email && ssoAuthToken;
+ return isLoginRoute && hasValidSSOParams;
+};
+
const validateRouteAccess = (to, from, next) => {
if (
window.chatwootConfig.signupEnabled !== 'true' &&
@@ -111,6 +119,11 @@ const validateRouteAccess = (to, from, next) => {
next(frontendURL(`accounts/${user.account_id}/dashboard`));
}
+ if (validateSSOLoginParams(to)) {
+ clearBrowserSessionCookies();
+ return next();
+ }
+
if (authIgnoreRoutes.includes(to.name)) {
return next();
}
diff --git a/app/javascript/dashboard/routes/login/Login.vue b/app/javascript/dashboard/routes/login/Login.vue
index 92fea2e36..428974af7 100644
--- a/app/javascript/dashboard/routes/login/Login.vue
+++ b/app/javascript/dashboard/routes/login/Login.vue
@@ -80,6 +80,7 @@ export default {
mixins: [globalConfigMixin],
props: {
ssoAuthToken: { type: String, default: '' },
+ ssoAccountId: { type: String, default: '' },
redirectUrl: { type: String, default: '' },
config: { type: String, default: '' },
email: { type: String, default: '' },
@@ -138,6 +139,7 @@ export default {
: this.credentials.email,
password: this.credentials.password,
sso_auth_token: this.ssoAuthToken,
+ ssoAccountId: this.ssoAccountId,
};
this.$store
.dispatch('login', credentials)
diff --git a/app/javascript/dashboard/routes/login/login.routes.js b/app/javascript/dashboard/routes/login/login.routes.js
index c53cffa92..f43e632bf 100644
--- a/app/javascript/dashboard/routes/login/login.routes.js
+++ b/app/javascript/dashboard/routes/login/login.routes.js
@@ -12,6 +12,7 @@ export default {
email: route.query.email,
ssoAuthToken: route.query.sso_auth_token,
redirectUrl: route.query.route_url,
+ ssoAccountId: route.query.sso_account_id,
}),
},
],
diff --git a/app/javascript/dashboard/store/modules/auth.js b/app/javascript/dashboard/store/modules/auth.js
index d9ec54564..5b2feef11 100644
--- a/app/javascript/dashboard/store/modules/auth.js
+++ b/app/javascript/dashboard/store/modules/auth.js
@@ -6,7 +6,7 @@ import authAPI from '../../api/auth';
import createAxios from '../../helper/APIHelper';
import actionCable from '../../helper/actionCable';
import { setUser, getHeaderExpiry, clearCookiesOnLogout } from '../utils/api';
-import { DEFAULT_REDIRECT_URL } from '../../constants';
+import { getLoginRedirectURL } from '../../helper/URLHelper';
const state = {
currentUser: {
@@ -88,15 +88,16 @@ export const getters = {
// actions
export const actions = {
- login({ commit }, credentials) {
+ login({ commit }, { ssoAccountId, ...credentials }) {
return new Promise((resolve, reject) => {
authAPI
.login(credentials)
- .then(() => {
+ .then(response => {
commit(types.default.SET_CURRENT_USER);
window.axios = createAxios(axios);
actionCable.init(Vue);
- window.location = DEFAULT_REDIRECT_URL;
+
+ window.location = getLoginRedirectURL(ssoAccountId, response.data);
resolve();
})
.catch(error => {
diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js
index e4b063899..60ca0d590 100644
--- a/app/javascript/dashboard/store/modules/conversations/getters.js
+++ b/app/javascript/dashboard/store/modules/conversations/getters.js
@@ -1,4 +1,5 @@
import authAPI from '../../../api/auth';
+import { MESSAGE_TYPE } from 'shared/constants/messages';
import { applyPageFilters } from './helpers';
export const getSelectedChatConversation = ({
@@ -19,6 +20,26 @@ const getters = {
);
return selectedChat || {};
},
+ getLastEmailInSelectedChat: (stage, _getters) => {
+ const selectedChat = _getters.getSelectedChat;
+ const { messages = [] } = selectedChat;
+ const lastEmail = [...messages].reverse().find(message => {
+ const {
+ content_attributes: contentAttributes = {},
+ message_type: messageType,
+ } = message;
+ const { email = {} } = contentAttributes;
+ const isIncomingOrOutgoing =
+ messageType === MESSAGE_TYPE.OUTGOING ||
+ messageType === MESSAGE_TYPE.INCOMING;
+ if (email.from && isIncomingOrOutgoing) {
+ return true;
+ }
+ return false;
+ });
+
+ return lastEmail;
+ },
getMineChats: _state => activeFilters => {
const currentUserID = authAPI.getCurrentUser().id;
diff --git a/app/javascript/dashboard/store/modules/csat.js b/app/javascript/dashboard/store/modules/csat.js
index d38b212be..7bc1dad6d 100644
--- a/app/javascript/dashboard/store/modules/csat.js
+++ b/app/javascript/dashboard/store/modules/csat.js
@@ -82,10 +82,13 @@ export const getters = {
};
export const actions = {
- get: async function getResponses({ commit }, { page = 1, from, to } = {}) {
+ get: async function getResponses(
+ { commit },
+ { page = 1, from, to, user_ids } = {}
+ ) {
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: true });
try {
- const response = await CSATReports.get({ page, from, to });
+ const response = await CSATReports.get({ page, from, to, user_ids });
commit(types.SET_CSAT_RESPONSE, response.data);
} catch (error) {
// Ignore error
@@ -93,10 +96,10 @@ export const actions = {
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: false });
}
},
- getMetrics: async function getMetrics({ commit }, { from, to }) {
+ getMetrics: async function getMetrics({ commit }, { from, to, user_ids }) {
commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: true });
try {
- const response = await CSATReports.getMetrics({ from, to });
+ const response = await CSATReports.getMetrics({ from, to, user_ids });
commit(types.SET_CSAT_RESPONSE_METRICS, response.data);
} catch (error) {
// Ignore error
diff --git a/app/javascript/dashboard/store/modules/labels.js b/app/javascript/dashboard/store/modules/labels.js
index 4183c28ed..f597212a3 100644
--- a/app/javascript/dashboard/store/modules/labels.js
+++ b/app/javascript/dashboard/store/modules/labels.js
@@ -45,7 +45,8 @@ export const actions = {
const response = await LabelsAPI.create(cannedObj);
commit(types.ADD_LABEL, response.data);
} catch (error) {
- throw new Error(error);
+ const errorMessage = error?.response?.data?.message;
+ throw new Error(errorMessage);
} finally {
commit(types.SET_LABEL_UI_FLAG, { isCreating: false });
}
diff --git a/app/javascript/dashboard/store/modules/reports.js b/app/javascript/dashboard/store/modules/reports.js
index 33f62a9c4..cb1efe3ff 100644
--- a/app/javascript/dashboard/store/modules/reports.js
+++ b/app/javascript/dashboard/store/modules/reports.js
@@ -20,6 +20,7 @@ const state = {
incoming_messages_count: 0,
outgoing_messages_count: 0,
resolutions_count: 0,
+ previous: {},
},
};
@@ -124,24 +125,6 @@ const mutations = {
},
[types.default.SET_ACCOUNT_SUMMARY](_state, summaryData) {
_state.accountSummary = summaryData;
- // Average First Response Time
- let avgFirstResTimeInHr = 0;
- if (summaryData.avg_first_response_time) {
- avgFirstResTimeInHr = (
- summaryData.avg_first_response_time / 3600
- ).toFixed(2);
- avgFirstResTimeInHr = `${avgFirstResTimeInHr} Hr`;
- }
- // Average Resolution Time
- let avgResolutionTimeInHr = 0;
- if (summaryData.avg_resolution_time) {
- avgResolutionTimeInHr = (summaryData.avg_resolution_time / 3600).toFixed(
- 2
- );
- avgResolutionTimeInHr = `${avgResolutionTimeInHr} Hr`;
- }
- _state.accountSummary.avg_first_response_time = avgFirstResTimeInHr;
- _state.accountSummary.avg_resolution_time = avgResolutionTimeInHr;
},
};
diff --git a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js
index bb718bb24..7dc0dade5 100644
--- a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js
@@ -131,4 +131,34 @@ describe('#getters', () => {
expect(getters.getAppliedConversationFilters(state)).toEqual(filtersList);
});
});
+
+ describe('#getLastEmailInSelectedChat', () => {
+ it('Returns cc in last email', () => {
+ const state = {};
+ const getSelectedChat = {
+ messages: [
+ {
+ message_type: 1,
+ content_attributes: {
+ email: {
+ from: 'why@how.my',
+ cc: ['nithin@me.co', 'we@who.why'],
+ },
+ },
+ },
+ ],
+ };
+ expect(
+ getters.getLastEmailInSelectedChat(state, { getSelectedChat })
+ ).toEqual({
+ message_type: 1,
+ content_attributes: {
+ email: {
+ from: 'why@how.my',
+ cc: ['nithin@me.co', 'we@who.why'],
+ },
+ },
+ });
+ });
+ });
});
diff --git a/app/javascript/dashboard/store/utils/api.js b/app/javascript/dashboard/store/utils/api.js
index 651f0cd61..37fd68c7b 100644
--- a/app/javascript/dashboard/store/utils/api.js
+++ b/app/javascript/dashboard/store/utils/api.js
@@ -38,13 +38,15 @@ export const setAuthCredentials = response => {
setUser(response.data.data, expiryDate);
};
+export const clearBrowserSessionCookies = () => {
+ Cookies.remove('auth_data');
+ Cookies.remove('user');
+};
+
export const clearCookiesOnLogout = () => {
window.bus.$emit(CHATWOOT_RESET);
window.bus.$emit(ANALYTICS_RESET);
-
- Cookies.remove('auth_data');
- Cookies.remove('user');
-
+ clearBrowserSessionCookies();
const globalConfig = window.globalConfig || {};
const logoutRedirectLink =
globalConfig.LOGOUT_REDIRECT_LINK || frontendURL('login');
diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js
index 9b3875856..79b9e927e 100644
--- a/app/javascript/packs/application.js
+++ b/app/javascript/packs/application.js
@@ -21,7 +21,10 @@ import App from '../dashboard/App';
import i18n from '../dashboard/i18n';
import createAxios from '../dashboard/helper/APIHelper';
import commonHelpers, { isJSONValid } from '../dashboard/helper/commons';
-import { getAlertAudio } from '../shared/helpers/AudioNotificationHelper';
+import {
+ getAlertAudio,
+ initOnEvents,
+} from '../shared/helpers/AudioNotificationHelper';
import { initFaviconSwitcher } from '../shared/helpers/faviconHelper';
import router from '../dashboard/routes';
import store from '../dashboard/store';
@@ -102,6 +105,13 @@ window.onload = () => {
vueActionCable.init();
};
+const setupAudioListeners = () => {
+ getAlertAudio().then(() => {
+ initOnEvents.forEach(event => {
+ document.removeEventListener(event, setupAudioListeners, false);
+ });
+ });
+};
window.addEventListener('load', () => {
verifyServiceWorkerExistence(registration =>
registration.pushManager.getSubscription().then(subscription => {
@@ -110,6 +120,9 @@ window.addEventListener('load', () => {
}
})
);
- getAlertAudio();
+ window.playAudioAlert = () => {};
+ initOnEvents.forEach(e => {
+ document.addEventListener(e, setupAudioListeners, false);
+ });
initFaviconSwitcher();
});
diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js
index c4bfcdb08..66f6ba790 100755
--- a/app/javascript/packs/sdk.js
+++ b/app/javascript/packs/sdk.js
@@ -25,6 +25,7 @@ const runSDK = ({ baseUrl, websiteToken }) => {
launcherTitle: chatwootSettings.launcherTitle || '',
showPopoutButton: chatwootSettings.showPopoutButton || false,
widgetStyle: chatwootSettings.widgetStyle || 'standard',
+ resetTriggered: false,
toggle(state) {
IFrameHelper.events.toggleBubble(state);
@@ -100,6 +101,8 @@ const runSDK = ({ baseUrl, websiteToken }) => {
baseUrl: window.$chatwoot.baseUrl,
websiteToken: window.$chatwoot.websiteToken,
});
+
+ window.$chatwoot.resetTriggered = true;
},
};
diff --git a/app/javascript/packs/widget.js b/app/javascript/packs/widget.js
index f9f57e2ef..421e2f8b9 100644
--- a/app/javascript/packs/widget.js
+++ b/app/javascript/packs/widget.js
@@ -4,7 +4,6 @@ import VueI18n from 'vue-i18n';
import store from '../widget/store';
import App from '../widget/App.vue';
import ActionCableConnector from '../widget/helpers/actionCable';
-import { getAlertAudio } from 'shared/helpers/AudioNotificationHelper';
import i18n from '../widget/i18n';
import router from '../widget/router';
@@ -33,5 +32,4 @@ window.onload = () => {
window.WOOT_WIDGET,
window.chatwootPubsubToken
);
- getAlertAudio();
};
diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js
index 2d851ba49..032519912 100644
--- a/app/javascript/sdk/IFrameHelper.js
+++ b/app/javascript/sdk/IFrameHelper.js
@@ -26,6 +26,10 @@ import { dispatchWindowEvent } from 'shared/helpers/CustomEventHelper';
import { CHATWOOT_ERROR, CHATWOOT_READY } from '../widget/constants/sdkEvents';
import { SET_USER_ERROR } from '../widget/constants/errorTypes';
import { getUserCookieName } from './cookieHelpers';
+import {
+ getAlertAudio,
+ initOnEvents,
+} from 'shared/helpers/AudioNotificationHelper';
import { isFlatWidgetStyle } from './settingsHelper';
export const IFrameHelper = {
@@ -114,6 +118,19 @@ export const IFrameHelper = {
iframe.setAttribute('style', `height: ${updatedIframeHeight} !important`);
},
+ setupAudioListeners: () => {
+ const { baseUrl = '' } = window.$chatwoot;
+ getAlertAudio(baseUrl).then(() =>
+ initOnEvents.forEach(event => {
+ document.removeEventListener(
+ event,
+ IFrameHelper.setupAudioListeners,
+ false
+ );
+ })
+ );
+ },
+
events: {
loaded: message => {
Cookies.set('cw_conversation', message.config.authToken, {
@@ -136,7 +153,18 @@ export const IFrameHelper = {
if (window.$chatwoot.user) {
IFrameHelper.sendMessage('set-user', window.$chatwoot.user);
}
+
dispatchWindowEvent({ eventName: CHATWOOT_READY });
+
+ window.playAudioAlert = () => {};
+
+ initOnEvents.forEach(e => {
+ document.addEventListener(e, IFrameHelper.setupAudioListeners, false);
+ });
+
+ if (!window.$chatwoot.resetTriggered) {
+ dispatchWindowEvent({ eventName: CHATWOOT_READY });
+ }
},
error: ({ errorType, data }) => {
dispatchWindowEvent({ eventName: CHATWOOT_ERROR, data: data });
@@ -212,6 +240,10 @@ export const IFrameHelper = {
closeChat: () => {
onBubbleClick({ toggleValue: false });
},
+
+ playAudio: () => {
+ window.playAudioAlert();
+ },
},
pushEvent: eventName => {
IFrameHelper.sendMessage('push-event', { eventName });
diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json
index 3b896a86b..0cbc85d22 100644
--- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json
+++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json
@@ -85,6 +85,11 @@
"merge-outline": "M3 6.75A.75.75 0 0 1 3.75 6h4.5a.75.75 0 0 1 .53.22L13.56 11h5.878L15.72 7.28a.75.75 0 1 1 1.06-1.06l4.998 5a.75.75 0 0 1 0 1.06l-4.998 5a.75.75 0 1 1-1.06-1.06l3.718-3.72H13.56l-4.78 4.78a.75.75 0 0 1-.531.22h-4.5a.75.75 0 0 1 0-1.5h4.19l4.25-4.25L7.94 7.5H3.75A.75.75 0 0 1 3 6.75Z",
"more-horizontal-outline": "M7.75 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM13.75 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM18 13.75a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Z",
"more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z",
+ "microphone-outline": "M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z",
+ "microphone-off-outline": "M19,11C19,12.19 18.66,13.3 18.1,14.28L16.87,13.05C17.14,12.43 17.3,11.74 17.3,11H19M15,11.16L9,5.18V5A3,3 0 0,1 12,2A3,3 0 0,1 15,5V11L15,11.16M4.27,3L21,19.73L19.73,21L15.54,16.81C14.77,17.27 13.91,17.58 13,17.72V21H11V17.72C7.72,17.23 5,14.41 5,11H6.7C6.7,14 9.24,16.1 12,16.1C12.81,16.1 13.6,15.91 14.31,15.58L12.65,13.92L12,14A3,3 0 0,1 9,11V10.28L3,4.27L4.27,3Z",
+ "microphone-stop-outline": "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4M9,9V15H15V9",
+ "microphone-pause-outline": "M13,16V8H15V16H13M9,16V8H11V16H9M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z",
+ "microphone-play-outline": "M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M10,16.5L16,12L10,7.5V16.5Z",
"number-symbol-outline": "M10.987 2.89a.75.75 0 1 0-1.474-.28L8.494 7.999 3.75 8a.75.75 0 1 0 0 1.5l4.46-.002-.946 5-4.514.002a.75.75 0 0 0 0 1.5l4.23-.002-.967 5.116a.75.75 0 1 0 1.474.278l1.02-5.395 5.474-.002-.968 5.119a.75.75 0 1 0 1.474.278l1.021-5.398 4.742-.002a.75.75 0 1 0 0-1.5l-4.458.002.946-5 4.512-.002a.75.75 0 1 0 0-1.5l-4.229.002.966-5.104a.75.75 0 0 0-1.474-.28l-1.018 5.385-5.474.002.966-5.107Zm-1.25 6.608 5.474-.003-.946 5-5.474.002.946-5Z",
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
"people-outline": "M4 13.999 13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14ZM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78ZM4 15.499l-.1.01a.51.51 0 0 0-.254.136.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242.826.445 2.003.714 3.266.753l.317.005.317-.005c1.263-.039 2.439-.308 3.266-.753.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001ZM8.5 3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm9 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3Zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2 2-.897 2-2-.897-2-2-2Z",
@@ -119,6 +124,7 @@
"brand-telegram-outline": "M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z",
"brand-twitter-outline": "M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z",
"brand-whatsapp-outline": "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z",
+ "brand-github-outline": "M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385c.6.105.825-.255.825-.57c0-.285-.015-1.23-.015-2.235c-3.015.555-3.795-.735-4.035-1.41c-.135-.345-.72-1.41-1.23-1.695c-.42-.225-1.02-.78-.015-.795c.945-.015 1.62.87 1.845 1.23c1.08 1.815 2.805 1.305 3.495.99c.105-.78.42-1.305.765-1.605c-2.67-.3-5.46-1.335-5.46-5.925c0-1.305.465-2.385 1.23-3.225c-.12-.3-.54-1.53.12-3.18c0 0 1.005-.315 3.3 1.23c.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23c.66 1.65.24 2.88.12 3.18c.765.84 1.23 1.905 1.23 3.225c0 4.605-2.805 5.625-5.475 5.925c.435.375.81 1.095.81 2.22c0 1.605-.015 2.895-.015 3.3c0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12Z",
"add-solid": "M11.883 3.007 12 3a1 1 0 0 1 .993.883L13 4v7h7a1 1 0 0 1 .993.883L21 12a1 1 0 0 1-.883.993L20 13h-7v7a1 1 0 0 1-.883.993L12 21a1 1 0 0 1-.993-.883L11 20v-7H4a1 1 0 0 1-.993-.883L3 12a1 1 0 0 1 .883-.993L4 11h7V4a1 1 0 0 1 .883-.993L12 3l-.117.007Z",
"subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z"
}
diff --git a/app/javascript/shared/components/FluentIcon/icons.json b/app/javascript/shared/components/FluentIcon/icons.json
index 62798657d..20981ed5c 100644
--- a/app/javascript/shared/components/FluentIcon/icons.json
+++ b/app/javascript/shared/components/FluentIcon/icons.json
@@ -2,12 +2,15 @@
"arrow-clockwise-outline": "M12 4.75a7.25 7.25 0 1 0 7.201 6.406c-.068-.588.358-1.156.95-1.156.515 0 .968.358 1.03.87a9.25 9.25 0 1 1-3.432-6.116V4.25a1 1 0 1 1 2.001 0v2.698l.034.052h-.034v.25a1 1 0 0 1-1 1h-3a1 1 0 1 1 0-2h.666A7.219 7.219 0 0 0 12 4.75Z",
"arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z",
"attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z",
+ "checkmark-outline": "M4.53 12.97a.75.75 0 0 0-1.06 1.06l4.5 4.5a.75.75 0 0 0 1.06 0l11-11a.75.75 0 0 0-1.06-1.06L8.5 16.94l-3.97-3.97Z",
"chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z",
"chevron-right-outline": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z",
"dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z",
"document-outline": "M18.5 20a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V4a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10Zm-5-15.379L17.378 8.5H14a.5.5 0 0 1-.5-.5V4.621Zm5.914 3.793-5.829-5.828c-.026-.026-.058-.046-.085-.07a2.072 2.072 0 0 0-.219-.18c-.04-.027-.086-.045-.128-.068-.071-.04-.141-.084-.216-.116a1.977 1.977 0 0 0-.624-.138C12.266 2.011 12.22 2 12.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414Z",
"emoji-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999Zm0 1.5a8.502 8.502 0 1 0 0 17.003A8.502 8.502 0 0 0 12 3.5ZM8.462 14.784A4.491 4.491 0 0 0 12 16.502a4.492 4.492 0 0 0 3.535-1.714.75.75 0 1 1 1.177.93A5.991 5.991 0 0 1 12 18.002a5.991 5.991 0 0 1-4.716-2.29.75.75 0 0 1 1.178-.928ZM9 8.75a1.25 1.25 0 1 1 0 2.499A1.25 1.25 0 0 1 9 8.75Zm6 0a1.25 1.25 0 1 1 0 2.499 1.25 1.25 0 0 1 0-2.499Z",
"link-outline": "M9.25 7a.75.75 0 0 1 .11 1.492l-.11.008H7a3.5 3.5 0 0 0-.206 6.994L7 15.5h2.25a.75.75 0 0 1 .11 1.492L9.25 17H7a5 5 0 0 1-.25-9.994L7 7h2.25ZM17 7a5 5 0 0 1 .25 9.994L17 17h-2.25a.75.75 0 0 1-.11-1.492l.11-.008H17a3.5 3.5 0 0 0 .206-6.994L17 8.5h-2.25a.75.75 0 0 1-.11-1.492L14.75 7H17ZM7 11.25h10a.75.75 0 0 1 .102 1.493L17 12.75H7a.75.75 0 0 1-.102-1.493L7 11.25h10H7Z",
+ "more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z",
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
- "send-outline": "M5.694 12 2.299 3.272c-.236-.607.356-1.188.942-.982l.093.04 18 9a.75.75 0 0 1 .097 1.283l-.097.058-18 9c-.583.291-1.217-.244-1.065-.847l.03-.096L5.694 12 2.299 3.272 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z"
+ "send-outline": "M5.694 12 2.299 3.272c-.236-.607.356-1.188.942-.982l.093.04 18 9a.75.75 0 0 1 .097 1.283l-.097.058-18 9c-.583.291-1.217-.244-1.065-.847l.03-.096L5.694 12 2.299 3.272 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z",
+ "sign-out-outline": ["M8.502 11.5a1.002 1.002 0 1 1 0 2.004 1.002 1.002 0 0 1 0-2.004Z","M12 4.354v6.651l7.442-.001L17.72 9.28a.75.75 0 0 1-.073-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073 2.997 2.997a.75.75 0 0 1 .073.976l-.073.084-2.996 3.004a.75.75 0 0 1-1.134-.975l.072-.085 1.713-1.717-7.431.001L12 19.25a.75.75 0 0 1-.88.739l-8.5-1.502A.75.75 0 0 1 2 17.75V5.75a.75.75 0 0 1 .628-.74l8.5-1.396a.75.75 0 0 1 .872.74Zm-1.5.883-7 1.15V17.12l7 1.236V5.237Z","M13 18.501h.765l.102-.006a.75.75 0 0 0 .648-.745l-.007-4.25H13v5.001ZM13.002 10 13 8.725V5h.745a.75.75 0 0 1 .743.647l.007.102.007 4.251h-1.5Z"]
}
diff --git a/app/javascript/shared/helpers/AudioNotificationHelper.js b/app/javascript/shared/helpers/AudioNotificationHelper.js
index 9896d6b8e..672816f90 100644
--- a/app/javascript/shared/helpers/AudioNotificationHelper.js
+++ b/app/javascript/shared/helpers/AudioNotificationHelper.js
@@ -1,9 +1,10 @@
import { MESSAGE_TYPE } from 'shared/constants/messages';
-import axios from 'axios';
+import { IFrameHelper } from 'widget/helpers/utils';
+
import { showBadgeOnFavicon } from './faviconHelper';
-export const getAlertAudio = async () => {
- window.playAudioAlert = () => {};
+export const initOnEvents = ['click', 'touchstart', 'keypress'];
+export const getAlertAudio = async (baseUrl = '') => {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const playsound = audioBuffer => {
window.playAudioAlert = () => {
@@ -16,11 +17,15 @@ export const getAlertAudio = async () => {
};
try {
- const response = await axios.get('/dashboard/audios/ding.mp3', {
- responseType: 'arraybuffer',
- });
+ const resourceUrl = `${baseUrl}/dashboard/audios/ding.mp3`;
+ const audioRequest = new Request(resourceUrl);
- audioCtx.decodeAudioData(response.data).then(playsound);
+ fetch(audioRequest)
+ .then(response => response.arrayBuffer())
+ .then(buffer => {
+ audioCtx.decodeAudioData(buffer).then(playsound);
+ return new Promise(res => res());
+ });
} catch (error) {
// error
}
@@ -89,6 +94,7 @@ export const newMessageNotification = data => {
currentUserId,
assigneeId
);
+
if (playAudio && isNotificationEnabled) {
window.playAudioAlert();
showBadgeOnFavicon();
@@ -96,5 +102,7 @@ export const newMessageNotification = data => {
};
export const playNewMessageNotificationInWidget = () => {
- window.playAudioAlert();
+ IFrameHelper.sendMessage({
+ event: 'playAudio',
+ });
};
diff --git a/app/javascript/shared/helpers/FileHelper.js b/app/javascript/shared/helpers/FileHelper.js
index a784b2ba2..d9ca9f943 100644
--- a/app/javascript/shared/helpers/FileHelper.js
+++ b/app/javascript/shared/helpers/FileHelper.js
@@ -19,7 +19,7 @@ export const fileSizeInMegaBytes = bytes => {
};
export const checkFileSizeLimit = (file, maximumUploadLimit) => {
- const fileSize = file?.file?.size;
+ const fileSize = file?.file?.size || file?.size;
const fileSizeInMB = fileSizeInMegaBytes(fileSize);
return fileSizeInMB <= maximumUploadLimit;
};
diff --git a/app/javascript/shared/helpers/MessageFormatter.js b/app/javascript/shared/helpers/MessageFormatter.js
index ba45a02b6..3bc89ec2f 100644
--- a/app/javascript/shared/helpers/MessageFormatter.js
+++ b/app/javascript/shared/helpers/MessageFormatter.js
@@ -1,4 +1,4 @@
-import marked from 'marked';
+import { marked } from 'marked';
import DOMPurify from 'dompurify';
import { escapeHtml } from './HTMLSanitizer';
@@ -47,7 +47,12 @@ class MessageFormatter {
const markedDownOutput = marked(withHash);
return markedDownOutput;
}
- return marked(this.message, { breaks: true, gfm: true });
+ DOMPurify.addHook('afterSanitizeAttributes', node => {
+ if ('target' in node) node.setAttribute('target', '_blank');
+ });
+ return DOMPurify.sanitize(
+ marked(this.message, { breaks: true, gfm: true })
+ );
}
get formattedMessage() {
diff --git a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js
index 3ac99dbdd..4e13fe832 100644
--- a/app/javascript/shared/helpers/specs/MessageFormatter.spec.js
+++ b/app/javascript/shared/helpers/specs/MessageFormatter.spec.js
@@ -6,14 +6,14 @@ describe('#MessageFormatter', () => {
const message =
'Chatwoot is an opensource tool. [Chatwoot](https://www.chatwoot.com)';
expect(new MessageFormatter(message).formattedMessage).toMatch(
- 'Chatwoot is an opensource tool. Chatwoot '
+ 'Chatwoot is an opensource tool. Chatwoot '
);
});
it('should format correctly', () => {
const message =
'Chatwoot is an opensource tool. https://www.chatwoot.com';
expect(new MessageFormatter(message).formattedMessage).toMatch(
- 'Chatwoot is an opensource tool. https://www.chatwoot.com '
+ 'Chatwoot is an opensource tool. https://www.chatwoot.com '
);
});
});
@@ -58,4 +58,14 @@ describe('#MessageFormatter', () => {
);
});
});
+
+ describe('#sanitize', () => {
+ it('sanitizes markup and removes all unnecessary elements', () => {
+ const message =
+ '[xssLink](javascript:alert(document.cookie))\n[normalLink](https://google.com)**I am a bold text paragraph**';
+ expect(new MessageFormatter(message).formattedMessage).toMatch(
+ 'xssLink normalLinkI am a bold text paragraph '
+ );
+ });
+ });
});
diff --git a/app/javascript/shared/mixins/inboxMixin.js b/app/javascript/shared/mixins/inboxMixin.js
index aebbeebc1..f0417ae93 100644
--- a/app/javascript/shared/mixins/inboxMixin.js
+++ b/app/javascript/shared/mixins/inboxMixin.js
@@ -44,6 +44,9 @@ export default {
const { medium: medium = '' } = this.inbox;
return this.isATwilioChannel && medium === 'sms';
},
+ isASmsInbox() {
+ return this.channelType === INBOX_TYPES.SMS || this.isATwilioSMSChannel;
+ },
isATwilioWhatsappChannel() {
const { medium: medium = '' } = this.inbox;
return this.isATwilioChannel && medium === 'whatsapp';
diff --git a/app/javascript/shared/mixins/specs/inboxMixin.spec.js b/app/javascript/shared/mixins/specs/inboxMixin.spec.js
index f128a44d8..dc7cc38cc 100644
--- a/app/javascript/shared/mixins/specs/inboxMixin.spec.js
+++ b/app/javascript/shared/mixins/specs/inboxMixin.spec.js
@@ -62,6 +62,18 @@ describe('inboxMixin', () => {
expect(wrapper.vm.isAWebWidgetInbox).toBe(true);
});
+ it('isASmsInbox returns true if channel type is sms', () => {
+ const Component = {
+ render() {},
+ mixins: [inboxMixin],
+ data() {
+ return { inbox: { channel_type: 'Channel::Sms' } };
+ },
+ };
+ const wrapper = shallowMount(Component);
+ expect(wrapper.vm.isASmsInbox).toBe(true);
+ });
+
it('isATwilioChannel returns true if channel type is Twilio', () => {
const Component = {
render() {},
@@ -94,6 +106,7 @@ describe('inboxMixin', () => {
const wrapper = shallowMount(Component);
expect(wrapper.vm.isATwilioChannel).toBe(true);
expect(wrapper.vm.isATwilioSMSChannel).toBe(true);
+ expect(wrapper.vm.isASmsInbox).toBe(true);
});
it('isATwilioWhatsappChannel returns true if channel type is Twilio and medium is whatsapp', () => {
diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js
index 3552765c7..fdb3842fd 100755
--- a/app/javascript/widget/api/conversation.js
+++ b/app/javascript/widget/api/conversation.js
@@ -48,6 +48,11 @@ const sendEmailTranscript = async ({ email }) => {
{ email }
);
};
+const toggleStatus = async () => {
+ return API.get(
+ `/api/v1/widget/conversations/toggle_status${window.location.search}`
+ );
+};
export {
createConversationAPI,
@@ -58,4 +63,5 @@ export {
toggleTyping,
setUserLastSeenAt,
sendEmailTranscript,
+ toggleStatus,
};
diff --git a/app/javascript/widget/components/ChatFooter.vue b/app/javascript/widget/components/ChatFooter.vue
index 423eb2642..7ed33acbd 100755
--- a/app/javascript/widget/components/ChatFooter.vue
+++ b/app/javascript/widget/components/ChatFooter.vue
@@ -37,12 +37,13 @@ import CustomButton from 'shared/components/Button';
import ChatInputWrap from 'widget/components/ChatInputWrap.vue';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { sendEmailTranscript } from 'widget/api/conversation';
-
+import routerMixin from 'widget/mixins/routerMixin';
export default {
components: {
ChatInputWrap,
CustomButton,
},
+ mixins: [routerMixin],
props: {
msg: {
type: String,
@@ -53,7 +54,7 @@ export default {
...mapGetters({
conversationAttributes: 'conversationAttributes/getConversationParams',
widgetColor: 'appConfig/getWidgetColor',
- getConversationSize: 'conversation/getConversationSize',
+ conversationSize: 'conversation/getConversationSize',
currentUser: 'contacts/getCurrentUser',
isWidgetStyleFlat: 'appConfig/isWidgetStyleFlat',
}),
@@ -80,12 +81,11 @@ export default {
'clearConversationAttributes',
]),
async handleSendMessage(content) {
- const conversationSize = this.getConversationSize;
await this.sendMessage({
content,
});
// Update conversation attributes on new conversation
- if (conversationSize === 0) {
+ if (this.conversationSize === 0) {
this.getAttributes();
}
},
@@ -95,7 +95,12 @@ export default {
startNewConversation() {
this.clearConversations();
this.clearConversationAttributes();
- window.bus.$emit(BUS_EVENTS.START_NEW_CONVERSATION);
+
+ // To create a new conversation, we are redirecting
+ // the user to pre-chat with contact fields disabled
+ // Pass disableContactFields params to the route
+ // This would disable the contact fields in the pre-chat form
+ this.replaceRoute('prechat-form', { disableContactFields: true });
},
async sendTranscript() {
const { email } = this.currentUser;
diff --git a/app/javascript/widget/components/HeaderActions.vue b/app/javascript/widget/components/HeaderActions.vue
index b9fb76ff7..4d6e5faa9 100644
--- a/app/javascript/widget/components/HeaderActions.vue
+++ b/app/javascript/widget/components/HeaderActions.vue
@@ -1,5 +1,13 @@
+
({}),
},
+ disableContactFields: {
+ type: Boolean,
+ default: false,
+ },
},
validations() {
const identityValidations = {
@@ -99,7 +103,7 @@ export default {
if (this.hasActiveCampaign) {
return identityValidations;
}
- if (this.options.requireEmail) {
+ if (this.areContactFieldsVisible) {
return {
...identityValidations,
...messageValidation,
@@ -135,6 +139,9 @@ export default {
}
return this.options.preChatMessage;
},
+ areContactFieldsVisible() {
+ return this.options.requireEmail && !this.disableContactFields;
+ },
},
methods: {
onSubmit() {
diff --git a/app/javascript/widget/components/dropdown/DropdownMenu.vue b/app/javascript/widget/components/dropdown/DropdownMenu.vue
new file mode 100644
index 000000000..1bccb7147
--- /dev/null
+++ b/app/javascript/widget/components/dropdown/DropdownMenu.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/widget/components/dropdown/DropdownMenuItem.vue b/app/javascript/widget/components/dropdown/DropdownMenuItem.vue
new file mode 100644
index 000000000..1ba355d98
--- /dev/null
+++ b/app/javascript/widget/components/dropdown/DropdownMenuItem.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
diff --git a/app/javascript/widget/helpers/actionCable.js b/app/javascript/widget/helpers/actionCable.js
index c5c262c1b..84edb35ba 100644
--- a/app/javascript/widget/helpers/actionCable.js
+++ b/app/javascript/widget/helpers/actionCable.js
@@ -11,12 +11,16 @@ class ActionCableConnector extends BaseActionCableConnector {
'conversation.typing_on': this.onTypingOn,
'conversation.typing_off': this.onTypingOff,
'conversation.status_changed': this.onStatusChange,
+ 'conversation.created': this.onConversationCreated,
'presence.update': this.onPresenceUpdate,
'contact.merged': this.onContactMerge,
};
}
onStatusChange = data => {
+ if (data.status === 'resolved') {
+ this.app.$store.dispatch('campaign/resetCampaign');
+ }
this.app.$store.dispatch('conversationAttributes/update', data);
};
@@ -33,6 +37,10 @@ class ActionCableConnector extends BaseActionCableConnector {
this.app.$store.dispatch('conversation/addOrUpdateMessage', data);
};
+ onConversationCreated = () => {
+ this.app.$store.dispatch('conversationAttributes/getAttributes');
+ };
+
onPresenceUpdate = data => {
this.app.$store.dispatch('agent/updatePresence', data.users);
};
diff --git a/app/javascript/widget/i18n/locale/en.json b/app/javascript/widget/i18n/locale/en.json
index 7718ea4d1..66bf83fe5 100644
--- a/app/javascript/widget/i18n/locale/en.json
+++ b/app/javascript/widget/i18n/locale/en.json
@@ -22,6 +22,7 @@
"IN_A_DAY": "Typically replies in a day"
},
"START_CONVERSATION": "Start Conversation",
+ "END_CONVERSATION": "End Conversation",
"CONTINUE_CONVERSATION": "Continue conversation",
"START_NEW_CONVERSATION": "Start a new conversation",
"UNREAD_VIEW": {
diff --git a/app/javascript/widget/mixins/routerMixin.js b/app/javascript/widget/mixins/routerMixin.js
index f75cc65e1..b3b37f6fd 100644
--- a/app/javascript/widget/mixins/routerMixin.js
+++ b/app/javascript/widget/mixins/routerMixin.js
@@ -1,8 +1,8 @@
export default {
methods: {
- async replaceRoute(name) {
+ async replaceRoute(name, params = {}) {
if (this.$route.name !== name) {
- return this.$router.replace({ name });
+ return this.$router.replace({ name, params });
}
return undefined;
},
diff --git a/app/javascript/widget/store/modules/campaign.js b/app/javascript/widget/store/modules/campaign.js
index 673f52567..381364543 100644
--- a/app/javascript/widget/store/modules/campaign.js
+++ b/app/javascript/widget/store/modules/campaign.js
@@ -100,6 +100,7 @@ export const actions = {
{ root: true }
);
await triggerCampaign({ campaignId, websiteToken });
+ commit('setCampaignExecuted', true);
commit('setActiveCampaign', {});
} catch (error) {
commit('setError', true);
@@ -113,6 +114,7 @@ export const actions = {
},
resetCampaign: async ({ commit }) => {
try {
+ commit('setCampaignExecuted', false);
commit('setActiveCampaign', {});
} catch (error) {
commit('setError', true);
@@ -130,6 +132,12 @@ export const mutations = {
setError($state, value) {
Vue.set($state.uiFlags, 'isError', value);
},
+ setHasFetched($state, value) {
+ Vue.set($state.uiFlags, 'hasFetched', value);
+ },
+ setCampaignExecuted($state, data) {
+ Vue.set($state, 'campaignHasExecuted', data);
+ },
};
export default {
diff --git a/app/javascript/widget/store/modules/contacts.js b/app/javascript/widget/store/modules/contacts.js
index 79fb36de6..ef146ce72 100644
--- a/app/javascript/widget/store/modules/contacts.js
+++ b/app/javascript/widget/store/modules/contacts.js
@@ -24,17 +24,38 @@ export const actions = {
},
update: async ({ dispatch }, { identifier, user: userObject }) => {
try {
+ const {
+ email,
+ name,
+ avatar_url,
+ identifier_hash,
+ phone_number,
+ company_name,
+ city,
+ country_code,
+ description,
+ custom_attributes,
+ social_profiles,
+ } = userObject;
const user = {
- email: userObject.email,
- name: userObject.name,
- avatar_url: userObject.avatar_url,
- identifier_hash: userObject.identifier_hash,
- phone_number: userObject.phone_number,
+ email,
+ name,
+ avatar_url,
+ identifier_hash,
+ phone_number,
+ additional_attributes: {
+ company_name,
+ city,
+ description,
+ country_code,
+ social_profiles,
+ },
+ custom_attributes,
};
await ContactsAPI.update(identifier, user);
dispatch('get');
- if (userObject.identifier_hash) {
+ if (identifier_hash) {
dispatch('conversation/clearConversations', {}, { root: true });
dispatch('conversation/fetchOldConversations', {}, { root: true });
}
diff --git a/app/javascript/widget/store/modules/conversation/actions.js b/app/javascript/widget/store/modules/conversation/actions.js
index 2ec3560a7..c915ea6e7 100644
--- a/app/javascript/widget/store/modules/conversation/actions.js
+++ b/app/javascript/widget/store/modules/conversation/actions.js
@@ -5,6 +5,7 @@ import {
sendAttachmentAPI,
toggleTyping,
setUserLastSeenAt,
+ toggleStatus,
} from 'widget/api/conversation';
import { createTemporaryMessage, getNonDeletedMessages } from './helpers';
@@ -130,4 +131,8 @@ export const actions = {
// IgnoreError
}
},
+
+ resolveConversation: async () => {
+ await toggleStatus();
+ },
};
diff --git a/app/javascript/widget/store/modules/specs/campaign/actions.spec.js b/app/javascript/widget/store/modules/specs/campaign/actions.spec.js
index cdd3bf12c..303149daa 100644
--- a/app/javascript/widget/store/modules/specs/campaign/actions.spec.js
+++ b/app/javascript/widget/store/modules/specs/campaign/actions.spec.js
@@ -132,6 +132,7 @@ describe('#actions', () => {
root: true,
},
],
+ ['setCampaignExecuted', true],
['setActiveCampaign', {}],
[
'conversation/setConversationUIFlag',
@@ -176,7 +177,10 @@ describe('#actions', () => {
it('sends correct actions if execute campaign API is success', async () => {
API.post.mockResolvedValue({});
await actions.resetCampaign({ commit });
- expect(commit.mock.calls).toEqual([['setActiveCampaign', {}]]);
+ expect(commit.mock.calls).toEqual([
+ ['setCampaignExecuted', false],
+ ['setActiveCampaign', {}],
+ ]);
});
});
});
diff --git a/app/javascript/widget/store/modules/specs/campaign/mutations.spec.js b/app/javascript/widget/store/modules/specs/campaign/mutations.spec.js
index b35d0df15..98a723a8b 100644
--- a/app/javascript/widget/store/modules/specs/campaign/mutations.spec.js
+++ b/app/javascript/widget/store/modules/specs/campaign/mutations.spec.js
@@ -25,4 +25,12 @@ describe('#mutations', () => {
expect(state.activeCampaign).toEqual(campaigns[0]);
});
});
+
+ describe('#setCampaignExecuted', () => {
+ it('set campaign executed flag', () => {
+ const state = { records: [], uiFlags: {}, campaignHasExecuted: false };
+ mutations.setCampaignExecuted(state, true);
+ expect(state.campaignHasExecuted).toEqual(true);
+ });
+ });
});
diff --git a/app/javascript/widget/views/Home.vue b/app/javascript/widget/views/Home.vue
index 32aae26f1..c15fea236 100755
--- a/app/javascript/widget/views/Home.vue
+++ b/app/javascript/widget/views/Home.vue
@@ -15,7 +15,6 @@
import configMixin from '../mixins/configMixin';
import TeamAvailability from 'widget/components/TeamAvailability';
import { mapGetters } from 'vuex';
-import { BUS_EVENTS } from 'shared/constants/busEvents';
import routerMixin from 'widget/mixins/routerMixin';
export default {
name: 'Home',
@@ -34,10 +33,7 @@ export default {
},
},
data() {
- return {
- isOnCollapsedView: false,
- isOnNewConversation: false,
- };
+ return {};
},
computed: {
...mapGetters({
@@ -46,12 +42,6 @@ export default {
conversationSize: 'conversation/getConversationSize',
}),
},
- mounted() {
- bus.$on(BUS_EVENTS.START_NEW_CONVERSATION, () => {
- this.isOnCollapsedView = true;
- this.isOnNewConversation = true;
- });
- },
methods: {
startConversation() {
if (this.preChatFormEnabled && !this.conversationSize) {
diff --git a/app/javascript/widget/views/PreChatForm.vue b/app/javascript/widget/views/PreChatForm.vue
index ac70e4e56..cb0b4c217 100644
--- a/app/javascript/widget/views/PreChatForm.vue
+++ b/app/javascript/widget/views/PreChatForm.vue
@@ -1,6 +1,10 @@
|