Feature: Contact Panel with conversation details (#397)
* Add Contact panel changes * Fix parent iframe blocked * Add Conversation Panel, Contact messages * Update contact panel with conversation details * Update designs in sidebar * Fix specs * Specs: Add specs for conversationMetadata and contact modules * Fix currentUrl issues * Fix spelling * Set default to empty string
This commit is contained in:
parent
434d6c2656
commit
439e064d90
28 changed files with 662 additions and 42 deletions
|
@ -38,7 +38,9 @@ class Api::V1::Widget::MessagesController < ActionController::Base
|
|||
inbox_id: inbox.id,
|
||||
contact_id: cookie_params[:contact_id],
|
||||
additional_attributes: {
|
||||
browser: browser_params
|
||||
browser: browser_params,
|
||||
referer: permitted_params[:message][:referer_url],
|
||||
initiated_at: timestamp_params
|
||||
}
|
||||
}
|
||||
end
|
||||
|
@ -53,6 +55,12 @@ class Api::V1::Widget::MessagesController < ActionController::Base
|
|||
}
|
||||
end
|
||||
|
||||
def timestamp_params
|
||||
{
|
||||
timestamp: permitted_params[:message][:timestamp]
|
||||
}
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= ::Inbox.find_by(id: cookie_params[:inbox_id])
|
||||
end
|
||||
|
@ -79,7 +87,7 @@ class Api::V1::Widget::MessagesController < ActionController::Base
|
|||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:before, message: [:content])
|
||||
params.permit(:before, message: [:content, :referer_url, :timestamp])
|
||||
end
|
||||
|
||||
def secret_key
|
||||
|
|
9
app/javascript/dashboard/api/contacts.js
Normal file
9
app/javascript/dashboard/api/contacts.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import ApiClient from './ApiClient';
|
||||
|
||||
class ContactAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('contacts');
|
||||
}
|
||||
}
|
||||
|
||||
export default new ContactAPI();
|
|
@ -217,3 +217,9 @@
|
|||
border-left: $size solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
:username="chat.meta.sender.name"
|
||||
/>
|
||||
<div class="user--profile__meta">
|
||||
<h3 class="user--name">
|
||||
<h3 v-if="!isContactPanelOpen" class="user--name text-truncate">
|
||||
{{ chat.meta.sender.name }}
|
||||
</h3>
|
||||
<button
|
||||
|
@ -113,3 +113,11 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.text-truncate {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
|
8
app/javascript/dashboard/i18n/locale/en/contact.json
Normal file
8
app/javascript/dashboard/i18n/locale/en/contact.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"CONTACT_PANEL": {
|
||||
"BROWSER": "Browser",
|
||||
"OS": "Operating System",
|
||||
"INITIATED_FROM": "Initiated from",
|
||||
"INITIATED_AT": "Initiated at"
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import { default as _agentMgmt } from './agentMgmt.json';
|
|||
import { default as _billing } from './billing.json';
|
||||
import { default as _cannedMgmt } from './cannedMgmt.json';
|
||||
import { default as _chatlist } from './chatlist.json';
|
||||
import { default as _contact } from './contact.json';
|
||||
import { default as _conversation } from './conversation.json';
|
||||
import { default as _inboxMgmt } from './inboxMgmt.json';
|
||||
import { default as _login } from './login.json';
|
||||
|
@ -16,6 +17,7 @@ export default {
|
|||
..._billing,
|
||||
..._cannedMgmt,
|
||||
..._chatlist,
|
||||
..._contact,
|
||||
..._conversation,
|
||||
..._inboxMgmt,
|
||||
..._login,
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div class="conv-details--item">
|
||||
<div class="conv-details--item__label">
|
||||
<i :class="icon" class="conv-details--item__icon"></i>
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="conv-details--item__value">
|
||||
{{ value }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: { type: String, required: true },
|
||||
icon: { type: String, required: true },
|
||||
value: { type: [String, Number], default: '' },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.conv-details--item {
|
||||
padding-bottom: $space-normal;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.conv-details--item__icon {
|
||||
padding-right: $space-micro;
|
||||
}
|
||||
|
||||
.conv-details--item__label {
|
||||
font-weight: $font-weight-medium;
|
||||
margin-bottom: $space-micro;
|
||||
}
|
||||
|
||||
.conv-details--item__value {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,37 +1,127 @@
|
|||
<template>
|
||||
<div class="medium-3 bg-white contact--panel">
|
||||
<thumbnail
|
||||
:src="contactImage"
|
||||
size="80px"
|
||||
:badge="contact.channel"
|
||||
:username="contact.name"
|
||||
/>
|
||||
<h4>
|
||||
{{ contact.name }}
|
||||
</h4>
|
||||
<div class="contact--profile">
|
||||
<div class="contact--info">
|
||||
<thumbnail
|
||||
:src="contact.avatar_url"
|
||||
size="56px"
|
||||
:badge="contact.channel"
|
||||
:username="contact.name"
|
||||
/>
|
||||
<div class="contact--details">
|
||||
<div class="contact--name">
|
||||
{{ contact.name }}
|
||||
</div>
|
||||
<a
|
||||
v-if="contact.email"
|
||||
:href="`mailto:${contact.email}`"
|
||||
class="contact--email"
|
||||
>
|
||||
{{ contact.email }}
|
||||
</a>
|
||||
<div class="contact--location">
|
||||
{{ contact.location }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="contact.bio" class="contact--bio">
|
||||
{{ contact.bio }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="browser" class="conversation--details">
|
||||
<contact-details-item
|
||||
v-if="browser.browser_name"
|
||||
:title="$t('CONTACT_PANEL.BROWSER')"
|
||||
:value="browserName"
|
||||
icon="ion-ios-world-outline"
|
||||
/>
|
||||
<contact-details-item
|
||||
v-if="browser.platform_name"
|
||||
:title="$t('CONTACT_PANEL.OS')"
|
||||
:value="platformName"
|
||||
icon="ion-laptop"
|
||||
/>
|
||||
<contact-details-item
|
||||
v-if="referer"
|
||||
:title="$t('CONTACT_PANEL.INITIATED_FROM')"
|
||||
:value="referer"
|
||||
icon="ion-link"
|
||||
/>
|
||||
<contact-details-item
|
||||
v-if="initiatedAt"
|
||||
:title="$t('CONTACT_PANEL.INITIATED_AT')"
|
||||
:value="initiatedAt.timestamp"
|
||||
icon="ion-clock"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactDetailsItem,
|
||||
Thumbnail,
|
||||
},
|
||||
props: {
|
||||
conversationId: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentChat: 'getSelectedChat',
|
||||
}),
|
||||
currentConversationMetaData() {
|
||||
return this.$store.getters[
|
||||
'conversationMetadata/getConversationMetadata'
|
||||
](this.conversationId);
|
||||
},
|
||||
additionalAttributes() {
|
||||
return this.currentConversationMetaData.additional_attributes || {};
|
||||
},
|
||||
browser() {
|
||||
return this.additionalAttributes.browser || {};
|
||||
},
|
||||
referer() {
|
||||
return this.additionalAttributes.referer;
|
||||
},
|
||||
initiatedAt() {
|
||||
return this.additionalAttributes.initiated_at;
|
||||
},
|
||||
browserName() {
|
||||
return `${this.browser.browser_name || ''} ${this.browser
|
||||
.browser_version || ''}`;
|
||||
},
|
||||
platformName() {
|
||||
const {
|
||||
platform_name: platformName,
|
||||
platform_version: platformVersion,
|
||||
} = this.browser;
|
||||
return `${platformName || ''} ${platformVersion || ''}`;
|
||||
},
|
||||
contactId() {
|
||||
return this.currentConversationMetaData.contact_id;
|
||||
},
|
||||
contact() {
|
||||
const { meta: { sender = {} } = {} } = this.currentChat || {};
|
||||
return sender;
|
||||
return this.$store.getters['contacts/getContact'](this.contactId);
|
||||
},
|
||||
contactImage() {
|
||||
return `/uploads/avatar/contact/${this.contact.id}/profilepic.jpeg`;
|
||||
},
|
||||
watch: {
|
||||
contactId(newContactId, prevContactId) {
|
||||
if (newContactId && newContactId !== prevContactId) {
|
||||
this.$store.dispatch('contacts/show', {
|
||||
id: this.currentConversationMetaData.contact_id,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('contacts/show', {
|
||||
id: this.currentConversationMetaData.contact_id,
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -41,9 +131,70 @@ export default {
|
|||
|
||||
.contact--panel {
|
||||
@include border-normal-left;
|
||||
font-size: $font-size-small;
|
||||
overflow-y: auto;
|
||||
background: $color-white;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.contact--profile {
|
||||
width: 100%;
|
||||
padding: $space-normal $space-medium $zero;
|
||||
align-items: center;
|
||||
|
||||
.user-thumbnail-box {
|
||||
margin-right: $space-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.contact--details {
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.contact--info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: $space-large $space-normal $space-normal;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.contact--name {
|
||||
@include text-ellipsis;
|
||||
|
||||
font-weight: $font-weight-bold;
|
||||
font-size: $font-size-default;
|
||||
}
|
||||
|
||||
.contact--email {
|
||||
@include text-ellipsis;
|
||||
|
||||
color: $color-body;
|
||||
display: block;
|
||||
line-height: $space-medium;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.contact--bio {
|
||||
margin-top: $space-normal;
|
||||
}
|
||||
|
||||
.conversation--details {
|
||||
padding: $space-normal $space-medium;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.conversation--labels {
|
||||
padding: $space-medium;
|
||||
|
||||
.icon {
|
||||
margin-right: $space-micro;
|
||||
font-size: $font-size-micro;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #fff;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,7 +11,10 @@
|
|||
@contactPanelToggle="onToggleContactPanel"
|
||||
>
|
||||
</conversation-box>
|
||||
<contact-panel v-if="isContactPanelOpen"></contact-panel>
|
||||
<contact-panel
|
||||
v-if="isContactPanelOpen"
|
||||
:conversation-id="conversationId"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ import auth from './modules/auth';
|
|||
import billing from './modules/billing';
|
||||
import cannedResponse from './modules/cannedResponse';
|
||||
import Channel from './modules/channels';
|
||||
import contacts from './modules/contacts';
|
||||
import conversationMetadata from './modules/conversationMetadata';
|
||||
import conversations from './modules/conversations';
|
||||
import inboxes from './modules/inboxes';
|
||||
import inboxMembers from './modules/inboxMembers';
|
||||
|
@ -19,6 +21,8 @@ export default new Vuex.Store({
|
|||
billing,
|
||||
cannedResponse,
|
||||
Channel,
|
||||
contacts,
|
||||
conversationMetadata,
|
||||
conversations,
|
||||
inboxes,
|
||||
inboxMembers,
|
||||
|
|
85
app/javascript/dashboard/store/modules/contacts.js
Normal file
85
app/javascript/dashboard/store/modules/contacts.js
Normal file
|
@ -0,0 +1,85 @@
|
|||
/* eslint no-param-reassign: 0 */
|
||||
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
||||
import * as types from '../mutation-types';
|
||||
import ContactAPI from '../../api/contacts';
|
||||
|
||||
const state = {
|
||||
records: [],
|
||||
uiFlags: {
|
||||
isFetching: false,
|
||||
isFetchingItem: false,
|
||||
isUpdating: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getContacts($state) {
|
||||
return $state.records;
|
||||
},
|
||||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
},
|
||||
getContact: $state => id => {
|
||||
const [contact = {}] = $state.records.filter(
|
||||
record => record.id === Number(id)
|
||||
);
|
||||
return contact;
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
get: async ({ commit }) => {
|
||||
commit(types.default.SET_CONTACT_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const response = await ContactAPI.get();
|
||||
commit(types.default.SET_CONTACTS, response.data.payload);
|
||||
commit(types.default.SET_CONTACT_UI_FLAG, { isFetching: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_CONTACT_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
|
||||
show: async ({ commit }, { id }) => {
|
||||
commit(types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: true });
|
||||
try {
|
||||
const response = await ContactAPI.show(id);
|
||||
commit(types.default.SET_CONTACT_ITEM, response.data.payload);
|
||||
commit(types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: false });
|
||||
}
|
||||
},
|
||||
|
||||
update: async ({ commit }, { id, ...updateObj }) => {
|
||||
commit(types.default.SET_CONTACT_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
const response = await ContactAPI.update(id, updateObj);
|
||||
commit(types.default.EDIT_CONTACT, response.data.payload);
|
||||
commit(types.default.SET_CONTACT_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_CONTACT_UI_FLAG, { isUpdating: false });
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
[types.default.SET_CONTACT_UI_FLAG]($state, data) {
|
||||
$state.uiFlags = {
|
||||
...$state.uiFlags,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
|
||||
[types.default.SET_CONTACTS]: MutationHelpers.set,
|
||||
[types.default.SET_CONTACT_ITEM]: MutationHelpers.setSingleRecord,
|
||||
[types.default.EDIT_CONTACT]: MutationHelpers.update,
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
import Vue from 'vue';
|
||||
import * as types from '../mutation-types';
|
||||
|
||||
const state = {
|
||||
records: {},
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getConversationMetadata: $state => id => {
|
||||
return $state.records[Number(id)] || {};
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {};
|
||||
|
||||
export const mutations = {
|
||||
[types.default.SET_CONVERSATION_METADATA]: ($state, { id, data }) => {
|
||||
Vue.set($state.records, id, data);
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
|
@ -31,12 +31,21 @@ const actions = {
|
|||
|
||||
fetchPreviousMessages: async ({ commit }, data) => {
|
||||
try {
|
||||
const response = await MessageApi.getPreviousMessages(data);
|
||||
const {
|
||||
data: { meta, payload },
|
||||
} = await MessageApi.getPreviousMessages(data);
|
||||
commit(
|
||||
`conversationMetadata/${types.default.SET_CONVERSATION_METADATA}`,
|
||||
{
|
||||
id: data.conversationId,
|
||||
data: meta,
|
||||
}
|
||||
);
|
||||
commit(types.default.SET_PREVIOUS_CONVERSATIONS, {
|
||||
id: data.conversationId,
|
||||
data: response.data.payload,
|
||||
data: payload,
|
||||
});
|
||||
if (response.data.payload.length < 20) {
|
||||
if (payload.length < 20) {
|
||||
commit(types.default.SET_ALL_MESSAGES_LOADED);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
@ -60,7 +60,6 @@ const mutations = {
|
|||
const [chat] = getSelectedChatConversation(_state);
|
||||
Vue.set(chat, 'allMessagesLoaded', false);
|
||||
},
|
||||
|
||||
[types.default.CLEAR_CURRENT_CHAT_WINDOW](_state) {
|
||||
_state.selectedChat.id = null;
|
||||
_state.selectedChat.agentTyping = 'off';
|
||||
|
|
|
@ -36,7 +36,7 @@ describe('#mutations', () => {
|
|||
});
|
||||
|
||||
describe('#EDIT_AGENT', () => {
|
||||
it('sets allMessagesLoaded flag if payload is empty', () => {
|
||||
it('update agent record', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, name: 'Agent1', email: 'agent1@chatwoot.com' }],
|
||||
};
|
||||
|
@ -52,7 +52,7 @@ describe('#mutations', () => {
|
|||
});
|
||||
|
||||
describe('#DELETE_AGENT', () => {
|
||||
it('sets allMessagesLoaded flag if payload is empty', () => {
|
||||
it('delete agent record', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, name: 'Agent1', email: 'agent1@chatwoot.com' }],
|
||||
};
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import axios from 'axios';
|
||||
import { actions } from '../../contacts';
|
||||
import * as types from '../../../mutation-types';
|
||||
import contactList from './fixtures';
|
||||
|
||||
const commit = jest.fn();
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#get', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({ data: { payload: contactList } });
|
||||
await actions.get({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CONTACT_UI_FLAG, { isFetching: true }],
|
||||
[types.default.SET_CONTACTS, contactList],
|
||||
[types.default.SET_CONTACT_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.get({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CONTACT_UI_FLAG, { isFetching: true }],
|
||||
[types.default.SET_CONTACT_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#show', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({ data: { payload: contactList[0] } });
|
||||
await actions.show({ commit }, { id: contactList[0].id });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: true }],
|
||||
[types.default.SET_CONTACT_ITEM, contactList[0]],
|
||||
[types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.show({ commit }, { id: contactList[0].id });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: true }],
|
||||
[types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.patch.mockResolvedValue({ data: { payload: contactList[0] } });
|
||||
await actions.update({ commit }, contactList[0]);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CONTACT_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.EDIT_CONTACT, contactList[0]],
|
||||
[types.default.SET_CONTACT_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(actions.update({ commit }, contactList[0])).rejects.toThrow(
|
||||
Error
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CONTACT_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.SET_CONTACT_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
export default [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Contact 1',
|
||||
email: 'contact1@chatwoot.com',
|
||||
phone_number: '9000000001',
|
||||
thumbnail: 'contact1.png',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Contact 2',
|
||||
email: 'contact2@chatwoot.com',
|
||||
phone_number: '9000000002',
|
||||
thumbnail: 'contact2.png',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Contact 3',
|
||||
email: 'contact3@chatwoot.com',
|
||||
phone_number: '9000000003',
|
||||
thumbnail: 'contact3.png',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Contact 4',
|
||||
email: 'contact4@chatwoot.com',
|
||||
phone_number: '9000000004',
|
||||
thumbnail: 'contact4.png',
|
||||
},
|
||||
];
|
|
@ -0,0 +1,33 @@
|
|||
import { getters } from '../../contacts';
|
||||
import contactList from './fixtures';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getContacts', () => {
|
||||
const state = {
|
||||
records: contactList,
|
||||
};
|
||||
expect(getters.getContacts(state)).toEqual(contactList);
|
||||
});
|
||||
|
||||
it('getContact', () => {
|
||||
const state = {
|
||||
records: contactList,
|
||||
};
|
||||
expect(getters.getContact(state)(2)).toEqual(contactList[1]);
|
||||
});
|
||||
|
||||
it('getUIFlags', () => {
|
||||
const state = {
|
||||
uiFlags: {
|
||||
isFetching: true,
|
||||
isFetchingItem: true,
|
||||
isUpdating: false,
|
||||
},
|
||||
};
|
||||
expect(getters.getUIFlags(state)).toEqual({
|
||||
isFetching: true,
|
||||
isFetchingItem: true,
|
||||
isUpdating: false,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
import * as types from '../../../mutation-types';
|
||||
import { mutations } from '../../contacts';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#SET_CONTACTS', () => {
|
||||
it('set contact records', () => {
|
||||
const state = { records: [] };
|
||||
mutations[types.default.SET_CONTACTS](state, [
|
||||
{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||
]);
|
||||
expect(state.records).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
name: 'contact1',
|
||||
email: 'contact1@chatwoot.com',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_CONTACT_ITEM', () => {
|
||||
it('push contact data to the store', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' }],
|
||||
};
|
||||
mutations[types.default.SET_CONTACT_ITEM](state, {
|
||||
id: 2,
|
||||
name: 'contact2',
|
||||
email: 'contact2@chatwoot.com',
|
||||
});
|
||||
expect(state.records).toEqual([
|
||||
{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||
{ id: 2, name: 'contact2', email: 'contact2@chatwoot.com' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#EDIT_CONTACT', () => {
|
||||
it('update contact', () => {
|
||||
const state = {
|
||||
records: [{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' }],
|
||||
};
|
||||
mutations[types.default.EDIT_CONTACT](state, {
|
||||
id: 1,
|
||||
name: 'contact2',
|
||||
email: 'contact2@chatwoot.com',
|
||||
});
|
||||
expect(state.records).toEqual([
|
||||
{ id: 1, name: 'contact2', email: 'contact2@chatwoot.com' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import { getters } from '../../conversationMetadata';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getConversationMetadata', () => {
|
||||
const state = {
|
||||
records: {
|
||||
1: {
|
||||
browser: { name: 'Chrome' },
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(getters.getConversationMetadata(state)(1)).toEqual({
|
||||
browser: { name: 'Chrome' },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import * as types from '../../../mutation-types';
|
||||
import { mutations } from '../../conversationMetadata';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#SET_INBOXES', () => {
|
||||
it('set inbox records', () => {
|
||||
const state = { records: {} };
|
||||
mutations[types.default.SET_CONVERSATION_METADATA](state, {
|
||||
id: 1,
|
||||
data: { browser: { name: 'Chrome' } },
|
||||
});
|
||||
expect(state.records).toEqual({
|
||||
1: { browser: { name: 'Chrome' } },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -56,6 +56,12 @@ export default {
|
|||
EDIT_CANNED: 'EDIT_CANNED',
|
||||
DELETE_CANNED: 'DELETE_CANNED',
|
||||
|
||||
// Contacts
|
||||
SET_CONTACT_UI_FLAG: 'SET_CONTACT_UI_FLAG',
|
||||
SET_CONTACT_ITEM: 'SET_CONTACT_ITEM',
|
||||
SET_CONTACTS: 'SET_CONTACTS',
|
||||
EDIT_CONTACT: 'EDIT_CONTACT',
|
||||
|
||||
// Reports
|
||||
SET_ACCOUNT_REPORTS: 'SET_ACCOUNT_REPORTS',
|
||||
SET_ACCOUNT_SUMMARY: 'SET_ACCOUNT_SUMMARY',
|
||||
|
@ -64,4 +70,7 @@ export default {
|
|||
// Billings
|
||||
SET_SUBSCRIPTION: 'SET_SUBSCRIPTION',
|
||||
TOGGLE_SUBSCRIPTION_LOADING: 'TOGGLE_SUBSCRIPTION_LOADING',
|
||||
|
||||
// Conversation Metadata
|
||||
SET_CONVERSATION_METADATA: 'SET_CONVERSATION_METADATA',
|
||||
};
|
||||
|
|
|
@ -126,14 +126,15 @@ const IFrameHelper = {
|
|||
}
|
||||
iframe.src = widgetUrl;
|
||||
|
||||
iframe.id = 'chatwoot_web_widget';
|
||||
iframe.id = 'chatwoot_live_chat_widget';
|
||||
iframe.style.visibility = 'hidden';
|
||||
holder.className = 'woot-widget-holder woot--hide';
|
||||
holder.appendChild(iframe);
|
||||
body.appendChild(holder);
|
||||
IFrameHelper.initPostMessageCommunication();
|
||||
IFrameHelper.initLocationListener();
|
||||
},
|
||||
getAppFrame: () => document.getElementById('chatwoot_web_widget'),
|
||||
getAppFrame: () => document.getElementById('chatwoot_live_chat_widget'),
|
||||
sendMessage: (key, value) => {
|
||||
const element = IFrameHelper.getAppFrame();
|
||||
element.contentWindow.postMessage(
|
||||
|
@ -154,9 +155,15 @@ const IFrameHelper = {
|
|||
Cookies.set('cw_conversation', message.config.authToken);
|
||||
IFrameHelper.sendMessage('config-set', {});
|
||||
IFrameHelper.onLoad(message.config.channelConfig);
|
||||
IFrameHelper.setCurrentUrl();
|
||||
}
|
||||
};
|
||||
},
|
||||
initLocationListener: () => {
|
||||
window.onhashchange = () => {
|
||||
IFrameHelper.setCurrentUrl();
|
||||
};
|
||||
},
|
||||
onLoad: ({ widget_color: widgetColor }) => {
|
||||
const iframe = IFrameHelper.getAppFrame();
|
||||
iframe.style.visibility = '';
|
||||
|
@ -187,6 +194,12 @@ const IFrameHelper = {
|
|||
bubbleHolder.appendChild(createNotificationBubble());
|
||||
onClickChatBubble();
|
||||
},
|
||||
setCurrentUrl: () => {
|
||||
console.log(IFrameHelper.getAppFrame(), document);
|
||||
IFrameHelper.sendMessage('set-current-url', {
|
||||
refererURL: window.location.href,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
function loadIframe({ baseUrl, websiteToken }) {
|
||||
|
|
|
@ -20,16 +20,6 @@ export const IFrameHelper = {
|
|||
|
||||
export default {
|
||||
name: 'App',
|
||||
|
||||
methods: {
|
||||
...mapActions('appConfig', ['setWidgetColor']),
|
||||
...mapActions('conversation', ['fetchOldConversations']),
|
||||
scrollConversationToBottom() {
|
||||
const container = this.$el.querySelector('.conversation-wrap');
|
||||
container.scrollTop = container.scrollHeight;
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (IFrameHelper.isIFrame()) {
|
||||
IFrameHelper.sendMessage({
|
||||
|
@ -55,9 +45,19 @@ export default {
|
|||
this.fetchOldConversations();
|
||||
} else if (message.event === 'widget-visible') {
|
||||
this.scrollConversationToBottom();
|
||||
} else if (message.event === 'set-current-url') {
|
||||
window.refererURL = message.refererURL;
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapActions('appConfig', ['setWidgetColor']),
|
||||
...mapActions('conversation', ['fetchOldConversations']),
|
||||
scrollConversationToBottom() {
|
||||
const container = this.$el.querySelector('.conversation-wrap');
|
||||
container.scrollTop = container.scrollHeight;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ const sendMessage = content => ({
|
|||
params: {
|
||||
message: {
|
||||
content,
|
||||
timestamp: new Date().toString(),
|
||||
referer_url: window.refererURL || '',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,14 +2,20 @@ import endPoints from '../endPoints';
|
|||
|
||||
describe('#sendMessage', () => {
|
||||
it('returns correct payload', () => {
|
||||
const spy = jest.spyOn(global, 'Date').mockImplementation(() => ({
|
||||
toString: () => 'mock date',
|
||||
}));
|
||||
expect(endPoints.sendMessage('hello')).toEqual({
|
||||
url: `/api/v1/widget/messages`,
|
||||
params: {
|
||||
message: {
|
||||
content: 'hello',
|
||||
referer_url: '',
|
||||
timestamp: 'mock date',
|
||||
},
|
||||
},
|
||||
});
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -4,6 +4,6 @@ json.payload do
|
|||
json.name contact.name
|
||||
json.email contact.email
|
||||
json.phone_number contact.phone_number
|
||||
json.thumbnail contact.avatar.thumb.url
|
||||
json.thumbnail contact.avatar.profile_thumb.url
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
json.meta do
|
||||
json.labels @conversation.label_list
|
||||
json.additional_attributes @conversation.additional_attributes
|
||||
json.contact_id @conversation.contact_id
|
||||
end
|
||||
|
||||
json.payload do
|
||||
|
|
Loading…
Reference in a new issue