Feature: View conversation labels on sidebar (#436)
This commit is contained in:
parent
90e678743b
commit
a2b025b548
13 changed files with 282 additions and 17 deletions
18
app/javascript/dashboard/api/conversations.js
Normal file
18
app/javascript/dashboard/api/conversations.js
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/* global axios */
|
||||||
|
import ApiClient from './ApiClient';
|
||||||
|
|
||||||
|
class ConversationApi extends ApiClient {
|
||||||
|
constructor() {
|
||||||
|
super('conversations');
|
||||||
|
}
|
||||||
|
|
||||||
|
getLabels(conversationID) {
|
||||||
|
return axios.get(`${this.url}/${conversationID}/labels`);
|
||||||
|
}
|
||||||
|
|
||||||
|
createLabels(conversationID) {
|
||||||
|
return axios.get(`${this.url}/${conversationID}/labels`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ConversationApi();
|
15
app/javascript/dashboard/api/specs/conversations.spec.js
Normal file
15
app/javascript/dashboard/api/specs/conversations.spec.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import conversations from '../conversations';
|
||||||
|
import ApiClient from '../ApiClient';
|
||||||
|
|
||||||
|
describe('#ConversationApi', () => {
|
||||||
|
it('creates correct instance', () => {
|
||||||
|
expect(conversations).toBeInstanceOf(ApiClient);
|
||||||
|
expect(conversations).toHaveProperty('get');
|
||||||
|
expect(conversations).toHaveProperty('show');
|
||||||
|
expect(conversations).toHaveProperty('create');
|
||||||
|
expect(conversations).toHaveProperty('update');
|
||||||
|
expect(conversations).toHaveProperty('delete');
|
||||||
|
expect(conversations).toHaveProperty('getLabels');
|
||||||
|
expect(conversations).toHaveProperty('createLabels');
|
||||||
|
});
|
||||||
|
});
|
|
@ -7,6 +7,10 @@
|
||||||
"CONVERSATIONS": {
|
"CONVERSATIONS": {
|
||||||
"NO_RECORDS_FOUND": "There are no previous conversations associated to this contact.",
|
"NO_RECORDS_FOUND": "There are no previous conversations associated to this contact.",
|
||||||
"TITLE": "Previous Conversations"
|
"TITLE": "Previous Conversations"
|
||||||
|
},
|
||||||
|
"LABELS": {
|
||||||
|
"NO_RECORDS_FOUND": "There are no labels associated to this conversation.",
|
||||||
|
"TITLE": "Labels"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,18 +59,22 @@
|
||||||
:contact-id="contact.id"
|
:contact-id="contact.id"
|
||||||
:conversation-id="conversationId"
|
:conversation-id="conversationId"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<conversation-labels :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 ContactConversations from './ContactConversations.vue';
|
import ContactConversations from './ContactConversations.vue';
|
||||||
|
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||||
|
import ConversationLabels from './ConversationLabels.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
ContactConversations,
|
ContactConversations,
|
||||||
ContactDetailsItem,
|
ContactDetailsItem,
|
||||||
|
ConversationLabels,
|
||||||
Thumbnail,
|
Thumbnail,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
<template>
|
||||||
|
<div class="contact-conversation--panel">
|
||||||
|
<contact-details-item
|
||||||
|
icon="ion-pricetags"
|
||||||
|
:title="$t('CONTACT_PANEL.LABELS.TITLE')"
|
||||||
|
/>
|
||||||
|
<div v-if="!uiFlags.isFetching">
|
||||||
|
<i v-if="!labels.length">
|
||||||
|
{{ $t('CONTACT_PANEL.LABELS.NO_RECORDS_FOUND') }}
|
||||||
|
</i>
|
||||||
|
<div v-else class="contact-conversation--list">
|
||||||
|
<span
|
||||||
|
v-for="label in labels"
|
||||||
|
:key="label"
|
||||||
|
class="conversation--label label primary"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<spinner v-else></spinner>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import Spinner from 'shared/components/Spinner.vue';
|
||||||
|
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ContactDetailsItem,
|
||||||
|
Spinner,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
conversationId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labels() {
|
||||||
|
return this.$store.getters['conversationLabels/getConversationLabels'](
|
||||||
|
this.conversationId
|
||||||
|
);
|
||||||
|
},
|
||||||
|
...mapGetters({
|
||||||
|
uiFlags: 'contactConversations/getUIFlags',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
conversationId(newConversationId, prevConversationId) {
|
||||||
|
if (newConversationId && newConversationId !== prevConversationId) {
|
||||||
|
this.$store.dispatch('conversationLabels/get', newConversationId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('conversationLabels/get', this.conversationId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation--label {
|
||||||
|
color: $color-white;
|
||||||
|
margin-right: $space-small;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
padding: $space-smaller;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -9,6 +9,7 @@ import Channel from './modules/channels';
|
||||||
import contacts from './modules/contacts';
|
import contacts from './modules/contacts';
|
||||||
import contactConversations from './modules/contactConversations';
|
import contactConversations from './modules/contactConversations';
|
||||||
import conversationMetadata from './modules/conversationMetadata';
|
import conversationMetadata from './modules/conversationMetadata';
|
||||||
|
import conversationLabels from './modules/conversationLabels';
|
||||||
import conversations from './modules/conversations';
|
import conversations from './modules/conversations';
|
||||||
import inboxes from './modules/inboxes';
|
import inboxes from './modules/inboxes';
|
||||||
import inboxMembers from './modules/inboxMembers';
|
import inboxMembers from './modules/inboxMembers';
|
||||||
|
@ -24,6 +25,7 @@ export default new Vuex.Store({
|
||||||
Channel,
|
Channel,
|
||||||
contacts,
|
contacts,
|
||||||
contactConversations,
|
contactConversations,
|
||||||
|
conversationLabels,
|
||||||
conversationMetadata,
|
conversationMetadata,
|
||||||
conversations,
|
conversations,
|
||||||
inboxes,
|
inboxes,
|
||||||
|
|
61
app/javascript/dashboard/store/modules/conversationLabels.js
Normal file
61
app/javascript/dashboard/store/modules/conversationLabels.js
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import * as types from '../mutation-types';
|
||||||
|
import ConversationAPI from '../../api/conversations';
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
records: {},
|
||||||
|
uiFlags: {
|
||||||
|
isFetching: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
getUIFlags($state) {
|
||||||
|
return $state.uiFlags;
|
||||||
|
},
|
||||||
|
getConversationLabels: $state => id => {
|
||||||
|
return $state.records[Number(id)] || [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
get: async ({ commit }, conversationId) => {
|
||||||
|
commit(types.default.SET_CONVERSATION_LABELS_UI_FLAG, {
|
||||||
|
isFetching: true,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const response = await ConversationAPI.getLabels(conversationId);
|
||||||
|
commit(types.default.SET_CONVERSATION_LABELS, {
|
||||||
|
id: conversationId,
|
||||||
|
data: response.data.payload,
|
||||||
|
});
|
||||||
|
commit(types.default.SET_CONVERSATION_LABELS_UI_FLAG, {
|
||||||
|
isFetching: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
commit(types.default.SET_CONVERSATION_LABELS_UI_FLAG, {
|
||||||
|
isFetching: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
[types.default.SET_CONVERSATION_LABELS_UI_FLAG]($state, data) {
|
||||||
|
$state.uiFlags = {
|
||||||
|
...$state.uiFlags,
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[types.default.SET_CONVERSATION_LABELS]: ($state, { id, data }) => {
|
||||||
|
Vue.set($state.records, id, data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
};
|
|
@ -0,0 +1,35 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { actions } from '../../conversationLabels';
|
||||||
|
import * as types from '../../../mutation-types';
|
||||||
|
|
||||||
|
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: ['customer-success', 'on-hold'] },
|
||||||
|
});
|
||||||
|
await actions.get({ commit }, 1);
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.default.SET_CONVERSATION_LABELS_UI_FLAG, { isFetching: true }],
|
||||||
|
|
||||||
|
[
|
||||||
|
types.default.SET_CONVERSATION_LABELS,
|
||||||
|
{ id: 1, data: ['customer-success', 'on-hold'] },
|
||||||
|
],
|
||||||
|
[types.default.SET_CONVERSATION_LABELS_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_CONVERSATION_LABELS_UI_FLAG, { isFetching: true }],
|
||||||
|
[types.default.SET_CONVERSATION_LABELS_UI_FLAG, { isFetching: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { getters } from '../../conversationLabels';
|
||||||
|
|
||||||
|
describe('#getters', () => {
|
||||||
|
it('getConversationLabels', () => {
|
||||||
|
const state = {
|
||||||
|
records: { 1: ['customer-success', 'on-hold'] },
|
||||||
|
};
|
||||||
|
expect(getters.getConversationLabels(state)(1)).toEqual([
|
||||||
|
'customer-success',
|
||||||
|
'on-hold',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
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 '../../conversationLabels';
|
||||||
|
|
||||||
|
describe('#mutations', () => {
|
||||||
|
describe('#SET_CONVERSATION_LABELS_UI_FLAG', () => {
|
||||||
|
it('set ui flags', () => {
|
||||||
|
const state = { uiFlags: { isFetching: true } };
|
||||||
|
mutations[types.default.SET_CONVERSATION_LABELS_UI_FLAG](state, {
|
||||||
|
isFetching: false,
|
||||||
|
});
|
||||||
|
expect(state.uiFlags).toEqual({
|
||||||
|
isFetching: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#SET_CONVERSATION_LABELS', () => {
|
||||||
|
it('set contact conversation records', () => {
|
||||||
|
const state = { records: {} };
|
||||||
|
mutations[types.default.SET_CONVERSATION_LABELS](state, {
|
||||||
|
id: 1,
|
||||||
|
data: ['customer-success', 'on-hold'],
|
||||||
|
});
|
||||||
|
expect(state.records).toEqual({
|
||||||
|
1: ['customer-success', 'on-hold'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -66,6 +66,10 @@ export default {
|
||||||
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
||||||
SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS',
|
SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS',
|
||||||
|
|
||||||
|
// Conversation Label
|
||||||
|
SET_CONVERSATION_LABELS_UI_FLAG: 'SET_CONVERSATION_LABELS_UI_FLAG',
|
||||||
|
SET_CONVERSATION_LABELS: 'SET_CONVERSATION_LABELS',
|
||||||
|
|
||||||
// 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',
|
||||||
|
|
|
@ -1,8 +1 @@
|
||||||
json.data do
|
json.payload @labels
|
||||||
json.meta do
|
|
||||||
end
|
|
||||||
|
|
||||||
json.payload do
|
|
||||||
json.labels @labels
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
|
@ -1,8 +1 @@
|
||||||
json.data do
|
json.payload @labels
|
||||||
json.meta do
|
|
||||||
end
|
|
||||||
|
|
||||||
json.payload do
|
|
||||||
json.labels @labels
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
Loading…
Reference in a new issue