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": {
|
||||
"NO_RECORDS_FOUND": "There are no previous conversations associated to this contact.",
|
||||
"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"
|
||||
:conversation-id="conversationId"
|
||||
/>
|
||||
|
||||
<conversation-labels :conversation-id="conversationId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||
import ContactConversations from './ContactConversations.vue';
|
||||
import ContactDetailsItem from './ContactDetailsItem.vue';
|
||||
import ConversationLabels from './ConversationLabels.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactConversations,
|
||||
ContactDetailsItem,
|
||||
ConversationLabels,
|
||||
Thumbnail,
|
||||
},
|
||||
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 contactConversations from './modules/contactConversations';
|
||||
import conversationMetadata from './modules/conversationMetadata';
|
||||
import conversationLabels from './modules/conversationLabels';
|
||||
import conversations from './modules/conversations';
|
||||
import inboxes from './modules/inboxes';
|
||||
import inboxMembers from './modules/inboxMembers';
|
||||
|
@ -24,6 +25,7 @@ export default new Vuex.Store({
|
|||
Channel,
|
||||
contacts,
|
||||
contactConversations,
|
||||
conversationLabels,
|
||||
conversationMetadata,
|
||||
conversations,
|
||||
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: 'SET_CONTACT_CONVERSATIONS',
|
||||
|
||||
// Conversation Label
|
||||
SET_CONVERSATION_LABELS_UI_FLAG: 'SET_CONVERSATION_LABELS_UI_FLAG',
|
||||
SET_CONVERSATION_LABELS: 'SET_CONVERSATION_LABELS',
|
||||
|
||||
// Reports
|
||||
SET_ACCOUNT_REPORTS: 'SET_ACCOUNT_REPORTS',
|
||||
SET_ACCOUNT_SUMMARY: 'SET_ACCOUNT_SUMMARY',
|
||||
|
|
|
@ -1,8 +1 @@
|
|||
json.data do
|
||||
json.meta do
|
||||
end
|
||||
|
||||
json.payload do
|
||||
json.labels @labels
|
||||
end
|
||||
end
|
||||
json.payload @labels
|
||||
|
|
|
@ -1,8 +1 @@
|
|||
json.data do
|
||||
json.meta do
|
||||
end
|
||||
|
||||
json.payload do
|
||||
json.labels @labels
|
||||
end
|
||||
end
|
||||
json.payload @labels
|
||||
|
|
Loading…
Reference in a new issue