Merge branch 'develop' into chore/conversation-participants
This commit is contained in:
commit
e1a6d23854
65 changed files with 1099 additions and 333 deletions
110
Gemfile.lock
110
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)
|
||||
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -49,7 +49,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
|||
return if permitted_params(channel_attributes)[:channel].blank?
|
||||
|
||||
validate_email_channel(channel_attributes) if @inbox.inbox_type == 'Email'
|
||||
|
||||
@inbox.channel.update!(permitted_params(channel_attributes)[:channel])
|
||||
update_channel_feature_flags
|
||||
end
|
||||
|
|
|
@ -68,7 +68,7 @@ class Api::V1::Widget::BaseController < ApplicationController
|
|||
mergee_contact: @contact
|
||||
).perform
|
||||
else
|
||||
@contact.update!(email: email, name: contact_name)
|
||||
@contact.update!(email: email, name: contact_name, phone_number: contact_phone_number)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -80,6 +80,10 @@ class Api::V1::Widget::BaseController < ApplicationController
|
|||
params[:contact][:name] || contact_email.split('@')[0]
|
||||
end
|
||||
|
||||
def contact_phone_number
|
||||
params[:contact][:phone_number]
|
||||
end
|
||||
|
||||
def browser_params
|
||||
{
|
||||
browser_name: browser.name,
|
||||
|
|
|
@ -51,6 +51,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
||||
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
|
||||
message: [:content, :referer_url, :timestamp, :echo_id])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -20,12 +20,30 @@
|
|||
color: $color-heading;
|
||||
}
|
||||
|
||||
.metric-wrap {
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-size: $font-size-big;
|
||||
font-weight: $font-weight-feather;
|
||||
margin-top: $space-smaller;
|
||||
}
|
||||
|
||||
.metric-trend {
|
||||
font-size: $font-size-small;
|
||||
margin-left: $space-small;
|
||||
}
|
||||
|
||||
.metric-up {
|
||||
color: $success-color;
|
||||
}
|
||||
|
||||
.metric-down {
|
||||
color: $alert-color;
|
||||
}
|
||||
|
||||
.desc {
|
||||
@include margin($zero);
|
||||
font-size: $font-size-small;
|
||||
|
|
65
app/javascript/dashboard/components/buttons/ToggleButton.vue
Normal file
65
app/javascript/dashboard/components/buttons/ToggleButton.vue
Normal file
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
:class="{ active }"
|
||||
role="switch"
|
||||
:aria-checked="active.toString()"
|
||||
@click="onClick"
|
||||
>
|
||||
<span aria-hidden="true" :class="{ active }"></span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
active: { type: Boolean, default: false },
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClick(e) {
|
||||
this.$emit('click', e);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.toggle-button {
|
||||
background-color: var(--s-200);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
height: 19px;
|
||||
width: 34px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--border-radius-large);
|
||||
cursor: pointer;
|
||||
transition-property: background-color;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 200ms;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-button.active {
|
||||
background-color: var(--w-500);
|
||||
}
|
||||
|
||||
.toggle-button span {
|
||||
--space-one-point-five: 1.5rem;
|
||||
height: var(--space-one-point-five);
|
||||
width: var(--space-one-point-five);
|
||||
display: inline-block;
|
||||
background-color: var(--white);
|
||||
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
|
||||
rgba(59, 130, 246, 0.5) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
|
||||
rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;
|
||||
transform: translate(0, 0);
|
||||
border-radius: 100%;
|
||||
transition-property: transform;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 200ms;
|
||||
}
|
||||
.toggle-button span.active {
|
||||
transform: translate(var(--space-one-point-five), var(--space-zero));
|
||||
}
|
||||
</style>
|
|
@ -19,23 +19,6 @@
|
|||
:current-role="currentRole"
|
||||
@add-label="showAddLabelPopup"
|
||||
/>
|
||||
<woot-key-shortcut-modal
|
||||
v-if="showShortcutModal"
|
||||
@close="closeKeyShortcutModal"
|
||||
@clickaway="closeKeyShortcutModal"
|
||||
/>
|
||||
<account-selector
|
||||
:show-account-modal="showAccountModal"
|
||||
@close-account-modal="toggleAccountModal"
|
||||
@show-create-account-modal="openCreateAccountModal"
|
||||
/>
|
||||
<add-account-modal
|
||||
:show="showCreateAccountModal"
|
||||
@close-account-create-modal="closeCreateAccountModal"
|
||||
/>
|
||||
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
||||
<add-label-modal @close="hideAddLabelPopup" />
|
||||
</woot-modal>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
|
@ -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');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -141,6 +141,10 @@ export default {
|
|||
type: String,
|
||||
default: 'plain_text',
|
||||
},
|
||||
dataType: {
|
||||
type: String,
|
||||
default: 'plain_text',
|
||||
},
|
||||
operators: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
|
|
|
@ -7,9 +7,12 @@
|
|||
<h3 class="heading">
|
||||
{{ heading }}
|
||||
</h3>
|
||||
<h4 class="metric">
|
||||
{{ point }}
|
||||
</h4>
|
||||
<div class="metric-wrap">
|
||||
<h4 class="metric">
|
||||
{{ point }}
|
||||
</h4>
|
||||
<span v-if="trend !== 0" :class="trendClass">{{ trendValue }}</span>
|
||||
</div>
|
||||
<p class="desc">
|
||||
{{ desc }}
|
||||
</p>
|
||||
|
@ -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}%`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -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' ||
|
||||
|
@ -155,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;
|
||||
},
|
||||
|
|
|
@ -50,7 +50,10 @@
|
|||
<bubble-actions
|
||||
:id="data.id"
|
||||
:sender="data.sender"
|
||||
:story-sender="storySender"
|
||||
:story-id="storyId"
|
||||
:is-a-tweet="isATweet"
|
||||
:has-instagram-story="hasInstagramStory"
|
||||
:is-email="isEmailContentType"
|
||||
:is-private="data.private"
|
||||
:message-type="data.message_type"
|
||||
|
@ -146,6 +149,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hasInstagramStory: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -209,6 +216,12 @@ export default {
|
|||
sender() {
|
||||
return this.data.sender || {};
|
||||
},
|
||||
storySender() {
|
||||
return this.contentAttributes.story_sender || null;
|
||||
},
|
||||
storyId() {
|
||||
return this.contentAttributes.story_id || null;
|
||||
},
|
||||
contentType() {
|
||||
const {
|
||||
data: { content_type: contentType },
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
class="message--read"
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
:has-instagram-story="hasInstagramStory"
|
||||
/>
|
||||
<li v-show="getUnreadCount != 0" class="unread--toast">
|
||||
<span class="text-uppercase">
|
||||
|
@ -64,6 +65,7 @@
|
|||
class="message--unread"
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
:has-instagram-story="hasInstagramStory"
|
||||
/>
|
||||
</ul>
|
||||
<div
|
||||
|
@ -215,6 +217,10 @@ export default {
|
|||
return this.conversationType === 'tweet';
|
||||
},
|
||||
|
||||
hasInstagramStory() {
|
||||
return this.conversationType === 'instagram_direct_message';
|
||||
},
|
||||
|
||||
selectedTweet() {
|
||||
if (this.selectedTweetId) {
|
||||
const { messages = [] } = this.getMessages;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
@ -35,6 +35,19 @@
|
|||
size="16"
|
||||
/>
|
||||
</button>
|
||||
<a
|
||||
v-if="hasInstagramStory && (isIncoming || isOutgoing) && linkToStory"
|
||||
:href="linkToStory"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<fluent-icon
|
||||
v-tooltip.top-start="$t('CHAT_LIST.LINK_TO_STORY')"
|
||||
icon="open"
|
||||
class="action--icon cursor-pointer"
|
||||
size="16"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
||||
:href="linkToTweet"
|
||||
|
@ -67,6 +80,14 @@ export default {
|
|||
type: String,
|
||||
default: '',
|
||||
},
|
||||
storySender: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
storyId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isEmail: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
|
@ -79,6 +100,10 @@ export default {
|
|||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
hasInstagramStory: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
messageType: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
|
@ -119,6 +144,13 @@ export default {
|
|||
return `https://twitter.com/${screenName ||
|
||||
this.inbox.name}/status/${sourceId}`;
|
||||
},
|
||||
linkToStory() {
|
||||
if (!this.storyId || !this.storySender) {
|
||||
return '';
|
||||
}
|
||||
const { storySender, storyId } = this;
|
||||
return `https://www.instagram.com/stories/${storySender}/${storyId}`;
|
||||
},
|
||||
showSentIndicator() {
|
||||
return this.isOutgoing && this.sourceId && this.isAnEmailChannel;
|
||||
},
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
"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",
|
||||
|
@ -42,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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -187,7 +187,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"WHATSAPP": {
|
||||
"WHATSAPP": {
|
||||
"TITLE": "WhatsApp Channel",
|
||||
"DESC": "Start supporting your customers via WhatsApp.",
|
||||
"PROVIDERS": {
|
||||
|
@ -211,7 +211,6 @@
|
|||
"PLACEHOLDER": "API key",
|
||||
"APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here",
|
||||
"ERROR": "Please enter a valid value."
|
||||
|
||||
},
|
||||
"SUBMIT_BUTTON": "Create WhatsApp Channel",
|
||||
"API": {
|
||||
|
@ -432,6 +431,11 @@
|
|||
},
|
||||
"PRE_CHAT_FORM": {
|
||||
"DESCRIPTION": "Pre chat forms enable you to capture user information before they start conversation with you.",
|
||||
"SET_FIELDS": "Pre chat form fields",
|
||||
"SET_FIELDS_HEADER": {
|
||||
"FIELDS": "Fields",
|
||||
"REQUIRED": "Required"
|
||||
},
|
||||
"ENABLE": {
|
||||
"LABEL": "Enable pre chat form",
|
||||
"OPTIONS": {
|
||||
|
@ -464,7 +468,7 @@
|
|||
"VALIDATION_ERROR": "Starting time should be before closing time.",
|
||||
"CHOOSE": "Choose"
|
||||
},
|
||||
"ALL_DAY":"All-Day"
|
||||
"ALL_DAY": "All-Day"
|
||||
},
|
||||
"IMAP": {
|
||||
"TITLE": "IMAP",
|
||||
|
|
33
app/javascript/dashboard/mixins/reportMixin.js
Normal file
33
app/javascript/dashboard/mixins/reportMixin.js
Normal file
|
@ -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];
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
41
app/javascript/dashboard/mixins/specs/reportMixin.spec.js
Normal file
41
app/javascript/dashboard/mixins/specs/reportMixin.spec.js
Normal file
|
@ -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);
|
||||
});
|
||||
});
|
18
app/javascript/dashboard/mixins/specs/reportMixinFixtures.js
Normal file
18
app/javascript/dashboard/mixins/specs/reportMixinFixtures.js
Normal file
|
@ -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,
|
||||
},
|
||||
};
|
|
@ -1,9 +1,33 @@
|
|||
<template>
|
||||
<div class="row app-wrapper">
|
||||
<sidebar :route="currentRoute" :class="sidebarClassName"></sidebar>
|
||||
<sidebar
|
||||
:route="currentRoute"
|
||||
:class="sidebarClassName"
|
||||
@toggle-account-modal="toggleAccountModal"
|
||||
@open-key-shortcut-modal="toggleKeyShortcutModal"
|
||||
@close-key-shortcut-modal="closeKeyShortcutModal"
|
||||
@show-add-label-popup="showAddLabelPopup"
|
||||
></sidebar>
|
||||
<section class="app-content columns" :class="contentClassName">
|
||||
<router-view></router-view>
|
||||
<command-bar />
|
||||
<account-selector
|
||||
:show-account-modal="showAccountModal"
|
||||
@close-account-modal="toggleAccountModal"
|
||||
@show-create-account-modal="openCreateAccountModal"
|
||||
/>
|
||||
<add-account-modal
|
||||
:show="showCreateAccountModal"
|
||||
@close-account-create-modal="closeCreateAccountModal"
|
||||
/>
|
||||
<woot-key-shortcut-modal
|
||||
v-if="showShortcutModal"
|
||||
@close="closeKeyShortcutModal"
|
||||
@clickaway="closeKeyShortcutModal"
|
||||
/>
|
||||
<woot-modal :show.sync="showAddLabelModal" :on-close="hideAddLabelPopup">
|
||||
<add-label-modal @close="hideAddLabelPopup" />
|
||||
</woot-modal>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -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;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -161,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;
|
||||
},
|
||||
|
|
|
@ -253,7 +253,7 @@ export default {
|
|||
this.$store.dispatch('contacts/get', requestParams);
|
||||
} else {
|
||||
this.$store.dispatch('contacts/search', {
|
||||
search: value,
|
||||
search: encodeURIComponent(value),
|
||||
...requestParams,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -126,17 +126,15 @@
|
|||
@close="toggleMergeModal"
|
||||
/>
|
||||
</div>
|
||||
<woot-confirm-delete-modal
|
||||
<woot-delete-modal
|
||||
v-if="showDeleteModal"
|
||||
:show.sync="showDeleteModal"
|
||||
:on-close="closeDelete"
|
||||
:on-confirm="confirmDeletion"
|
||||
:title="$t('DELETE_CONTACT.CONFIRM.TITLE')"
|
||||
:message="confirmDeleteMessage"
|
||||
:confirm-text="deleteConfirmText"
|
||||
:reject-text="deleteRejectText"
|
||||
:confirm-value="contact.name"
|
||||
:confirm-place-holder-text="confirmPlaceHolderText"
|
||||
@on-confirm="confirmDeletion"
|
||||
@on-close="closeDelete"
|
||||
:confirm-text="$t('DELETE_CONTACT.CONFIRM.YES')"
|
||||
:reject-text="$t('DELETE_CONTACT.CONFIRM.NO')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -221,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() {
|
||||
|
|
|
@ -34,19 +34,10 @@
|
|||
<td>{{ automation.name }}</td>
|
||||
<td>{{ automation.description }}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-button"
|
||||
:class="{ active: automation.active }"
|
||||
role="switch"
|
||||
:aria-checked="automation.active.toString()"
|
||||
<toggle-button
|
||||
:active="automation.active"
|
||||
@click="toggleAutomation(automation, automation.active)"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
:class="{ active: automation.active }"
|
||||
></span>
|
||||
</button>
|
||||
/>
|
||||
</td>
|
||||
<td>{{ readableTime(automation.created_on) }}</td>
|
||||
<td class="button-wrapper">
|
||||
|
@ -140,11 +131,13 @@ import AddAutomationRule from './AddAutomationRule.vue';
|
|||
import EditAutomationRule from './EditAutomationRule.vue';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
import ToggleButton from 'dashboard/components/buttons/ToggleButton';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddAutomationRule,
|
||||
EditAutomationRule,
|
||||
ToggleButton,
|
||||
},
|
||||
mixins: [alertMixin, timeMixin],
|
||||
data() {
|
||||
|
@ -290,41 +283,4 @@ export default {
|
|||
.automation__status-checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
.toggle-button {
|
||||
background-color: var(--s-200);
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
height: 19px;
|
||||
width: 34px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: var(--border-radius-large);
|
||||
cursor: pointer;
|
||||
transition-property: background-color;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 200ms;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-button.active {
|
||||
background-color: var(--w-500);
|
||||
}
|
||||
|
||||
.toggle-button span {
|
||||
--space-one-point-five: 1.5rem;
|
||||
height: var(--space-one-point-five);
|
||||
width: var(--space-one-point-five);
|
||||
display: inline-block;
|
||||
background-color: var(--white);
|
||||
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
|
||||
rgba(59, 130, 246, 0.5) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px,
|
||||
rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;
|
||||
transform: translate(0, 0);
|
||||
border-radius: 100%;
|
||||
transition-property: transform;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 200ms;
|
||||
}
|
||||
.toggle-button span.active {
|
||||
transform: translate(var(--space-one-point-five), var(--space-zero));
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -15,28 +15,57 @@
|
|||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="medium-9">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.PRE_CHAT_MESSAGE.LABEL') }}
|
||||
<textarea
|
||||
v-model.trim="preChatMessage"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.PRE_CHAT_FORM.PRE_CHAT_MESSAGE.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<div>
|
||||
<input
|
||||
v-model="preChatFieldOptions"
|
||||
type="checkbox"
|
||||
value="requireEmail"
|
||||
@input="handlePreChatFieldOptions"
|
||||
/>
|
||||
<label for="requireEmail">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.REQUIRE_EMAIL.LABEL') }}
|
||||
<div v-if="preChatFormEnabled">
|
||||
<label class="medium-9">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.PRE_CHAT_MESSAGE.LABEL') }}
|
||||
<textarea
|
||||
v-model.trim="preChatMessage"
|
||||
type="text"
|
||||
:placeholder="
|
||||
$t('INBOX_MGMT.PRE_CHAT_FORM.PRE_CHAT_MESSAGE.PLACEHOLDER')
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<label> {{ $t('INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS') }} </label>
|
||||
<table class="table table-striped">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col"></th>
|
||||
<th scope="col">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS_HEADER.FIELDS') }}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{{ $t('INBOX_MGMT.PRE_CHAT_FORM.SET_FIELDS_HEADER.REQUIRED') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<draggable v-model="preChatFields" tag="tbody">
|
||||
<tr v-for="(item, index) in preChatFields" :key="index">
|
||||
<th scope="col"><fluent-icon icon="drag" /></th>
|
||||
<td scope="row">
|
||||
<toggle-button
|
||||
:active="item['enabled']"
|
||||
@click="handlePreChatFieldOptions($event, 'enabled', item)"
|
||||
/>
|
||||
</td>
|
||||
<td :class="{ 'disabled-text': !item['enabled'] }">
|
||||
{{ item.label }}
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
v-model="item['required']"
|
||||
type="checkbox"
|
||||
:value="`${item.name}-required`"
|
||||
:disabled="!item['enabled']"
|
||||
@click="handlePreChatFieldOptions($event, 'required', item)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</draggable>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<woot-submit-button
|
||||
:button-text="$t('INBOX_MGMT.SETTINGS_POPUP.UPDATE')"
|
||||
:loading="uiFlags.isUpdatingInbox"
|
||||
|
@ -47,8 +76,13 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
import draggable from 'vuedraggable';
|
||||
import ToggleButton from 'dashboard/components/buttons/ToggleButton';
|
||||
export default {
|
||||
components: {
|
||||
draggable,
|
||||
ToggleButton,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
inbox: {
|
||||
|
@ -61,6 +95,7 @@ export default {
|
|||
preChatFormEnabled: false,
|
||||
preChatMessage: '',
|
||||
preChatFieldOptions: [],
|
||||
preChatFields: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -81,19 +116,19 @@ export default {
|
|||
pre_chat_form_options: preChatFormOptions,
|
||||
} = this.inbox;
|
||||
this.preChatFormEnabled = preChatFormEnabled;
|
||||
const { pre_chat_message: preChatMessage, require_email: requireEmail } =
|
||||
preChatFormOptions || {};
|
||||
const {
|
||||
pre_chat_message: preChatMessage,
|
||||
pre_chat_fields: preChatFields,
|
||||
} = preChatFormOptions || {};
|
||||
this.preChatMessage = preChatMessage;
|
||||
if (requireEmail) {
|
||||
this.preChatFieldOptions = ['requireEmail'];
|
||||
}
|
||||
this.preChatFields = preChatFields;
|
||||
},
|
||||
handlePreChatFieldOptions(event) {
|
||||
if (this.preChatFieldOptions.includes(event.target.value)) {
|
||||
this.preChatFieldOptions = [];
|
||||
} else {
|
||||
this.preChatFieldOptions = [event.target.value];
|
||||
}
|
||||
handlePreChatFieldOptions(event, type, item) {
|
||||
this.preChatFields.forEach((field, index) => {
|
||||
if (field.name === item.name) {
|
||||
this.preChatFields[index][type] = !item[type];
|
||||
}
|
||||
});
|
||||
},
|
||||
async updateInbox() {
|
||||
try {
|
||||
|
@ -104,7 +139,7 @@ export default {
|
|||
pre_chat_form_enabled: this.preChatFormEnabled,
|
||||
pre_chat_form_options: {
|
||||
pre_chat_message: this.preChatMessage,
|
||||
require_email: this.preChatFieldOptions.includes('requireEmail'),
|
||||
pre_chat_fields: this.preChatFields,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -125,4 +160,16 @@ export default {
|
|||
.prechat--title {
|
||||
margin: var(--space-medium) 0 var(--space-slab);
|
||||
}
|
||||
|
||||
.disabled-text {
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--s-500);
|
||||
}
|
||||
|
||||
table thead th {
|
||||
text-transform: none;
|
||||
}
|
||||
checkbox {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -93,21 +93,21 @@ export default {
|
|||
}
|
||||
return color;
|
||||
},
|
||||
addLabel() {
|
||||
this.$store
|
||||
.dispatch('labels/create', {
|
||||
async addLabel() {
|
||||
try {
|
||||
await this.$store.dispatch('labels/create', {
|
||||
color: this.color,
|
||||
description: this.description,
|
||||
title: this.title,
|
||||
show_on_sidebar: this.showOnSidebar,
|
||||
})
|
||||
.then(() => {
|
||||
this.showAlert(this.$t('LABEL_MGMT.ADD.API.SUCCESS_MESSAGE'));
|
||||
this.onClose();
|
||||
})
|
||||
.catch(() => {
|
||||
this.showAlert(this.$t('LABEL_MGMT.ADD.API.ERROR_MESSAGE'));
|
||||
});
|
||||
this.showAlert(this.$t('LABEL_MGMT.ADD.API.SUCCESS_MESSAGE'));
|
||||
this.onClose();
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error.message || this.$t('LABEL_MGMT.ADD.API.ERROR_MESSAGE');
|
||||
this.showAlert(errorMessage);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -24,7 +24,8 @@
|
|||
:heading="metric.NAME"
|
||||
:index="index"
|
||||
:on-click="changeSelection"
|
||||
:point="accountSummary[metric.KEY]"
|
||||
:point="displayMetric(metric.KEY)"
|
||||
:trend="calculateTrend(metric.KEY)"
|
||||
:selected="index === currentSelection"
|
||||
/>
|
||||
</div>
|
||||
|
@ -49,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',
|
||||
|
@ -63,6 +65,7 @@ export default {
|
|||
components: {
|
||||
ReportFilterSelector,
|
||||
},
|
||||
mixins: [reportMixin],
|
||||
data() {
|
||||
return {
|
||||
from: 0,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
</div>
|
||||
|
@ -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,
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import * as types from '../mutation-types';
|
|||
import Report from '../../api/reports';
|
||||
|
||||
import { downloadCsvFile } from '../../helper/downloadCsvFile';
|
||||
import { formatTime } from '@chatwoot/utils';
|
||||
|
||||
const state = {
|
||||
fetchingStatus: false,
|
||||
|
@ -21,6 +20,7 @@ const state = {
|
|||
incoming_messages_count: 0,
|
||||
outgoing_messages_count: 0,
|
||||
resolutions_count: 0,
|
||||
previous: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -125,18 +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 = formatTime(summaryData.avg_first_response_time);
|
||||
}
|
||||
// Average Resolution Time
|
||||
let avgResolutionTimeInHr = 0;
|
||||
if (summaryData.avg_resolution_time) {
|
||||
avgResolutionTimeInHr = formatTime(summaryData.avg_resolution_time);
|
||||
}
|
||||
_state.accountSummary.avg_first_response_time = avgFirstResTimeInHr;
|
||||
_state.accountSummary.avg_resolution_time = avgResolutionTimeInHr;
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -119,7 +119,8 @@ export const IFrameHelper = {
|
|||
},
|
||||
|
||||
setupAudioListeners: () => {
|
||||
getAlertAudio().then(() =>
|
||||
const { baseUrl = '' } = window.$chatwoot;
|
||||
getAlertAudio(baseUrl).then(() =>
|
||||
initOnEvents.forEach(event => {
|
||||
document.removeEventListener(
|
||||
event,
|
||||
|
|
|
@ -126,5 +126,6 @@
|
|||
"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"
|
||||
"subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z",
|
||||
"drag-outline": "M15 3.707V8.5a.5.5 0 0 0 1 0V3.707l1.146 1.147a.5.5 0 0 0 .708-.708l-2-2a.499.499 0 0 0-.708 0l-2 2a.5.5 0 0 0 .708.708L15 3.707ZM2 4.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5Zm0 5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5Zm.5 4.5a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6ZM15 16.293V11.5a.5.5 0 0 1 1 0v4.793l1.146-1.147a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.708 0l-2-2a.5.5 0 0 1 .708-.708L15 16.293Z"
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { IFrameHelper } from 'widget/helpers/utils';
|
|||
import { showBadgeOnFavicon } from './faviconHelper';
|
||||
|
||||
export const initOnEvents = ['click', 'touchstart', 'keypress'];
|
||||
export const getAlertAudio = async () => {
|
||||
export const getAlertAudio = async (baseUrl = '') => {
|
||||
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const playsound = audioBuffer => {
|
||||
window.playAudioAlert = () => {
|
||||
|
@ -17,7 +17,8 @@ export const getAlertAudio = async () => {
|
|||
};
|
||||
|
||||
try {
|
||||
const audioRequest = new Request('/dashboard/audios/ding.mp3');
|
||||
const resourceUrl = `${baseUrl}/dashboard/audios/ding.mp3`;
|
||||
const audioRequest = new Request(resourceUrl);
|
||||
|
||||
fetch(audioRequest)
|
||||
.then(response => response.arrayBuffer())
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -6,14 +6,14 @@ describe('#MessageFormatter', () => {
|
|||
const message =
|
||||
'Chatwoot is an opensource tool. [Chatwoot](https://www.chatwoot.com)';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||
'<p>Chatwoot is an opensource tool. <a rel="noreferrer noopener nofollow" href="https://www.chatwoot.com" class="link" title="" target="_blank">Chatwoot</a></p>'
|
||||
'<p>Chatwoot is an opensource tool. <a title="" class="link" href="https://www.chatwoot.com" rel="noreferrer noopener nofollow" target="_blank">Chatwoot</a></p>'
|
||||
);
|
||||
});
|
||||
it('should format correctly', () => {
|
||||
const message =
|
||||
'Chatwoot is an opensource tool. https://www.chatwoot.com';
|
||||
expect(new MessageFormatter(message).formattedMessage).toMatch(
|
||||
'<p>Chatwoot is an opensource tool. <a rel="noreferrer noopener nofollow" href="https://www.chatwoot.com" class="link" title="" target="_blank">https://www.chatwoot.com</a></p>'
|
||||
'<p>Chatwoot is an opensource tool. <a title="" class="link" href="https://www.chatwoot.com" rel="noreferrer noopener nofollow" target="_blank">https://www.chatwoot.com</a></p>'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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(
|
||||
'<p><a title="" class="link" rel="noreferrer noopener nofollow" target="_blank">xssLink</a><br><a title="" class="link" href="https://google.com" rel="noreferrer noopener nofollow" target="_blank">normalLink</a><strong>I am a bold text paragraph</strong></p>'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ const createConversation = params => {
|
|||
contact: {
|
||||
name: params.fullName,
|
||||
email: params.emailAddress,
|
||||
phone_number: params.phoneNumber,
|
||||
},
|
||||
message: {
|
||||
content: params.message,
|
||||
|
|
|
@ -9,30 +9,40 @@
|
|||
>
|
||||
{{ headerMessage }}
|
||||
</div>
|
||||
<form-input
|
||||
v-if="areContactFieldsVisible"
|
||||
v-model="fullName"
|
||||
class="mt-5"
|
||||
:label="$t('PRE_CHAT_FORM.FIELDS.FULL_NAME.LABEL')"
|
||||
:placeholder="$t('PRE_CHAT_FORM.FIELDS.FULL_NAME.PLACEHOLDER')"
|
||||
type="text"
|
||||
:error="
|
||||
$v.fullName.$error ? $t('PRE_CHAT_FORM.FIELDS.FULL_NAME.ERROR') : ''
|
||||
"
|
||||
/>
|
||||
<form-input
|
||||
v-if="areContactFieldsVisible"
|
||||
v-model="emailAddress"
|
||||
class="mt-5"
|
||||
:label="$t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.LABEL')"
|
||||
:placeholder="$t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.PLACEHOLDER')"
|
||||
type="email"
|
||||
:error="
|
||||
$v.emailAddress.$error
|
||||
? $t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.ERROR')
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
<div v-for="(item, index) in preChatFields" :key="index">
|
||||
<form-input
|
||||
v-if="isContactFieldVisible('fullName', item)"
|
||||
v-model="fullName"
|
||||
class="mt-5"
|
||||
:label="$t('PRE_CHAT_FORM.FIELDS.FULL_NAME.LABEL')"
|
||||
:placeholder="$t('PRE_CHAT_FORM.FIELDS.FULL_NAME.PLACEHOLDER')"
|
||||
type="text"
|
||||
:error="
|
||||
$v.fullName && $v.fullName.$error
|
||||
? $t('PRE_CHAT_FORM.FIELDS.FULL_NAME.REQUIRED_ERROR')
|
||||
: ''
|
||||
"
|
||||
/>
|
||||
<form-input
|
||||
v-if="isContactFieldVisible('emailAddress', item)"
|
||||
v-model="emailAddress"
|
||||
class="mt-5"
|
||||
:label="$t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.LABEL')"
|
||||
:placeholder="$t('PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.PLACEHOLDER')"
|
||||
type="email"
|
||||
:error="$v.emailAddress && emailErrorMessage"
|
||||
/>
|
||||
<form-input
|
||||
v-if="isContactFieldVisible('phoneNumber', item)"
|
||||
v-model="phoneNumber"
|
||||
class="mt-5"
|
||||
:label="$t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.LABEL')"
|
||||
:placeholder="$t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.PLACEHOLDER')"
|
||||
type="number"
|
||||
:error="$v.phoneNumber && phoneNumberErrorMessage"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form-text-area
|
||||
v-if="!hasActiveCampaign"
|
||||
v-model="message"
|
||||
|
@ -61,7 +71,9 @@ import FormTextArea from '../Form/TextArea';
|
|||
import Spinner from 'shared/components/Spinner';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getContrastingTextColor } from '@chatwoot/utils';
|
||||
import { isPhoneE164OrEmpty } from 'shared/helpers/Validators';
|
||||
import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
|
||||
import { isEmptyObject } from 'widget/helpers/utils';
|
||||
import routerMixin from 'widget/mixins/routerMixin';
|
||||
export default {
|
||||
|
@ -75,7 +87,7 @@ export default {
|
|||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
default: () => {},
|
||||
},
|
||||
disableContactFields: {
|
||||
type: Boolean,
|
||||
|
@ -83,15 +95,33 @@ export default {
|
|||
},
|
||||
},
|
||||
validations() {
|
||||
const identityValidations = {
|
||||
fullName: {
|
||||
required,
|
||||
},
|
||||
emailAddress: {
|
||||
required,
|
||||
email,
|
||||
},
|
||||
};
|
||||
let identityValidations = {};
|
||||
if (this.isContactFieldRequired('emailAddress')) {
|
||||
identityValidations = {
|
||||
...identityValidations,
|
||||
emailAddress: {
|
||||
required,
|
||||
email,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (this.isContactFieldRequired('phoneNumber')) {
|
||||
identityValidations = {
|
||||
...identityValidations,
|
||||
phoneNumber: {
|
||||
required,
|
||||
isPhoneE164OrEmpty,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (this.isContactFieldRequired('fullName')) {
|
||||
identityValidations = {
|
||||
...identityValidations,
|
||||
fullName: {
|
||||
required,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const messageValidation = {
|
||||
message: {
|
||||
|
@ -103,18 +133,16 @@ export default {
|
|||
if (this.hasActiveCampaign) {
|
||||
return identityValidations;
|
||||
}
|
||||
if (this.areContactFieldsVisible) {
|
||||
return {
|
||||
...identityValidations,
|
||||
...messageValidation,
|
||||
};
|
||||
}
|
||||
return messageValidation;
|
||||
return {
|
||||
...identityValidations,
|
||||
...messageValidation,
|
||||
};
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
fullName: '',
|
||||
emailAddress: '',
|
||||
phoneNumber: '',
|
||||
message: '',
|
||||
};
|
||||
},
|
||||
|
@ -139,11 +167,48 @@ export default {
|
|||
}
|
||||
return this.options.preChatMessage;
|
||||
},
|
||||
areContactFieldsVisible() {
|
||||
return this.options.requireEmail && !this.disableContactFields;
|
||||
preChatFields() {
|
||||
return this.options.preChatFields || [];
|
||||
},
|
||||
emailErrorMessage() {
|
||||
let errorMessage = '';
|
||||
if (!this.$v.emailAddress.$error) {
|
||||
errorMessage = '';
|
||||
} else if (!this.$v.emailAddress.required) {
|
||||
errorMessage = this.$t(
|
||||
'PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.REQUIRED_ERROR'
|
||||
);
|
||||
} else if (!this.$v.emailAddress.email) {
|
||||
errorMessage = this.$t(
|
||||
'PRE_CHAT_FORM.FIELDS.EMAIL_ADDRESS.VALID_ERROR'
|
||||
);
|
||||
}
|
||||
return errorMessage;
|
||||
},
|
||||
phoneNumberErrorMessage() {
|
||||
let errorMessage = '';
|
||||
if (!this.$v.phoneNumber.$error) {
|
||||
errorMessage = '';
|
||||
} else if (!this.$v.phoneNumber.required) {
|
||||
errorMessage = this.$t(
|
||||
'PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.REQUIRED_ERROR'
|
||||
);
|
||||
} else if (!this.$v.phoneNumber.email) {
|
||||
errorMessage = this.$t('PRE_CHAT_FORM.FIELDS.PHONE_NUMBER.VALID_ERROR');
|
||||
}
|
||||
return errorMessage;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isContactFieldVisible(field, item) {
|
||||
return (
|
||||
item.name === field &&
|
||||
this.preChatFields.find(option => option.name === field).enabled
|
||||
);
|
||||
},
|
||||
isContactFieldRequired(field) {
|
||||
return this.preChatFields.find(option => option.name === field).required;
|
||||
},
|
||||
onSubmit() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
|
@ -151,6 +216,7 @@ export default {
|
|||
}
|
||||
this.$emit('submit', {
|
||||
fullName: this.fullName,
|
||||
phoneNumber: this.phoneNumber,
|
||||
emailAddress: this.emailAddress,
|
||||
message: this.message,
|
||||
activeCampaignId: this.activeCampaign.id,
|
||||
|
|
|
@ -43,12 +43,19 @@
|
|||
"FULL_NAME": {
|
||||
"LABEL": "Full Name",
|
||||
"PLACEHOLDER": "Please enter your full name",
|
||||
"ERROR": "Full Name is required"
|
||||
"REQUIRED_ERROR": "Full Name is required"
|
||||
},
|
||||
"EMAIL_ADDRESS": {
|
||||
"LABEL": "Email Address",
|
||||
"PLACEHOLDER": "Please enter your email address",
|
||||
"ERROR": "Invalid email address"
|
||||
"REQUIRED_ERROR": "Email Address is required",
|
||||
"VALID_ERROR": "Please enter a valid email address"
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"LABEL": "Phone Number",
|
||||
"PLACEHOLDER": "Please enter your phone number",
|
||||
"REQUIRED_ERROR": "Phone Number is required",
|
||||
"VALID_ERROR": "Phone number should be of E.164 format eg: +1415555555"
|
||||
},
|
||||
"MESSAGE": {
|
||||
"LABEL": "Message",
|
||||
|
|
|
@ -24,12 +24,15 @@ export default {
|
|||
preChatFormOptions() {
|
||||
let requireEmail = false;
|
||||
let preChatMessage = '';
|
||||
let preChatFields = null;
|
||||
const options = window.chatwootWebChannel.preChatFormOptions || {};
|
||||
requireEmail = options.require_email;
|
||||
preChatMessage = options.pre_chat_message;
|
||||
preChatFields = options.pre_chat_fields;
|
||||
return {
|
||||
requireEmail,
|
||||
preChatMessage,
|
||||
preChatFields,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -35,13 +35,20 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
onSubmit({ fullName, emailAddress, message, activeCampaignId }) {
|
||||
onSubmit({
|
||||
fullName,
|
||||
emailAddress,
|
||||
message,
|
||||
activeCampaignId,
|
||||
phoneNumber,
|
||||
}) {
|
||||
if (activeCampaignId) {
|
||||
bus.$emit('execute-campaign', activeCampaignId);
|
||||
this.$store.dispatch('contacts/update', {
|
||||
user: {
|
||||
email: emailAddress,
|
||||
name: fullName,
|
||||
phone_number: phoneNumber,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
|
@ -49,6 +56,7 @@ export default {
|
|||
fullName: fullName,
|
||||
emailAddress: emailAddress,
|
||||
message: message,
|
||||
phoneNumber: phoneNumber,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -34,7 +34,7 @@ class Attachment < ApplicationRecord
|
|||
has_one_attached :file
|
||||
validate :acceptable_file
|
||||
|
||||
enum file_type: [:image, :audio, :video, :file, :location, :fallback]
|
||||
enum file_type: [:image, :audio, :video, :file, :location, :fallback, :share, :story_mention]
|
||||
|
||||
def push_event_data
|
||||
return unless file_type
|
||||
|
|
|
@ -32,7 +32,8 @@ class Channel::WebWidget < ApplicationRecord
|
|||
self.table_name = 'channel_web_widgets'
|
||||
EDITABLE_ATTRS = [:website_url, :widget_color, :welcome_title, :welcome_tagline, :reply_time, :pre_chat_form_enabled,
|
||||
:continuity_via_email, :hmac_mandatory,
|
||||
{ pre_chat_form_options: [:pre_chat_message, :require_email] },
|
||||
{ pre_chat_form_options: [:pre_chat_message, :require_email,
|
||||
{ pre_chat_fields: [:label, :name, :enabled, :type, :enabled, :required] }] },
|
||||
{ selected_feature_flags: [] }].freeze
|
||||
|
||||
validates :website_url, presence: true
|
||||
|
|
|
@ -64,7 +64,7 @@ class Message < ApplicationRecord
|
|||
# [:deleted] : Used to denote whether the message was deleted by the agent
|
||||
# [:external_created_at] : Can specify if the message was created at a different timestamp externally
|
||||
store :content_attributes, accessors: [:submitted_email, :items, :submitted_values, :email, :in_reply_to, :deleted,
|
||||
:external_created_at], coder: JSON
|
||||
:external_created_at, :story_sender, :story_id], coder: JSON
|
||||
|
||||
store :external_source_ids, accessors: [:slack], coder: JSON, prefix: :external_source_id
|
||||
|
||||
|
|
|
@ -82,6 +82,7 @@ Rails.application.configure do
|
|||
allow do
|
||||
origins '*'
|
||||
resource '/packs/*', headers: :any, methods: [:get, :options]
|
||||
resource '/dashboard/audios/ding.mp3', headers: :any, methods: [:get, :options]
|
||||
resource '*', headers: :any, methods: :any, expose: ['access-token', 'client', 'uid', 'expiry']
|
||||
end
|
||||
end
|
||||
|
|
|
@ -121,6 +121,7 @@ Rails.application.configure do
|
|||
allow do
|
||||
origins '*'
|
||||
resource '/packs/*', headers: :any, methods: [:get, :options]
|
||||
resource '/dashboard/audios/ding.mp3', headers: :any, methods: [:get, :options]
|
||||
if ActiveModel::Type::Boolean.new.cast(ENV.fetch('CW_API_ONLY_SERVER', false))
|
||||
resource '*', headers: :any, methods: :any, expose: ['access-token', 'client', 'uid', 'expiry']
|
||||
end
|
||||
|
|
|
@ -87,6 +87,7 @@ Rails.application.configure do
|
|||
allow do
|
||||
origins '*'
|
||||
resource '/packs/*', headers: :any, methods: [:get, :options]
|
||||
resource '/dashboard/audios/ding.mp3', headers: :any, methods: [:get, :options]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -58,6 +58,7 @@ Rails.application.configure do
|
|||
allow do
|
||||
origins '*'
|
||||
resource '/packs/*', headers: :any, methods: [:get, :options]
|
||||
resource '/dashboard/audios/ding.mp3', headers: :any, methods: [:get, :options]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -43,7 +43,7 @@ en:
|
|||
failed: Signup failed
|
||||
contacts:
|
||||
import:
|
||||
failed: File is blank
|
||||
failed: File is blank
|
||||
|
||||
reports:
|
||||
period: Reporting period %{since} to %{until}
|
||||
|
@ -67,6 +67,7 @@ en:
|
|||
conversation_mention: "You have been mentioned in conversation [ID - %{display_id}] by %{name}"
|
||||
conversations:
|
||||
messages:
|
||||
instagram_story_content: "%{story_sender} mentioned you in the story: "
|
||||
deleted: This message was deleted
|
||||
activity:
|
||||
status:
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
class AddCustomFieldsToPreChatForm < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
Channel::WebWidget.find_in_batches do |channels_batch|
|
||||
channels_batch.each do |channel|
|
||||
pre_chat_message = channel[:pre_chat_form_options]['pre_chat_message'] || 'Share your queries or comments here.'
|
||||
pre_chat_fields = pre_chat_fields?(channel)
|
||||
channel[:pre_chat_form_options] = {
|
||||
'pre_chat_message': pre_chat_message,
|
||||
'pre_chat_fields': pre_chat_fields
|
||||
}
|
||||
channel.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def pre_chat_fields?(channel)
|
||||
email_enabled = channel[:pre_chat_form_options]['require_email'] || false
|
||||
[
|
||||
{
|
||||
'label': 'Email Id', 'name': 'emailAddress', 'type': 'email', 'required': true, 'enabled': email_enabled
|
||||
}, {
|
||||
'label': 'Full name', 'name': 'fullName', 'type': 'text', 'required': true, 'enabled': email_enabled
|
||||
}, {
|
||||
'label': 'Phone number', 'name': 'phoneNumber', 'type': 'number', 'required': true, 'enabled': false
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2022_03_08_193420) do
|
||||
ActiveRecord::Schema.define(version: 2022_03_09_103742) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_stat_statements"
|
||||
|
|
|
@ -55,4 +55,72 @@ FactoryBot.define do
|
|||
end
|
||||
initialize_with { attributes }
|
||||
end
|
||||
|
||||
factory :instagram_message_attachment_event, class: Hash do
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'id': 'instagram-message-id-1234',
|
||||
'time': '2021-09-08T06:34:04+0000',
|
||||
'messaging': [
|
||||
{
|
||||
'sender': {
|
||||
'id': 'Sender-id-1'
|
||||
},
|
||||
'recipient': {
|
||||
'id': 'chatwoot-app-user-id-1'
|
||||
},
|
||||
'timestamp': '2021-09-08T06:34:04+0000',
|
||||
'message': {
|
||||
'mid': 'message-id-1',
|
||||
'attachments': [
|
||||
{
|
||||
'type': 'share',
|
||||
'payload': {
|
||||
'url': 'https://imagekit.io/blog/content/images/2020/05/media_library.jpeg'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
end
|
||||
initialize_with { attributes }
|
||||
end
|
||||
|
||||
factory :instagram_story_mention_event, class: Hash do
|
||||
entry do
|
||||
[
|
||||
{
|
||||
'id': 'instagram-message-id-1234',
|
||||
'time': '2021-09-08T06:34:04+0000',
|
||||
'messaging': [
|
||||
{
|
||||
'sender': {
|
||||
'id': 'Sender-id-1'
|
||||
},
|
||||
'recipient': {
|
||||
'id': 'chatwoot-app-user-id-1'
|
||||
},
|
||||
'timestamp': '2021-09-08T06:34:04+0000',
|
||||
'message': {
|
||||
'mid': 'message-id-1',
|
||||
'attachments': [
|
||||
{
|
||||
'type': 'story_mention',
|
||||
'payload': {
|
||||
'url': 'https://imagekit.io/blog/content/images/2020/05/media_library.jpeg'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
end
|
||||
initialize_with { attributes }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,13 +6,30 @@ describe Webhooks::InstagramEventsJob do
|
|||
|
||||
before do
|
||||
stub_request(:post, /graph.facebook.com/)
|
||||
stub_request(:get, 'https://imagekit.io/blog/content/images/2020/05/media_library.jpeg')
|
||||
.with(
|
||||
headers: {
|
||||
'Accept' => '*/*',
|
||||
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
|
||||
'User-Agent' => 'Down/5.3.0'
|
||||
}
|
||||
)
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
end
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let(:return_onject) do
|
||||
{ name: 'Jane',
|
||||
id: 'Sender-id-1',
|
||||
account_id: instagram_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png' }
|
||||
end
|
||||
let!(:instagram_channel) { create(:channel_instagram_fb_page, account: account, instagram_id: 'chatwoot-app-user-id-1') }
|
||||
let!(:instagram_inbox) { create(:inbox, channel: instagram_channel, account: account, greeting_enabled: false) }
|
||||
let!(:dm_params) { build(:instagram_message_create_event).with_indifferent_access }
|
||||
let!(:test_params) { build(:instagram_test_text_event).with_indifferent_access }
|
||||
let!(:attachment_params) { build(:instagram_message_attachment_event).with_indifferent_access }
|
||||
let!(:story_mention_params) { build(:instagram_story_mention_event).with_indifferent_access }
|
||||
let(:fb_object) { double }
|
||||
|
||||
describe '#perform' do
|
||||
|
@ -20,12 +37,7 @@ describe Webhooks::InstagramEventsJob do
|
|||
it 'creates incoming message in the instagram inbox' do
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
{
|
||||
name: 'Jane',
|
||||
id: 'Sender-id-1',
|
||||
account_id: instagram_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png'
|
||||
}.with_indifferent_access
|
||||
return_onject.with_indifferent_access
|
||||
)
|
||||
instagram_webhook.perform_now(dm_params[:entry])
|
||||
|
||||
|
@ -39,12 +51,7 @@ describe Webhooks::InstagramEventsJob do
|
|||
it 'creates test text message in the instagram inbox' do
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
{
|
||||
name: 'Jane',
|
||||
id: 'Sender-id-1',
|
||||
account_id: instagram_inbox.account_id,
|
||||
profile_pic: 'https://chatwoot-assets.local/sample.png'
|
||||
}.with_indifferent_access
|
||||
return_onject.with_indifferent_access
|
||||
)
|
||||
instagram_webhook.perform_now(test_params[:entry])
|
||||
|
||||
|
@ -53,6 +60,46 @@ describe Webhooks::InstagramEventsJob do
|
|||
expect(instagram_inbox.messages.count).to be 1
|
||||
expect(instagram_inbox.messages.last.content).to eq('This is a test message from facebook.')
|
||||
end
|
||||
|
||||
it 'creates incoming message with attachments in the instagram inbox' do
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_onject.with_indifferent_access
|
||||
)
|
||||
instagram_webhook.perform_now(attachment_params[:entry])
|
||||
|
||||
instagram_inbox.reload
|
||||
|
||||
expect(instagram_inbox.contacts.count).to be 1
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
expect(instagram_inbox.messages.last.attachments.count).to be 1
|
||||
end
|
||||
|
||||
it 'creates incoming message with attachments in the instagram inbox for story mention' do
|
||||
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
|
||||
allow(fb_object).to receive(:get_object).and_return(
|
||||
return_onject.with_indifferent_access,
|
||||
{ story:
|
||||
{
|
||||
mention: {
|
||||
link:
|
||||
'https://lookaside.fbsbx.com/ig_messaging_cdn/?asset_id=17920786367196703&signature=Aby8EXbvNu4on9efDQecXDasiJX2s0FgWhFGz3mNFB__CsHR22O_1bJiYHkbp3mC1NQeW4jHxls9WyqVgRPcyonUbSJmD44UwLfFhbCK2obesWnFi7VOnisqLu48Xd6KYuNex7uSCQKWM-nw55zQ23bBgfCYw6h5hiJjFHwJDZYm65zXpQ',
|
||||
id: '17920786367196703'
|
||||
}
|
||||
},
|
||||
from: {
|
||||
username: 'Sender-id-1', id: 'Sender-id-1'
|
||||
},
|
||||
id: 'instagram-message-id-1234' }.with_indifferent_access
|
||||
)
|
||||
|
||||
instagram_webhook.perform_now(story_mention_params[:entry])
|
||||
|
||||
instagram_inbox.reload
|
||||
|
||||
expect(instagram_inbox.messages.count).to be 1
|
||||
expect(instagram_inbox.messages.last.attachments.count).to be 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,29 @@
|
|||
type: object
|
||||
properties:
|
||||
value:
|
||||
type: number
|
||||
timestamp:
|
||||
avg_first_response_time:
|
||||
type: string
|
||||
avg_resolution_time:
|
||||
type: string
|
||||
conversations_count:
|
||||
type: number
|
||||
incoming_messages_count:
|
||||
type: number
|
||||
outgoing_messages_count:
|
||||
type: number
|
||||
resolutions_count:
|
||||
type: number
|
||||
previous:
|
||||
type: object
|
||||
properties:
|
||||
avg_first_response_time:
|
||||
type: string
|
||||
avg_resolution_time:
|
||||
type: string
|
||||
conversations_count:
|
||||
type: number
|
||||
incoming_messages_count:
|
||||
type: number
|
||||
outgoing_messages_count:
|
||||
type: number
|
||||
resolutions_count:
|
||||
type: number
|
|
@ -4692,11 +4692,46 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"value": {
|
||||
"avg_first_response_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"timestamp": {
|
||||
"type": "string"
|
||||
"incoming_messages_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"resolutions_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"previous": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"avg_first_response_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"avg_resolution_time": {
|
||||
"type": "string"
|
||||
},
|
||||
"conversations_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"incoming_messages_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"outgoing_messages_count": {
|
||||
"type": "number"
|
||||
},
|
||||
"resolutions_count": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue