Feature: View a contact's previous conversation (#422)
* Add API to fetch conversations of a contact * Add conversation list in sidebar
This commit is contained in:
parent
fc6a8c2601
commit
655c585358
19 changed files with 491 additions and 5 deletions
23
app/controllers/api/v1/contacts/conversations_controller.rb
Normal file
23
app/controllers/api/v1/contacts/conversations_controller.rb
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
class Api::V1::Contacts::ConversationsController < Api::BaseController
|
||||||
|
def index
|
||||||
|
@conversations = current_account.conversations.includes(
|
||||||
|
:assignee, :contact, :inbox
|
||||||
|
).where(inbox_id: inbox_ids, contact_id: permitted_params[:contact_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def inbox_ids
|
||||||
|
if current_user.administrator?
|
||||||
|
current_account.inboxes.pluck(:id)
|
||||||
|
elsif current_user.agent?
|
||||||
|
current_user.assigned_inboxes.pluck(:id)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def permitted_params
|
||||||
|
params.permit(:contact_id)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,9 +1,14 @@
|
||||||
|
/* global axios */
|
||||||
import ApiClient from './ApiClient';
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
class ContactAPI extends ApiClient {
|
class ContactAPI extends ApiClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
super('contacts');
|
super('contacts');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getConversations(contactId) {
|
||||||
|
return axios.get(`${this.url}/${contactId}/conversations`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ContactAPI();
|
export default new ContactAPI();
|
||||||
|
|
14
app/javascript/dashboard/api/specs/contacts.spec.js
Normal file
14
app/javascript/dashboard/api/specs/contacts.spec.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import agents from '../contacts';
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
describe('#ContactsAPI', () => {
|
||||||
|
it('creates correct instance', () => {
|
||||||
|
expect(agents).toBeInstanceOf(ApiClient);
|
||||||
|
expect(agents).toHaveProperty('get');
|
||||||
|
expect(agents).toHaveProperty('show');
|
||||||
|
expect(agents).toHaveProperty('create');
|
||||||
|
expect(agents).toHaveProperty('update');
|
||||||
|
expect(agents).toHaveProperty('delete');
|
||||||
|
expect(agents).toHaveProperty('getConversations');
|
||||||
|
});
|
||||||
|
});
|
|
@ -41,6 +41,7 @@
|
||||||
color: $color-body;
|
color: $color-body;
|
||||||
width: 27rem;
|
width: 27rem;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
max-width: 96%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation--meta {
|
.conversation--meta {
|
||||||
|
@ -91,4 +92,12 @@
|
||||||
font-weight: $font-weight-medium;
|
font-weight: $font-weight-medium;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.compact {
|
||||||
|
padding-left: 0;
|
||||||
|
|
||||||
|
.conversation--details {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
@click="cardClick(chat)"
|
@click="cardClick(chat)"
|
||||||
>
|
>
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
|
v-if="!hideThumbnail"
|
||||||
:src="chat.meta.sender.thumbnail"
|
:src="chat.meta.sender.thumbnail"
|
||||||
:badge="chat.meta.sender.channel"
|
:badge="chat.meta.sender.channel"
|
||||||
class="columns"
|
class="columns"
|
||||||
|
@ -15,7 +16,7 @@
|
||||||
<h4 class="conversation--user">
|
<h4 class="conversation--user">
|
||||||
{{ chat.meta.sender.name }}
|
{{ chat.meta.sender.name }}
|
||||||
<span
|
<span
|
||||||
v-if="isInboxNameVisible"
|
v-if="!hideInboxName && isInboxNameVisible"
|
||||||
v-tooltip.bottom="inboxName(chat.inbox_id)"
|
v-tooltip.bottom="inboxName(chat.inbox_id)"
|
||||||
class="label"
|
class="label"
|
||||||
>
|
>
|
||||||
|
@ -58,6 +59,14 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
|
hideInboxName: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
hideThumbnail: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
|
@ -3,6 +3,10 @@
|
||||||
"BROWSER": "Browser",
|
"BROWSER": "Browser",
|
||||||
"OS": "Operating System",
|
"OS": "Operating System",
|
||||||
"INITIATED_FROM": "Initiated from",
|
"INITIATED_FROM": "Initiated from",
|
||||||
"INITIATED_AT": "Initiated at"
|
"INITIATED_AT": "Initiated at",
|
||||||
|
"CONVERSATIONS": {
|
||||||
|
"NO_RECORDS_FOUND": "There are no previous conversations associated to this contact.",
|
||||||
|
"TITLE": "Previous Conversations"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
<template>
|
||||||
|
<div class="contact-conversation--panel">
|
||||||
|
<contact-details-item
|
||||||
|
icon="ion-chatbubbles"
|
||||||
|
:title="$t('CONTACT_PANEL.CONVERSATIONS.TITLE')"
|
||||||
|
/>
|
||||||
|
<div v-if="!uiFlags.isFetching">
|
||||||
|
<i v-if="!previousConversations.length">
|
||||||
|
{{ $t('CONTACT_PANEL.CONVERSATIONS.NO_RECORDS_FOUND') }}
|
||||||
|
</i>
|
||||||
|
<div v-else class="contact-conversation--list">
|
||||||
|
<conversation-card
|
||||||
|
v-for="conversation in previousConversations"
|
||||||
|
:key="conversation.id"
|
||||||
|
:chat="conversation"
|
||||||
|
:hide-inbox-name="true"
|
||||||
|
:hide-thumbnail="true"
|
||||||
|
class="compact"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<spinner v-else></spinner>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import ConversationCard from 'dashboard/components/widgets/conversation/ConversationCard.vue';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
|
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ConversationCard,
|
||||||
|
ContactDetailsItem,
|
||||||
|
Spinner,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
contactId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
conversations() {
|
||||||
|
return this.$store.getters['contactConversations/getContactConversation'](
|
||||||
|
this.contactId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
previousConversations() {
|
||||||
|
return this.conversations.filter(
|
||||||
|
conversation => conversation.id !== Number(this.conversationId)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
...mapGetters({
|
||||||
|
uiFlags: 'contactConversations/getUIFlags',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
contactId(newContactId, prevContactId) {
|
||||||
|
if (newContactId && newContactId !== prevContactId) {
|
||||||
|
this.$store.dispatch('contactConversations/get', newContactId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('contactConversations/get', this.contactId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~dashboard/assets/scss/variables';
|
||||||
|
@import '~dashboard/assets/scss/mixins';
|
||||||
|
|
||||||
|
.contact-conversation--panel {
|
||||||
|
@include border-normal-top;
|
||||||
|
padding: $space-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-conversation--list {
|
||||||
|
margin-top: -$space-normal;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,7 +4,7 @@
|
||||||
<i :class="icon" class="conv-details--item__icon"></i>
|
<i :class="icon" class="conv-details--item__icon"></i>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="conv-details--item__value">
|
<div v-if="value" class="conv-details--item__value">
|
||||||
{{ value }}
|
{{ value }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -54,15 +54,22 @@
|
||||||
icon="ion-clock"
|
icon="ion-clock"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<contact-conversations
|
||||||
|
v-if="contact.id"
|
||||||
|
:contact-id="contact.id"
|
||||||
|
:conversation-id="conversationId"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||||
|
import ContactConversations from './ContactConversations.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
ContactConversations,
|
||||||
ContactDetailsItem,
|
ContactDetailsItem,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
},
|
},
|
||||||
|
@ -179,7 +186,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation--details {
|
.conversation--details {
|
||||||
padding: $space-normal $space-medium;
|
padding: $space-medium;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import billing from './modules/billing';
|
||||||
import cannedResponse from './modules/cannedResponse';
|
import cannedResponse from './modules/cannedResponse';
|
||||||
import Channel from './modules/channels';
|
import Channel from './modules/channels';
|
||||||
import contacts from './modules/contacts';
|
import contacts from './modules/contacts';
|
||||||
|
import contactConversations from './modules/contactConversations';
|
||||||
import conversationMetadata from './modules/conversationMetadata';
|
import conversationMetadata from './modules/conversationMetadata';
|
||||||
import conversations from './modules/conversations';
|
import conversations from './modules/conversations';
|
||||||
import inboxes from './modules/inboxes';
|
import inboxes from './modules/inboxes';
|
||||||
|
@ -22,6 +23,7 @@ export default new Vuex.Store({
|
||||||
cannedResponse,
|
cannedResponse,
|
||||||
Channel,
|
Channel,
|
||||||
contacts,
|
contacts,
|
||||||
|
contactConversations,
|
||||||
conversationMetadata,
|
conversationMetadata,
|
||||||
conversations,
|
conversations,
|
||||||
inboxes,
|
inboxes,
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import * as types from '../mutation-types';
|
||||||
|
import ContactAPI from '../../api/contacts';
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
records: {},
|
||||||
|
uiFlags: {
|
||||||
|
isFetching: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
getUIFlags($state) {
|
||||||
|
return $state.uiFlags;
|
||||||
|
},
|
||||||
|
getContactConversation: $state => id => {
|
||||||
|
return $state.records[Number(id)] || [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
get: async ({ commit }, contactId) => {
|
||||||
|
commit(types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, {
|
||||||
|
isFetching: true,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const response = await ContactAPI.getConversations(contactId);
|
||||||
|
commit(types.default.SET_CONTACT_CONVERSATIONS, {
|
||||||
|
id: contactId,
|
||||||
|
data: response.data.payload,
|
||||||
|
});
|
||||||
|
commit(types.default.SET_ALL_CONVERSATION, response.data.payload, {
|
||||||
|
root: true,
|
||||||
|
});
|
||||||
|
commit(types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, {
|
||||||
|
isFetching: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
commit(types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, {
|
||||||
|
isFetching: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
[types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG]($state, data) {
|
||||||
|
$state.uiFlags = {
|
||||||
|
...$state.uiFlags,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[types.default.SET_CONTACT_CONVERSATIONS]: ($state, { id, data }) => {
|
||||||
|
Vue.set($state.records, id, data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
};
|
|
@ -0,0 +1,41 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { actions } from '../../contactConversations';
|
||||||
|
import * as types from '../../../mutation-types';
|
||||||
|
import conversationList 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: conversationList } });
|
||||||
|
await actions.get({ commit }, 1);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG, { isFetching: true }],
|
||||||
|
|
||||||
|
[
|
||||||
|
types.default.SET_CONTACT_CONVERSATIONS,
|
||||||
|
{ id: 1, data: conversationList },
|
||||||
|
],
|
||||||
|
[types.default.SET_ALL_CONVERSATION, conversationList, { root: true }],
|
||||||
|
[
|
||||||
|
types.default.SET_CONTACT_CONVERSATIONS_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_CONVERSATIONS_UI_FLAG, { isFetching: true }],
|
||||||
|
[
|
||||||
|
types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG,
|
||||||
|
{ isFetching: false },
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,82 @@
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
sender: {
|
||||||
|
id: 1,
|
||||||
|
name: 'sender1',
|
||||||
|
thumbnail: '',
|
||||||
|
channel: 'Channel::WebWidget',
|
||||||
|
},
|
||||||
|
assignee: null,
|
||||||
|
},
|
||||||
|
id: 1,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
content: 'Hello',
|
||||||
|
account_id: 1,
|
||||||
|
inbox_id: 1,
|
||||||
|
conversation_id: 1,
|
||||||
|
message_type: 1,
|
||||||
|
created_at: 1578555084,
|
||||||
|
updated_at: '2020-01-09T07:31:24.419Z',
|
||||||
|
private: false,
|
||||||
|
user_id: 1,
|
||||||
|
status: 'sent',
|
||||||
|
fb_id: null,
|
||||||
|
content_type: 'text',
|
||||||
|
content_attributes: {},
|
||||||
|
sender: {
|
||||||
|
name: 'Sender 1',
|
||||||
|
avatar_url: 'random_url',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inbox_id: 1,
|
||||||
|
status: 0,
|
||||||
|
timestamp: 1578555084,
|
||||||
|
user_last_seen_at: 0,
|
||||||
|
agent_last_seen_at: 1578555084,
|
||||||
|
unread_count: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
meta: {
|
||||||
|
sender: {
|
||||||
|
id: 2,
|
||||||
|
name: 'sender1',
|
||||||
|
thumbnail: '',
|
||||||
|
channel: 'Channel::WebWidget',
|
||||||
|
},
|
||||||
|
assignee: null,
|
||||||
|
},
|
||||||
|
id: 2,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
content: 'Hello',
|
||||||
|
account_id: 1,
|
||||||
|
inbox_id: 2,
|
||||||
|
conversation_id: 2,
|
||||||
|
message_type: 1,
|
||||||
|
created_at: 1578555084,
|
||||||
|
updated_at: '2020-01-09T07:31:24.419Z',
|
||||||
|
private: false,
|
||||||
|
user_id: 2,
|
||||||
|
status: 'sent',
|
||||||
|
fb_id: null,
|
||||||
|
content_type: 'text',
|
||||||
|
content_attributes: {},
|
||||||
|
sender: {
|
||||||
|
name: 'Sender 1',
|
||||||
|
avatar_url: 'random_url',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inbox_id: 2,
|
||||||
|
status: 0,
|
||||||
|
timestamp: 1578555084,
|
||||||
|
user_last_seen_at: 0,
|
||||||
|
agent_last_seen_at: 1578555084,
|
||||||
|
unread_count: 0,
|
||||||
|
},
|
||||||
|
];
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { getters } from '../../contactConversations';
|
||||||
|
|
||||||
|
describe('#getters', () => {
|
||||||
|
it('getContactConversation', () => {
|
||||||
|
const state = {
|
||||||
|
records: { 1: [{ id: 1, contact_id: 1, message: 'Hello' }] },
|
||||||
|
};
|
||||||
|
expect(getters.getContactConversation(state)(1)).toEqual([
|
||||||
|
{ id: 1, contact_id: 1, message: 'Hello' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getUIFlags', () => {
|
||||||
|
const state = {
|
||||||
|
uiFlags: {
|
||||||
|
isFetching: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(getters.getUIFlags(state)).toEqual({
|
||||||
|
isFetching: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
import * as types from '../../../mutation-types';
|
||||||
|
import { mutations } from '../../contactConversations';
|
||||||
|
|
||||||
|
describe('#mutations', () => {
|
||||||
|
describe('#SET_CONTACT_CONVERSATIONS_UI_FLAG', () => {
|
||||||
|
it('set ui flags', () => {
|
||||||
|
const state = { uiFlags: { isFetching: true } };
|
||||||
|
mutations[types.default.SET_CONTACT_CONVERSATIONS_UI_FLAG](state, {
|
||||||
|
isFetching: false,
|
||||||
|
});
|
||||||
|
expect(state.uiFlags).toEqual({
|
||||||
|
isFetching: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#SET_CONTACT_CONVERSATIONS', () => {
|
||||||
|
it('set contact conversation records', () => {
|
||||||
|
const state = { records: {} };
|
||||||
|
mutations[types.default.SET_CONTACT_CONVERSATIONS](state, {
|
||||||
|
id: 1,
|
||||||
|
data: [{ id: 1, contact_id: 1, message: 'hello' }],
|
||||||
|
});
|
||||||
|
expect(state.records).toEqual({
|
||||||
|
1: [{ id: 1, contact_id: 1, message: 'hello' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -62,6 +62,10 @@ export default {
|
||||||
SET_CONTACTS: 'SET_CONTACTS',
|
SET_CONTACTS: 'SET_CONTACTS',
|
||||||
EDIT_CONTACT: 'EDIT_CONTACT',
|
EDIT_CONTACT: 'EDIT_CONTACT',
|
||||||
|
|
||||||
|
// Contact Conversation
|
||||||
|
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
||||||
|
SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS',
|
||||||
|
|
||||||
// Reports
|
// Reports
|
||||||
SET_ACCOUNT_REPORTS: 'SET_ACCOUNT_REPORTS',
|
SET_ACCOUNT_REPORTS: 'SET_ACCOUNT_REPORTS',
|
||||||
SET_ACCOUNT_SUMMARY: 'SET_ACCOUNT_SUMMARY',
|
SET_ACCOUNT_SUMMARY: 'SET_ACCOUNT_SUMMARY',
|
||||||
|
|
26
app/views/api/v1/contacts/conversations/index.json.jbuilder
Normal file
26
app/views/api/v1/contacts/conversations/index.json.jbuilder
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
json.payload do
|
||||||
|
json.array! @conversations do |conversation|
|
||||||
|
json.meta do
|
||||||
|
json.sender do
|
||||||
|
json.id conversation.contact.id
|
||||||
|
json.name conversation.contact.name
|
||||||
|
json.thumbnail conversation.contact.avatar_url
|
||||||
|
json.channel conversation.inbox.try(:channel_type)
|
||||||
|
end
|
||||||
|
json.assignee conversation.assignee
|
||||||
|
end
|
||||||
|
|
||||||
|
json.id conversation.display_id
|
||||||
|
if conversation.unread_incoming_messages.count.zero?
|
||||||
|
json.messages [conversation.messages.last.try(:push_event_data)]
|
||||||
|
else
|
||||||
|
json.messages conversation.unread_messages.map(&:push_event_data)
|
||||||
|
end
|
||||||
|
json.inbox_id conversation.inbox_id
|
||||||
|
json.status conversation.status_before_type_cast
|
||||||
|
json.timestamp conversation.messages.last.try(:created_at).try(:to_i)
|
||||||
|
json.user_last_seen_at conversation.user_last_seen_at.to_i
|
||||||
|
json.agent_last_seen_at conversation.agent_last_seen_at.to_i
|
||||||
|
json.unread_count conversation.unread_incoming_messages.count
|
||||||
|
end
|
||||||
|
end
|
|
@ -37,7 +37,6 @@ Rails.application.routes.draw do
|
||||||
resources :accounts, only: [:create]
|
resources :accounts, only: [:create]
|
||||||
resources :inboxes, only: [:index, :destroy]
|
resources :inboxes, only: [:index, :destroy]
|
||||||
resources :agents, except: [:show, :edit, :new]
|
resources :agents, except: [:show, :edit, :new]
|
||||||
resources :contacts, only: [:index, :show, :update, :create]
|
|
||||||
resources :labels, only: [:index]
|
resources :labels, only: [:index]
|
||||||
resources :canned_responses, except: [:show, :edit, :new]
|
resources :canned_responses, except: [:show, :edit, :new]
|
||||||
resources :inbox_members, only: [:create, :show], param: :inbox_id
|
resources :inbox_members, only: [:create, :show], param: :inbox_id
|
||||||
|
@ -73,6 +72,12 @@ Rails.application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :contacts, only: [:index, :show, :update, :create] do
|
||||||
|
scope module: :contacts do
|
||||||
|
resources :conversations, only: [:index]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# this block is only required if subscription via chargebee is enabled
|
# this block is only required if subscription via chargebee is enabled
|
||||||
if ENV['BILLING_ENABLED']
|
if ENV['BILLING_ENABLED']
|
||||||
resources :subscriptions, only: [:index] do
|
resources :subscriptions, only: [:index] do
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe '/api/v1/contacts/:id/conversations', type: :request do
|
||||||
|
let(:account) { create(:account) }
|
||||||
|
let(:contact) { create(:contact, account: account) }
|
||||||
|
let(:inbox_1) { create(:inbox, account: account) }
|
||||||
|
let(:inbox_2) { create(:inbox, account: account) }
|
||||||
|
let(:contact_inbox_1) { create(:contact_inbox, contact: contact, inbox: inbox_1) }
|
||||||
|
let(:contact_inbox_2) { create(:contact_inbox, contact: contact, inbox: inbox_2) }
|
||||||
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||||
|
let(:agent) { create(:user, account: account, role: :agent) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:inbox_member, user: agent, inbox: inbox_1)
|
||||||
|
2.times.each { create(:conversation, account: account, inbox: inbox_1, contact: contact, contact_inbox: contact_inbox_1) }
|
||||||
|
2.times.each { create(:conversation, account: account, inbox: inbox_2, contact: contact, contact_inbox: contact_inbox_2) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'GET /api/v1/contacts/:id/conversations' do
|
||||||
|
context 'when unauthenticated user' do
|
||||||
|
it 'returns unauthorized' do
|
||||||
|
get "/api/v1/contacts/#{contact.id}/conversations"
|
||||||
|
expect(response).to have_http_status(:unauthorized)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when user is logged in' do
|
||||||
|
context 'with user as administrator' do
|
||||||
|
it 'returns conversations from all inboxes' do
|
||||||
|
get "/api/v1/contacts/#{contact.id}/conversations", headers: admin.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
|
||||||
|
expect(json_response['payload'].length).to eq 4
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with user as agent' do
|
||||||
|
it 'returns conversations from the inboxes which agent has access to' do
|
||||||
|
get "/api/v1/contacts/#{contact.id}/conversations", headers: agent.create_new_auth_token
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
|
||||||
|
expect(json_response['payload'].length).to eq 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue