Merge branch 'develop' into feat/reload-banner-chat-list
This commit is contained in:
commit
b9b00f7e9f
48 changed files with 572 additions and 178 deletions
|
@ -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
|
||||
|
|
|
@ -7,6 +7,9 @@ class V2::ReportBuilder
|
|||
def initialize(account, params)
|
||||
@account = account
|
||||
@params = params
|
||||
|
||||
timezone_offset = (params[:timezone_offset] || 0).to_f
|
||||
@timezone = ActiveSupport::TimeZone[timezone_offset]&.name
|
||||
end
|
||||
|
||||
def timeseries
|
||||
|
@ -64,60 +67,58 @@ class V2::ReportBuilder
|
|||
@team ||= account.teams.find(params[:id])
|
||||
end
|
||||
|
||||
def get_grouped_values(object_scope)
|
||||
object_scope.group_by_period(
|
||||
params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at,
|
||||
default_value: 0,
|
||||
range: range,
|
||||
permit: %w[day week month year],
|
||||
time_zone: @timezone
|
||||
)
|
||||
end
|
||||
|
||||
def conversations_count
|
||||
scope.conversations
|
||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
||||
.count
|
||||
(get_grouped_values scope.conversations).count
|
||||
end
|
||||
|
||||
def incoming_messages_count
|
||||
scope.messages.incoming.unscope(:order)
|
||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
||||
.count
|
||||
(get_grouped_values scope.messages.incoming.unscope(:order)).count
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
scope.messages.outgoing.unscope(:order)
|
||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
||||
.count
|
||||
(get_grouped_values scope.messages.outgoing.unscope(:order)).count
|
||||
end
|
||||
|
||||
def resolutions_count
|
||||
scope.conversations
|
||||
.resolved
|
||||
.group_by_period(params[:group_by] || DEFAULT_GROUP_BY,
|
||||
:created_at, range: range, default_value: 0, permit: %w[day week month year])
|
||||
.count
|
||||
(get_grouped_values scope.conversations.resolved).count
|
||||
end
|
||||
|
||||
def avg_first_response_time
|
||||
scope.events
|
||||
.where(name: 'first_response')
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.average(:value)
|
||||
(get_grouped_values scope.events.where(name: 'first_response')).average(:value)
|
||||
end
|
||||
|
||||
def avg_resolution_time
|
||||
scope.events.where(name: 'conversation_resolved')
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.average(:value)
|
||||
(get_grouped_values scope.events.where(name: 'conversation_resolved')).average(:value)
|
||||
end
|
||||
|
||||
# Taking average of average is not too accurate
|
||||
# https://en.wikipedia.org/wiki/Simpson's_paradox
|
||||
# TODO: Will optimize this later
|
||||
def avg_resolution_time_summary
|
||||
return 0 if avg_resolution_time.values.empty?
|
||||
avg_rt = scope.events
|
||||
.where(name: 'conversation_resolved', created_at: range)
|
||||
.average(:value)
|
||||
|
||||
(avg_resolution_time.values.sum / avg_resolution_time.values.length)
|
||||
return 0 if avg_rt.blank?
|
||||
|
||||
avg_rt
|
||||
end
|
||||
|
||||
def avg_first_response_time_summary
|
||||
return 0 if avg_first_response_time.values.empty?
|
||||
avg_frt = scope.events
|
||||
.where(name: 'first_response', created_at: range)
|
||||
.average(:value)
|
||||
|
||||
(avg_first_response_time.values.sum / avg_first_response_time.values.length)
|
||||
return 0 if avg_frt.blank?
|
||||
|
||||
avg_frt
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -58,7 +58,8 @@ class Api::V2::Accounts::ReportsController < Api::V1::Accounts::BaseController
|
|||
since: params[:since],
|
||||
until: params[:until],
|
||||
id: params[:id],
|
||||
group_by: params[:group_by]
|
||||
group_by: params[:group_by],
|
||||
timezone_offset: params[:timezone_offset]
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -27,7 +27,9 @@ class DashboardController < ActionController::Base
|
|||
'ANALYTICS_TOKEN',
|
||||
'ANALYTICS_HOST',
|
||||
'DIRECT_UPLOADS_ENABLED',
|
||||
'HCAPTCHA_SITE_KEY'
|
||||
'HCAPTCHA_SITE_KEY',
|
||||
'LOGOUT_REDIRECT_LINK',
|
||||
'DISABLE_USER_PROFILE_UPDATE'
|
||||
).merge(app_config)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
class WidgetTestsController < ActionController::Base
|
||||
before_action :set_web_widget
|
||||
before_action :ensure_web_widget
|
||||
before_action :ensure_widget_position
|
||||
before_action :ensure_widget_type
|
||||
before_action :ensure_widget_style
|
||||
|
||||
def index
|
||||
render
|
||||
|
@ -7,7 +10,24 @@ class WidgetTestsController < ActionController::Base
|
|||
|
||||
private
|
||||
|
||||
def set_web_widget
|
||||
@web_widget = Channel::WebWidget.first
|
||||
def ensure_widget_style
|
||||
@widget_style = params[:widget_style] || 'standard'
|
||||
end
|
||||
|
||||
def ensure_widget_position
|
||||
@widget_position = params[:position] || 'left'
|
||||
end
|
||||
|
||||
def ensure_widget_type
|
||||
@widget_type = params[:type] || 'expanded_bubble'
|
||||
end
|
||||
|
||||
def inbox_id
|
||||
@inbox_id ||= params[:inbox_id] || Channel::WebWidget.first.inbox.id
|
||||
end
|
||||
|
||||
def ensure_web_widget
|
||||
@inbox = Inbox.find(inbox_id)
|
||||
@web_widget = @inbox.channel
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
const getTimeOffset = () => -new Date().getTimezoneOffset() / 60;
|
||||
|
||||
class ReportsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('reports', { accountScoped: true, apiVersion: 'v2' });
|
||||
|
@ -8,13 +10,27 @@ class ReportsAPI extends ApiClient {
|
|||
|
||||
getReports(metric, since, until, type = 'account', id, group_by) {
|
||||
return axios.get(`${this.url}`, {
|
||||
params: { metric, since, until, type, id, group_by },
|
||||
params: {
|
||||
metric,
|
||||
since,
|
||||
until,
|
||||
type,
|
||||
id,
|
||||
group_by,
|
||||
timezone_offset: getTimeOffset(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
getSummary(since, until, type = 'account', id, group_by) {
|
||||
return axios.get(`${this.url}/summary`, {
|
||||
params: { since, until, type, id, group_by },
|
||||
params: {
|
||||
since,
|
||||
until,
|
||||
type,
|
||||
id,
|
||||
group_by,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ describe('#Reports API', () => {
|
|||
since: 1621103400,
|
||||
until: 1621621800,
|
||||
type: 'account',
|
||||
timezone_offset: -0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -54,3 +54,9 @@
|
|||
.text-y-800 {
|
||||
color: var(--y-800);
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<div v-if="showShowCurrentAccountContext" class="account-context--group">
|
||||
{{ $t('SIDEBAR.CURRENTLY_VIEWING_ACCOUNT') }}
|
||||
<p class="account-context--name text-ellipsis">
|
||||
{{ account.name }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
account: 'getCurrentAccount',
|
||||
userAccounts: 'getUserAccounts',
|
||||
}),
|
||||
showShowCurrentAccountContext() {
|
||||
return this.userAccounts.length > 1 && this.account.name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.account-context--group {
|
||||
border-radius: var(--border-radius-normal);
|
||||
border: 1px solid var(--color-border);
|
||||
font-size: var(--font-size-mini);
|
||||
padding: var(--space-small);
|
||||
margin-bottom: var(--space-small);
|
||||
|
||||
.account-context--name {
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<div v-if="hasSecondaryMenu" class="main-nav secondary-menu">
|
||||
<account-context />
|
||||
<transition-group name="menu-list" tag="ul" class="menu vertical">
|
||||
<secondary-nav-item
|
||||
v-for="menuItem in accessibleMenuItems"
|
||||
|
@ -18,9 +19,11 @@
|
|||
<script>
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import SecondaryNavItem from './SecondaryNavItem.vue';
|
||||
import AccountContext from './AccountContext.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AccountContext,
|
||||
SecondaryNavItem,
|
||||
},
|
||||
props: {
|
||||
|
|
|
@ -110,7 +110,7 @@ import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
|
|||
|
||||
import { REPLY_EDITOR_MODES } from './constants';
|
||||
export default {
|
||||
name: 'ReplyTopPanel',
|
||||
name: 'ReplyBottomPanel',
|
||||
components: { FileUpload },
|
||||
mixins: [eventListenerMixins, uiSettingsMixin, inboxMixin],
|
||||
props: {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<li v-if="hasAttachments || data.content" :class="alignBubble">
|
||||
<li
|
||||
v-if="hasAttachments || data.content || isEmailContentType"
|
||||
:class="alignBubble"
|
||||
>
|
||||
<div :class="wrapClass">
|
||||
<div v-tooltip.top-start="messageToolTip" :class="bubbleClass">
|
||||
<bubble-mail-head
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
</div>
|
||||
<div class="features-item">
|
||||
<h2 class="block-title">
|
||||
<span class="emoji">🏷</span>{{ $t('ONBOARDING.LABELS.TITLE') }}
|
||||
<span class="emoji">🔖</span>{{ $t('ONBOARDING.LABELS.TITLE') }}
|
||||
</h2>
|
||||
<p class="intro-body">
|
||||
{{ $t('ONBOARDING.LABELS.DESCRIPTION') }}
|
||||
|
|
|
@ -68,11 +68,21 @@
|
|||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="showMessageSignature"
|
||||
v-if="isSignatureEnabledForInbox"
|
||||
v-tooltip="$t('CONVERSATION.FOOTER.MESSAGE_SIGN_TOOLTIP')"
|
||||
class="message-signature-wrap"
|
||||
>
|
||||
<p class="message-signature" v-html="formatMessage(messageSignature)" />
|
||||
<p
|
||||
v-if="isSignatureAvailable"
|
||||
class="message-signature"
|
||||
v-html="formatMessage(messageSignature)"
|
||||
/>
|
||||
<p v-else class="message-signature">
|
||||
{{ $t('CONVERSATION.FOOTER.MESSAGE_SIGNATURE_NOT_CONFIGURED') }}
|
||||
<router-link :to="profilePath">
|
||||
{{ $t('CONVERSATION.FOOTER.CLICK_HERE') }}
|
||||
</router-link>
|
||||
</p>
|
||||
</div>
|
||||
<reply-bottom-panel
|
||||
:mode="replyType"
|
||||
|
@ -125,6 +135,7 @@ import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
|||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
import { DirectUpload } from 'activestorage';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -181,6 +192,7 @@ export default {
|
|||
messageSignature: 'getMessageSignature',
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
|
||||
showRichContentEditor() {
|
||||
|
@ -351,13 +363,19 @@ export default {
|
|||
enableMultipleFileUpload() {
|
||||
return this.isAnEmailChannel || this.isAWebWidgetInbox || this.isAPIInbox;
|
||||
},
|
||||
showMessageSignature() {
|
||||
isSignatureEnabledForInbox() {
|
||||
return !this.isPrivate && this.isAnEmailChannel && this.sendWithSignature;
|
||||
},
|
||||
isSignatureAvailable() {
|
||||
return !!this.messageSignature;
|
||||
},
|
||||
sendWithSignature() {
|
||||
const { send_with_signature: isEnabled } = this.uiSettings;
|
||||
return isEnabled;
|
||||
},
|
||||
profilePath() {
|
||||
return frontendURL(`accounts/${this.accountId}/profile/settings`);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentChat(conversation) {
|
||||
|
@ -470,7 +488,7 @@ export default {
|
|||
}
|
||||
if (!this.showMentions) {
|
||||
let newMessage = this.message;
|
||||
if (this.sendWithSignature && this.messageSignature) {
|
||||
if (this.isSignatureEnabledForInbox && this.messageSignature) {
|
||||
newMessage += '\n\n' + this.messageSignature;
|
||||
}
|
||||
const messagePayload = this.getMessagePayload(newMessage);
|
||||
|
@ -714,4 +732,12 @@ export default {
|
|||
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
|
||||
}
|
||||
}
|
||||
|
||||
.message-signature {
|
||||
margin-bottom: 0;
|
||||
|
||||
::v-deep p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -61,7 +61,9 @@
|
|||
"ENABLE_SIGN_TOOLTIP": "Enable signature",
|
||||
"DISABLE_SIGN_TOOLTIP": "Disable signature",
|
||||
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
|
||||
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents"
|
||||
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents",
|
||||
"MESSAGE_SIGNATURE_NOT_CONFIGURED": "Message signature is not configured, please configure it in profile settings.",
|
||||
"CLICK_HERE": "Click here to update"
|
||||
},
|
||||
"REPLYBOX": {
|
||||
"REPLY": "Reply",
|
||||
|
|
|
@ -142,6 +142,7 @@
|
|||
}
|
||||
},
|
||||
"SIDEBAR": {
|
||||
"CURRENTLY_VIEWING_ACCOUNT": "Currently viewing:",
|
||||
"CONVERSATIONS": "Conversations",
|
||||
"ALL_CONVERSATIONS": "All Conversations",
|
||||
"MENTIONED_CONVERSATIONS": "Mentions",
|
||||
|
|
|
@ -48,7 +48,10 @@
|
|||
@input="$v.displayName.$touch"
|
||||
/>
|
||||
</label>
|
||||
<label :class="{ error: $v.email.$error }">
|
||||
<label
|
||||
v-if="!globalConfig.disableUserProfileUpdate"
|
||||
:class="{ error: $v.email.$error }"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.FORM.EMAIL.LABEL') }}
|
||||
<input
|
||||
v-model.trim="email"
|
||||
|
@ -67,7 +70,7 @@
|
|||
</div>
|
||||
</form>
|
||||
<message-signature />
|
||||
<change-password />
|
||||
<change-password v-if="!globalConfig.disableUserProfileUpdate" />
|
||||
<notification-settings />
|
||||
<div class="profile--settings--row row">
|
||||
<div class="columns small-3">
|
||||
|
|
|
@ -50,6 +50,7 @@ import subDays from 'date-fns/subDays';
|
|||
import startOfDay from 'date-fns/startOfDay';
|
||||
import getUnixTime from 'date-fns/getUnixTime';
|
||||
import { GROUP_BY_FILTER } from '../constants';
|
||||
import endOfDay from 'date-fns/endOfDay';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -79,9 +80,9 @@ export default {
|
|||
},
|
||||
to() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return this.fromCustomDate(this.customDateRange[1]);
|
||||
return this.toCustomDate(this.customDateRange[1]);
|
||||
}
|
||||
return this.fromCustomDate(new Date());
|
||||
return this.toCustomDate(new Date());
|
||||
},
|
||||
from() {
|
||||
if (this.isDateRangeSelected) {
|
||||
|
@ -134,6 +135,9 @@ export default {
|
|||
fromCustomDate(date) {
|
||||
return getUnixTime(startOfDay(date));
|
||||
},
|
||||
toCustomDate(date) {
|
||||
return getUnixTime(endOfDay(date));
|
||||
},
|
||||
changeDateSelection(selectedRange) {
|
||||
this.currentDateRangeSelection = selectedRange;
|
||||
this.onDateRangeChange();
|
||||
|
|
|
@ -148,13 +148,15 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
||||
const CUSTOM_DATE_RANGE_ID = 5;
|
||||
import subDays from 'date-fns/subDays';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import endOfDay from 'date-fns/endOfDay';
|
||||
import getUnixTime from 'date-fns/getUnixTime';
|
||||
import startOfDay from 'date-fns/startOfDay';
|
||||
import subDays from 'date-fns/subDays';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import WootDateRangePicker from 'dashboard/components/ui/DateRangePicker.vue';
|
||||
|
||||
import { GROUP_BY_FILTER } from '../constants';
|
||||
const CUSTOM_DATE_RANGE_ID = 5;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -194,9 +196,9 @@ export default {
|
|||
},
|
||||
to() {
|
||||
if (this.isDateRangeSelected) {
|
||||
return this.fromCustomDate(this.customDateRange[1]);
|
||||
return this.toCustomDate(this.customDateRange[1]);
|
||||
}
|
||||
return this.fromCustomDate(new Date());
|
||||
return this.toCustomDate(new Date());
|
||||
},
|
||||
from() {
|
||||
if (this.isDateRangeSelected) {
|
||||
|
@ -253,6 +255,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
onDateRangeChange() {
|
||||
console.log(this.from, this.to);
|
||||
this.$emit('date-range-change', {
|
||||
from: this.from,
|
||||
to: this.to,
|
||||
|
@ -262,6 +265,9 @@ export default {
|
|||
fromCustomDate(date) {
|
||||
return getUnixTime(startOfDay(date));
|
||||
},
|
||||
toCustomDate(date) {
|
||||
return getUnixTime(endOfDay(date));
|
||||
},
|
||||
changeDateSelection(selectedRange) {
|
||||
this.currentDateRangeSelection = selectedRange;
|
||||
this.onDateRangeChange();
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
</div>
|
||||
</form>
|
||||
<div class="column text-center sigin__footer">
|
||||
<p>
|
||||
<p v-if="!globalConfig.disableUserProfileUpdate">
|
||||
<router-link to="auth/reset/password">
|
||||
{{ $t('LOGIN.FORGOT_PASSWORD') }}
|
||||
</router-link>
|
||||
|
|
|
@ -71,6 +71,19 @@ export const getters = {
|
|||
|
||||
return messageSignature || '';
|
||||
},
|
||||
|
||||
getCurrentAccount(_state) {
|
||||
const { accounts = [] } = _state.currentUser;
|
||||
const [currentAccount = {}] = accounts.filter(
|
||||
account => account.id === _state.currentAccountId
|
||||
);
|
||||
return currentAccount || {};
|
||||
},
|
||||
|
||||
getUserAccounts(_state) {
|
||||
const { accounts = [] } = _state.currentUser;
|
||||
return accounts;
|
||||
},
|
||||
};
|
||||
|
||||
// actions
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
/* eslint no-console: 0 */
|
||||
/* eslint no-param-reassign: 0 */
|
||||
/* eslint no-shadow: 0 */
|
||||
import compareAsc from 'date-fns/compareAsc';
|
||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||
|
||||
import * as types from '../mutation-types';
|
||||
import Report from '../../api/reports';
|
||||
|
||||
|
@ -48,7 +45,8 @@ export const actions = {
|
|||
).then(accountReport => {
|
||||
let { data } = accountReport;
|
||||
data = data.filter(
|
||||
el => compareAsc(new Date(), fromUnixTime(el.timestamp)) > -1
|
||||
el =>
|
||||
reportObj.to - el.timestamp > 0 && el.timestamp - reportObj.from >= 0
|
||||
);
|
||||
if (
|
||||
reportObj.metric === 'avg_first_response_time' ||
|
||||
|
|
|
@ -53,4 +53,60 @@ describe('#getters', () => {
|
|||
).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getCurrentAccount', () => {
|
||||
it('returns correct values', () => {
|
||||
expect(
|
||||
getters.getCurrentAccount({
|
||||
currentUser: {},
|
||||
currentAccountId: 1,
|
||||
})
|
||||
).toEqual({});
|
||||
|
||||
expect(
|
||||
getters.getCurrentAccount({
|
||||
currentUser: {
|
||||
accounts: [
|
||||
{
|
||||
name: 'Chatwoot',
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
currentAccountId: 1,
|
||||
})
|
||||
).toEqual({
|
||||
name: 'Chatwoot',
|
||||
id: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUserAccounts', () => {
|
||||
it('returns correct values', () => {
|
||||
expect(
|
||||
getters.getUserAccounts({
|
||||
currentUser: {},
|
||||
})
|
||||
).toEqual([]);
|
||||
|
||||
expect(
|
||||
getters.getUserAccounts({
|
||||
currentUser: {
|
||||
accounts: [
|
||||
{
|
||||
name: 'Chatwoot',
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
name: 'Chatwoot',
|
||||
id: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -44,5 +44,9 @@ export const clearCookiesOnLogout = () => {
|
|||
|
||||
Cookies.remove('auth_data');
|
||||
Cookies.remove('user');
|
||||
window.location = frontendURL('login');
|
||||
|
||||
const globalConfig = window.globalConfig || {};
|
||||
const logoutRedirectLink =
|
||||
globalConfig.LOGOUT_REDIRECT_LINK || frontendURL('login');
|
||||
window.location = logoutRedirectLink;
|
||||
};
|
||||
|
|
|
@ -1,25 +1,11 @@
|
|||
import Cookies from 'js-cookie';
|
||||
import { IFrameHelper } from '../sdk/IFrameHelper';
|
||||
import { getBubbleView } from '../sdk/bubbleHelpers';
|
||||
import md5 from 'md5';
|
||||
import { getUserCookieName } from '../sdk/cookieHelpers';
|
||||
|
||||
const REQUIRED_USER_KEYS = ['avatar_url', 'email', 'name'];
|
||||
|
||||
const ALLOWED_USER_ATTRIBUTES = [...REQUIRED_USER_KEYS, 'identifier_hash'];
|
||||
|
||||
export const getUserString = ({ identifier = '', user }) => {
|
||||
const userStringWithSortedKeys = ALLOWED_USER_ATTRIBUTES.reduce(
|
||||
(acc, key) => `${acc}${key}${user[key] || ''}`,
|
||||
''
|
||||
);
|
||||
return `${userStringWithSortedKeys}identifier${identifier}`;
|
||||
};
|
||||
|
||||
const computeHashForUserData = (...args) => md5(getUserString(...args));
|
||||
|
||||
export const hasUserKeys = user =>
|
||||
REQUIRED_USER_KEYS.reduce((acc, key) => acc || !!user[key], false);
|
||||
import { getBubbleView } from '../sdk/settingsHelper';
|
||||
import {
|
||||
computeHashForUserData,
|
||||
getUserCookieName,
|
||||
hasUserKeys,
|
||||
} from '../sdk/cookieHelpers';
|
||||
|
||||
const runSDK = ({ baseUrl, websiteToken }) => {
|
||||
if (window.$chatwoot) {
|
||||
|
@ -38,6 +24,7 @@ const runSDK = ({ baseUrl, websiteToken }) => {
|
|||
type: getBubbleView(chatwootSettings.type),
|
||||
launcherTitle: chatwootSettings.launcherTitle || '',
|
||||
showPopoutButton: chatwootSettings.showPopoutButton || false,
|
||||
widgetStyle: chatwootSettings.widgetStyle || 'standard',
|
||||
|
||||
toggle(state) {
|
||||
IFrameHelper.events.toggleBubble(state);
|
||||
|
|
|
@ -26,6 +26,7 @@ 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 { isFlatWidgetStyle } from './settingsHelper';
|
||||
|
||||
export const IFrameHelper = {
|
||||
getUrl({ baseUrl, websiteToken }) {
|
||||
|
@ -52,6 +53,10 @@ export const IFrameHelper = {
|
|||
if (window.$chatwoot.hideMessageBubble) {
|
||||
holderClassName += ` woot-widget--without-bubble`;
|
||||
}
|
||||
if (isFlatWidgetStyle(window.$chatwoot.widgetStyle)) {
|
||||
holderClassName += ` woot-widget-holder--flat`;
|
||||
}
|
||||
|
||||
addClass(widgetHolder, holderClassName);
|
||||
widgetHolder.appendChild(iframe);
|
||||
body.appendChild(widgetHolder);
|
||||
|
@ -121,6 +126,7 @@ export const IFrameHelper = {
|
|||
position: window.$chatwoot.position,
|
||||
hideMessageBubble: window.$chatwoot.hideMessageBubble,
|
||||
showPopoutButton: window.$chatwoot.showPopoutButton,
|
||||
widgetStyle: window.$chatwoot.widgetStyle,
|
||||
});
|
||||
IFrameHelper.onLoad({
|
||||
widgetColor: message.config.channelConfig.widgetColor,
|
||||
|
@ -222,21 +228,27 @@ export const IFrameHelper = {
|
|||
createBubbleHolder();
|
||||
onLocationChangeListener();
|
||||
if (!window.$chatwoot.hideMessageBubble) {
|
||||
let className = 'woot-widget-bubble';
|
||||
let closeBtnClassName = `woot-elements--${window.$chatwoot.position} woot-widget-bubble woot--close woot--hide`;
|
||||
|
||||
if (isFlatWidgetStyle(window.$chatwoot.widgetStyle)) {
|
||||
className += ' woot-widget-bubble--flat';
|
||||
closeBtnClassName += ' woot-widget-bubble--flat';
|
||||
}
|
||||
|
||||
const chatIcon = createBubbleIcon({
|
||||
className: 'woot-widget-bubble',
|
||||
className,
|
||||
src: bubbleImg,
|
||||
target: chatBubble,
|
||||
});
|
||||
|
||||
const closeIcon = closeBubble;
|
||||
const closeIconclassName = `woot-elements--${window.$chatwoot.position} woot-widget-bubble woot--close woot--hide`;
|
||||
addClass(closeIcon, closeIconclassName);
|
||||
addClass(closeBubble, closeBtnClassName);
|
||||
|
||||
chatIcon.style.background = widgetColor;
|
||||
closeIcon.style.background = widgetColor;
|
||||
closeBubble.style.background = widgetColor;
|
||||
|
||||
bubbleHolder.appendChild(chatIcon);
|
||||
bubbleHolder.appendChild(closeIcon);
|
||||
bubbleHolder.appendChild(closeBubble);
|
||||
bubbleHolder.appendChild(createNotificationBubble());
|
||||
onClickChatBubble();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { addClass, removeClass, toggleClass, wootOn } from './DOMHelpers';
|
||||
import { IFrameHelper } from './IFrameHelper';
|
||||
import { BUBBLE_DESIGN } from './constants';
|
||||
import { isExpandedView } from './settingsHelper';
|
||||
|
||||
export const bubbleImg =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAAwgJEBk0TVheY2R5eo+ut8jb5OXs8fX2+cjRDTIAAADsSURBVHgBldZbkoMgFIThRgQv8SKKgGf/C51UnJqaRI30/9zfe+NQUQ3TvG7bOk9DVeCmshmj/CuOTYnrdBfkUOg0zlOtl9OWVuEk4+QyZ3DIevmSt/ioTvK1VH/s5bY3YdM9SBZ/mUUyWgx+U06ycgp7D8msxSvtc4HXL9BLdj2elSEfhBJAI0QNgJEBI1BEBsQClVBVGDgwYOLAhJkDM1YOrNg4sLFAsLJgZsHEgoEFFQt0JAFGFjQsKAMJ0LFAexKgZYFyJIDxJIBNJEDNAtSJBLCeBDCOBFAPzwFA94ED+zmhwDO9358r8ANtIsMXi7qVAwAAAABJRU5ErkJggg==';
|
||||
|
@ -13,10 +13,6 @@ export const chatBubble = document.createElement('button');
|
|||
export const closeBubble = document.createElement('button');
|
||||
export const notificationBubble = document.createElement('span');
|
||||
|
||||
export const getBubbleView = type =>
|
||||
BUBBLE_DESIGN.includes(type) ? type : BUBBLE_DESIGN[0];
|
||||
export const isExpandedView = type => getBubbleView(type) === BUBBLE_DESIGN[1];
|
||||
|
||||
export const setBubbleText = bubbleText => {
|
||||
if (isExpandedView(window.$chatwoot.type)) {
|
||||
const textNode = document.getElementById('woot-widget--expanded__text');
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
export const BUBBLE_DESIGN = ['standard', 'expanded_bubble'];
|
||||
export const WIDGET_DESIGN = ['standard', 'flat'];
|
||||
|
|
|
@ -1,5 +1,23 @@
|
|||
import md5 from 'md5';
|
||||
|
||||
const REQUIRED_USER_KEYS = ['avatar_url', 'email', 'name'];
|
||||
const ALLOWED_USER_ATTRIBUTES = [...REQUIRED_USER_KEYS, 'identifier_hash'];
|
||||
|
||||
export const getUserCookieName = () => {
|
||||
const SET_USER_COOKIE_PREFIX = 'cw_user_';
|
||||
const { websiteToken: websiteIdentifier } = window.$chatwoot;
|
||||
return `${SET_USER_COOKIE_PREFIX}${websiteIdentifier}`;
|
||||
};
|
||||
|
||||
export const getUserString = ({ identifier = '', user }) => {
|
||||
const userStringWithSortedKeys = ALLOWED_USER_ATTRIBUTES.reduce(
|
||||
(acc, key) => `${acc}${key}${user[key] || ''}`,
|
||||
''
|
||||
);
|
||||
return `${userStringWithSortedKeys}identifier${identifier}`;
|
||||
};
|
||||
|
||||
export const computeHashForUserData = (...args) => md5(getUserString(...args));
|
||||
|
||||
export const hasUserKeys = user =>
|
||||
REQUIRED_USER_KEYS.reduce((acc, key) => acc || !!user[key], false);
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
export const SDK_CSS = `.woot-widget-holder {
|
||||
box-shadow: 0 5px 40px rgba(0, 0, 0, .16) !important;
|
||||
export const SDK_CSS = `
|
||||
:root {
|
||||
--b-100: #F2F3F7;
|
||||
}
|
||||
|
||||
.woot-widget-holder {
|
||||
box-shadow: 0 5px 40px rgba(0, 0, 0, .16);
|
||||
opacity: 1;
|
||||
will-change: transform, opacity;
|
||||
transform: translateY(0);
|
||||
|
@ -9,6 +14,12 @@ export const SDK_CSS = `.woot-widget-holder {
|
|||
z-index: 2147483000 !important;
|
||||
}
|
||||
|
||||
.woot-widget-holder.woot-widget-holder--flat {
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
border: 1px solid var(--b-100);
|
||||
}
|
||||
|
||||
.woot-widget-holder iframe {
|
||||
border: 0;
|
||||
height: 100% !important;
|
||||
|
@ -22,21 +33,45 @@ export const SDK_CSS = `.woot-widget-holder {
|
|||
height: auto;
|
||||
bottom: 94px;
|
||||
box-shadow: none !important;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.woot-widget-bubble {
|
||||
background: #1f93ff;
|
||||
border-radius: 100px !important;
|
||||
border-radius: 100px;
|
||||
border-width: 0px;
|
||||
bottom: 20px;
|
||||
padding: 0px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, .16) !important;
|
||||
cursor: pointer;
|
||||
height: 64px !important;
|
||||
height: 64px;
|
||||
padding: 0px;
|
||||
position: fixed;
|
||||
width: 64px !important;
|
||||
z-index: 2147483000 !important;
|
||||
user-select: none;
|
||||
width: 64px;
|
||||
z-index: 2147483000 !important;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget-bubble--flat {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.woot-widget-holder.woot-widget-holder--flat {
|
||||
bottom: 90px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget-bubble--flat {
|
||||
height: 56px;
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget-bubble--flat img {
|
||||
margin: 16px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.woot-widget-bubble--flat.woot--close::before,
|
||||
.woot-widget-bubble.woot-widget-bubble--flat.woot--close::after {
|
||||
left: 28px;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
.woot-widget-bubble.unread-notification::after {
|
||||
|
@ -184,7 +219,7 @@ export const SDK_CSS = `.woot-widget-holder {
|
|||
|
||||
@media only screen and (min-width: 667px) {
|
||||
.woot-widget-holder {
|
||||
border-radius: 16px !important;
|
||||
border-radius: 16px;
|
||||
bottom: 104px;
|
||||
height: calc(85% - 64px - 20px);
|
||||
max-height: 590px !important;
|
||||
|
|
11
app/javascript/sdk/settingsHelper.js
Normal file
11
app/javascript/sdk/settingsHelper.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { BUBBLE_DESIGN, WIDGET_DESIGN } from './constants';
|
||||
|
||||
export const getBubbleView = type =>
|
||||
BUBBLE_DESIGN.includes(type) ? type : BUBBLE_DESIGN[0];
|
||||
|
||||
export const isExpandedView = type => getBubbleView(type) === BUBBLE_DESIGN[1];
|
||||
|
||||
export const getWidgetStyle = style =>
|
||||
WIDGET_DESIGN.includes(style) ? style : WIDGET_DESIGN[0];
|
||||
|
||||
export const isFlatWidgetStyle = style => style === 'flat';
|
|
@ -1,17 +0,0 @@
|
|||
import { getBubbleView, isExpandedView } from '../bubbleHelpers';
|
||||
|
||||
describe('#getBubbleView', () => {
|
||||
it('returns correct view', () => {
|
||||
expect(getBubbleView('')).toEqual('standard');
|
||||
expect(getBubbleView('standard')).toEqual('standard');
|
||||
expect(getBubbleView('expanded_bubble')).toEqual('expanded_bubble');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isExpandedView', () => {
|
||||
it('returns true if it is expanded view', () => {
|
||||
expect(isExpandedView('')).toEqual(false);
|
||||
expect(isExpandedView('standard')).toEqual(false);
|
||||
expect(isExpandedView('expanded_bubble')).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -1,4 +1,8 @@
|
|||
import { getUserCookieName } from '../cookieHelpers';
|
||||
import {
|
||||
getUserCookieName,
|
||||
getUserString,
|
||||
hasUserKeys,
|
||||
} from '../cookieHelpers';
|
||||
|
||||
describe('#getUserCookieName', () => {
|
||||
it('returns correct cookie name', () => {
|
||||
|
@ -6,3 +10,40 @@ describe('#getUserCookieName', () => {
|
|||
expect(getUserCookieName()).toBe('cw_user_123456');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUserString', () => {
|
||||
it('returns correct user string', () => {
|
||||
expect(
|
||||
getUserString({
|
||||
user: {
|
||||
name: 'Pranav',
|
||||
email: 'pranav@example.com',
|
||||
avatar_url: 'https://images.chatwoot.com/placeholder',
|
||||
identifier_hash: '12345',
|
||||
},
|
||||
identifier: '12345',
|
||||
})
|
||||
).toBe(
|
||||
'avatar_urlhttps://images.chatwoot.com/placeholderemailpranav@example.comnamePranavidentifier_hash12345identifier12345'
|
||||
);
|
||||
|
||||
expect(
|
||||
getUserString({
|
||||
user: {
|
||||
email: 'pranav@example.com',
|
||||
avatar_url: 'https://images.chatwoot.com/placeholder',
|
||||
},
|
||||
})
|
||||
).toBe(
|
||||
'avatar_urlhttps://images.chatwoot.com/placeholderemailpranav@example.comnameidentifier_hashidentifier'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#hasUserKeys', () => {
|
||||
it('checks whether the allowed list of keys are present', () => {
|
||||
expect(hasUserKeys({})).toBe(false);
|
||||
expect(hasUserKeys({ randomKey: 'randomValue' })).toBe(false);
|
||||
expect(hasUserKeys({ avatar_url: 'randomValue' })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
38
app/javascript/sdk/specs/settingsHelpers.spec.js
Normal file
38
app/javascript/sdk/specs/settingsHelpers.spec.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import {
|
||||
getBubbleView,
|
||||
getWidgetStyle,
|
||||
isExpandedView,
|
||||
isFlatWidgetStyle,
|
||||
} from '../settingsHelper';
|
||||
|
||||
describe('#getBubbleView', () => {
|
||||
it('returns correct view', () => {
|
||||
expect(getBubbleView('')).toEqual('standard');
|
||||
expect(getBubbleView('standard')).toEqual('standard');
|
||||
expect(getBubbleView('expanded_bubble')).toEqual('expanded_bubble');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isExpandedView', () => {
|
||||
it('returns true if it is expanded view', () => {
|
||||
expect(isExpandedView('')).toEqual(false);
|
||||
expect(isExpandedView('standard')).toEqual(false);
|
||||
expect(isExpandedView('expanded_bubble')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getWidgetStyle', () => {
|
||||
it('returns correct view', () => {
|
||||
expect(getWidgetStyle('')).toEqual('standard');
|
||||
expect(getWidgetStyle('standard')).toEqual('standard');
|
||||
expect(getWidgetStyle('flat')).toEqual('flat');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isFlatWidgetStyle', () => {
|
||||
it('returns true if it is expanded view', () => {
|
||||
expect(isFlatWidgetStyle('')).toEqual(false);
|
||||
expect(isFlatWidgetStyle('standard')).toEqual(false);
|
||||
expect(isFlatWidgetStyle('flat')).toEqual(true);
|
||||
});
|
||||
});
|
|
@ -14,6 +14,7 @@ const {
|
|||
PRIVACY_URL: privacyURL,
|
||||
TERMS_URL: termsURL,
|
||||
WIDGET_BRAND_URL: widgetBrandURL,
|
||||
DISABLE_USER_PROFILE_UPDATE: disableUserProfileUpdate,
|
||||
} = window.globalConfig || {};
|
||||
|
||||
const state = {
|
||||
|
@ -24,6 +25,7 @@ const state = {
|
|||
chatwootInboxToken,
|
||||
createNewAccountFromDashboard,
|
||||
directUploadsEnabled: directUploadsEnabled === 'true',
|
||||
disableUserProfileUpdate: disableUserProfileUpdate === 'true',
|
||||
displayManifest,
|
||||
hCaptchaSiteKey,
|
||||
installationName,
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
import { getUserString, hasUserKeys } from '../../packs/sdk';
|
||||
|
||||
describe('#getUserString', () => {
|
||||
it('returns correct user string', () => {
|
||||
expect(
|
||||
getUserString({
|
||||
user: {
|
||||
name: 'Pranav',
|
||||
email: 'pranav@example.com',
|
||||
avatar_url: 'https://images.chatwoot.com/placeholder',
|
||||
identifier_hash: '12345',
|
||||
},
|
||||
identifier: '12345',
|
||||
})
|
||||
).toBe(
|
||||
'avatar_urlhttps://images.chatwoot.com/placeholderemailpranav@example.comnamePranavidentifier_hash12345identifier12345'
|
||||
);
|
||||
|
||||
expect(
|
||||
getUserString({
|
||||
user: {
|
||||
email: 'pranav@example.com',
|
||||
avatar_url: 'https://images.chatwoot.com/placeholder',
|
||||
},
|
||||
})
|
||||
).toBe(
|
||||
'avatar_urlhttps://images.chatwoot.com/placeholderemailpranav@example.comnameidentifier_hashidentifier'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#hasUserKeys', () => {
|
||||
it('checks whether the allowed list of keys are present', () => {
|
||||
expect(hasUserKeys({})).toBe(false);
|
||||
expect(hasUserKeys({ randomKey: 'randomValue' })).toBe(false);
|
||||
expect(hasUserKeys({ avatar_url: 'randomValue' })).toBe(true);
|
||||
});
|
||||
});
|
|
@ -12,6 +12,7 @@
|
|||
'is-mobile': isMobile,
|
||||
'is-widget-right': isRightAligned,
|
||||
'is-bubble-hidden': hideMessageBubble,
|
||||
'is-flat-design': isWidgetStyleFlat,
|
||||
}"
|
||||
>
|
||||
<router-view></router-view>
|
||||
|
@ -61,6 +62,7 @@ export default {
|
|||
isWidgetOpen: 'appConfig/getIsWidgetOpen',
|
||||
messageCount: 'conversation/getMessageCount',
|
||||
unreadMessageCount: 'conversation/getUnreadMessageCount',
|
||||
isWidgetStyleFlat: 'appConfig/isWidgetStyleFlat',
|
||||
}),
|
||||
isIFrame() {
|
||||
return IFrameHelper.isIFrame();
|
||||
|
|
|
@ -57,3 +57,30 @@ body {
|
|||
padding-left: $space-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.is-flat-design {
|
||||
.chat-bubble {
|
||||
border-bottom-left-radius: 0 !important;
|
||||
border-bottom-right-radius: 0 !important;
|
||||
border-top-left-radius: 0 !important;
|
||||
border-top-right-radius: 0 !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.chat-message--input {
|
||||
border-radius: 0 !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&.is-focused {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<footer
|
||||
v-if="!hideReplyBox"
|
||||
class="shadow-sm rounded-lg bg-white mb-1 z-50 relative"
|
||||
class="shadow-sm bg-white mb-1 z-50 relative"
|
||||
:class="{ 'rounded-lg': !isWidgetStyleFlat }"
|
||||
>
|
||||
<chat-input-wrap
|
||||
:on-send-message="handleSendMessage"
|
||||
|
@ -54,6 +55,7 @@ export default {
|
|||
widgetColor: 'appConfig/getWidgetColor',
|
||||
getConversationSize: 'conversation/getConversationSize',
|
||||
currentUser: 'contacts/getCurrentUser',
|
||||
isWidgetStyleFlat: 'appConfig/isWidgetStyleFlat',
|
||||
}),
|
||||
textColor() {
|
||||
return getContrastingTextColor(this.widgetColor);
|
||||
|
|
|
@ -97,7 +97,6 @@ export default {
|
|||
@import '~widget/assets/scss/mixins';
|
||||
|
||||
.header-wrap {
|
||||
border-radius: $space-normal $space-normal 0 0;
|
||||
flex-shrink: 0;
|
||||
transition: max-height 300ms;
|
||||
z-index: 99;
|
||||
|
|
|
@ -6,14 +6,15 @@ import {
|
|||
} from '../types';
|
||||
|
||||
const state = {
|
||||
showPopoutButton: false,
|
||||
hideMessageBubble: false,
|
||||
position: 'right',
|
||||
isWebWidgetTriggered: false,
|
||||
isCampaignViewClicked: false,
|
||||
isWebWidgetTriggered: false,
|
||||
isWidgetOpen: false,
|
||||
widgetColor: '',
|
||||
position: 'right',
|
||||
referrerHost: '',
|
||||
showPopoutButton: false,
|
||||
widgetColor: '',
|
||||
widgetStyle: 'standard',
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
|
@ -23,14 +24,19 @@ export const getters = {
|
|||
getIsWidgetOpen: $state => $state.isWidgetOpen,
|
||||
getWidgetColor: $state => $state.widgetColor,
|
||||
getReferrerHost: $state => $state.referrerHost,
|
||||
isWidgetStyleFlat: $state => $state.widgetStyle === 'flat',
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
setAppConfig({ commit }, { showPopoutButton, position, hideMessageBubble }) {
|
||||
setAppConfig(
|
||||
{ commit },
|
||||
{ showPopoutButton, position, hideMessageBubble, widgetStyle = 'rounded' }
|
||||
) {
|
||||
commit(SET_WIDGET_APP_CONFIG, {
|
||||
showPopoutButton: !!showPopoutButton,
|
||||
position: position || 'right',
|
||||
hideMessageBubble: !!hideMessageBubble,
|
||||
position: position || 'right',
|
||||
showPopoutButton: !!showPopoutButton,
|
||||
widgetStyle,
|
||||
});
|
||||
},
|
||||
toggleWidgetOpen({ commit }, isWidgetOpen) {
|
||||
|
@ -49,6 +55,7 @@ export const mutations = {
|
|||
$state.showPopoutButton = data.showPopoutButton;
|
||||
$state.position = data.position;
|
||||
$state.hideMessageBubble = data.hideMessageBubble;
|
||||
$state.widgetStyle = data.widgetStyle;
|
||||
},
|
||||
[TOGGLE_WIDGET_OPEN]($state, isWidgetOpen) {
|
||||
$state.isWidgetOpen = isWidgetOpen;
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -183,6 +183,9 @@ class Conversation < ApplicationRecord
|
|||
end
|
||||
|
||||
def notify_conversation_updation
|
||||
return unless previous_changes.keys.present? && (previous_changes.keys & %w[team_id assignee_id status snoozed_until
|
||||
custom_attributes]).present?
|
||||
|
||||
dispatcher_dispatch(CONVERSATION_UPDATED)
|
||||
end
|
||||
|
||||
|
|
|
@ -14,10 +14,11 @@
|
|||
|
||||
window.chatwootSettings = {
|
||||
hideMessageBubble: false,
|
||||
position: 'left',
|
||||
position: '<%= @widget_position %>',
|
||||
locale: 'en',
|
||||
type: 'expanded_bubble',
|
||||
type: '<%= @widget_type %>',
|
||||
showPopoutButton: true,
|
||||
widgetStyle: '<%= @widget_style %>',
|
||||
};
|
||||
|
||||
(function(d,t) {
|
||||
|
|
|
@ -53,3 +53,9 @@
|
|||
- name: HCAPTCHA_SERVER_KEY
|
||||
value:
|
||||
locked: false
|
||||
- name: LOGOUT_REDIRECT_LINK
|
||||
value: /app/login
|
||||
locked: false
|
||||
- name: DISABLE_USER_PROFILE_UPDATE
|
||||
value: false
|
||||
locked: false
|
||||
|
|
|
@ -6,7 +6,10 @@ describe ::ContactIdentifyAction do
|
|||
let!(:account) { create(:account) }
|
||||
let(:custom_attributes) { { test: 'test', test1: 'test1' } }
|
||||
let!(:contact) { create(:contact, account: account, custom_attributes: custom_attributes) }
|
||||
let(:params) { { name: 'test', identifier: 'test_id', custom_attributes: { test: 'new test', test2: 'test2' } } }
|
||||
let(:params) do
|
||||
{ name: 'test', identifier: 'test_id', additional_attributes: { location: 'Bengaulru', company_name: 'Meta' },
|
||||
custom_attributes: { test: 'new test', test2: 'test2' } }
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
it 'updates the contact' do
|
||||
|
@ -15,9 +18,18 @@ describe ::ContactIdentifyAction do
|
|||
expect(contact.reload.name).to eq 'test'
|
||||
# custom attributes are merged properly without overwriting existing ones
|
||||
expect(contact.custom_attributes).to eq({ 'test' => 'new test', 'test1' => 'test1', 'test2' => 'test2' })
|
||||
expect(contact.additional_attributes).to eq({ 'company_name' => 'Meta', 'location' => 'Bengaulru' })
|
||||
expect(contact.reload.identifier).to eq 'test_id'
|
||||
end
|
||||
|
||||
it 'merge deeply nested additional attributes' do
|
||||
create(:contact, account: account, identifier: '', email: 'test@test.com',
|
||||
additional_attributes: { location: 'Bengaulru', company_name: 'Meta', social_profiles: { linkedin: 'saras' } })
|
||||
params = { email: 'test@test.com', additional_attributes: { social_profiles: { twitter: 'saras' } } }
|
||||
result = described_class.new(contact: contact, params: params).perform
|
||||
expect(result.additional_attributes['social_profiles']).to eq({ 'linkedin' => 'saras', 'twitter' => 'saras' })
|
||||
end
|
||||
|
||||
it 'enques avatar job when avatar url parameter is passed' do
|
||||
params = { name: 'test', avatar_url: 'https://chatwoot-assets.local/sample.png' }
|
||||
expect(ContactAvatarJob).to receive(:perform_later).with(contact, params[:avatar_url]).once
|
||||
|
|
|
@ -90,6 +90,20 @@ RSpec.describe Conversation, type: :model do
|
|||
.with(described_class::CONVERSATION_READ, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||
.with(described_class::ASSIGNEE_CHANGED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
||||
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
|
||||
.with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
||||
end
|
||||
|
||||
it 'will not run conversation_updated event for empty updates' do
|
||||
conversation.save!
|
||||
expect(Rails.configuration.dispatcher).not_to have_received(:dispatch)
|
||||
.with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
||||
end
|
||||
|
||||
it 'will not run conversation_updated event for non whitelisted keys' do
|
||||
conversation.update(updated_at: DateTime.now.utc)
|
||||
expect(Rails.configuration.dispatcher).not_to have_received(:dispatch)
|
||||
.with(described_class::CONVERSATION_UPDATED, kind_of(Time), conversation: conversation, notifiable_assignee_change: true)
|
||||
end
|
||||
|
||||
it 'creates conversation activities' do
|
||||
|
|
Loading…
Reference in a new issue