Feat: Assign conversations to teams. (#1849)

This commit is contained in:
Nithin David Thomas 2021-03-15 18:37:03 +05:30 committed by GitHub
parent 941d4219f0
commit c99c63cd79
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 231 additions and 12 deletions

View file

@ -33,12 +33,17 @@ class ConversationApi extends ApiClient {
} }
assignAgent({ conversationId, agentId }) { assignAgent({ conversationId, agentId }) {
axios.post( return axios.post(
`${this.url}/${conversationId}/assignments?assignee_id=${agentId}`, `${this.url}/${conversationId}/assignments?assignee_id=${agentId}`,
{} {}
); );
} }
assignTeam({ conversationId, teamId }) {
const params = { team_id: teamId };
return axios.post(`${this.url}/${conversationId}/assignments`, params);
}
markMessageRead({ id }) { markMessageRead({ id }) {
return axios.post(`${this.url}/${id}/update_last_seen`); return axios.post(`${this.url}/${id}/update_last_seen`);
} }

View file

@ -11,6 +11,7 @@ describe('#ConversationAPI', () => {
expect(conversationAPI).toHaveProperty('delete'); expect(conversationAPI).toHaveProperty('delete');
expect(conversationAPI).toHaveProperty('toggleStatus'); expect(conversationAPI).toHaveProperty('toggleStatus');
expect(conversationAPI).toHaveProperty('assignAgent'); expect(conversationAPI).toHaveProperty('assignAgent');
expect(conversationAPI).toHaveProperty('assignTeam');
expect(conversationAPI).toHaveProperty('markMessageRead'); expect(conversationAPI).toHaveProperty('markMessageRead');
expect(conversationAPI).toHaveProperty('toggleTyping'); expect(conversationAPI).toHaveProperty('toggleTyping');
expect(conversationAPI).toHaveProperty('mute'); expect(conversationAPI).toHaveProperty('mute');

View file

@ -16,7 +16,7 @@
margin-bottom: var(--space-normal); margin-bottom: var(--space-normal);
.multiselect--active { .multiselect--active {
> .multiselect__tags { >.multiselect__tags {
border-color: $color-woot; border-color: $color-woot;
} }
} }
@ -124,6 +124,7 @@
} }
.sidebar-labels-wrap { .sidebar-labels-wrap {
&.has-edited, &.has-edited,
&:hover { &:hover {
.multiselect { .multiselect {
@ -132,16 +133,48 @@
} }
.multiselect { .multiselect {
> .multiselect__select { >.multiselect__select {
visibility: hidden; visibility: hidden;
} }
> .multiselect__tags { >.multiselect__tags {
border-color: transparent; border-color: transparent;
} }
&.multiselect--active > .multiselect__tags { &.multiselect--active>.multiselect__tags {
border-color: $color-woot; border-color: $color-woot;
} }
} }
} }
.multiselect-wrap--small {
$multiselect-height: 3.8rem;
.multiselect__tags,
.multiselect__input,
.multiselect {
background: var(--white);
font-size: var(--font-size-small);
height: $multiselect-height;
min-height: $multiselect-height;
}
.multiselect__input {
height: $multiselect-height - $space-micro;
min-height: $multiselect-height - $space-micro;
}
.multiselect__single {
font-size: var(--font-size-small);
padding: var(--space-small) 0;
}
.multiselect__placeholder {
padding: var(--space-small) 0;
}
.multiselect__select {
min-height: $multiselect-height;
}
}

View file

@ -51,6 +51,7 @@
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team", "VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
"CHANGE_STATUS": "Conversation status changed", "CHANGE_STATUS": "Conversation status changed",
"CHANGE_AGENT": "Conversation Assignee changed", "CHANGE_AGENT": "Conversation Assignee changed",
"CHANGE_TEAM": "Conversation team changed",
"SENT_BY": "Sent by:", "SENT_BY": "Sent by:",
"ASSIGNMENT": { "ASSIGNMENT": {
"SELECT_AGENT": "Select Agent", "SELECT_AGENT": "Select Agent",
@ -98,5 +99,14 @@
"DESCRIPTION": "Labels provide an easier way to categorize your conversation. Create some labels like #support-enquiry, #billing-question etc., so that you can use them in a conversation later.", "DESCRIPTION": "Labels provide an easier way to categorize your conversation. Create some labels like #support-enquiry, #billing-question etc., so that you can use them in a conversation later.",
"NEW_LINK": "Click here to create tags" "NEW_LINK": "Click here to create tags"
} }
},
"CONVERSATION_SIDEBAR": {
"DETAILS_TITLE": "Conversations Details",
"ASSIGNEE_LABEL": "Assigned Agent",
"TEAM_LABEL": "Assigned Team",
"SELECT": {
"PLACEHOLDER": "None"
}
} }
} }

View file

@ -4,6 +4,43 @@
<i class="ion-chevron-right" /> <i class="ion-chevron-right" />
</span> </span>
<contact-info :contact="contact" :channel-type="channelType" /> <contact-info :contact="contact" :channel-type="channelType" />
<div class="conversation--actions">
<h4 class="sub-block-title">
{{ $t('CONVERSATION_SIDEBAR.DETAILS_TITLE') }}
</h4>
<div class="multiselect-wrap--small">
<label class="multiselect__label">
{{ $t('CONVERSATION_SIDEBAR.ASSIGNEE_LABEL') }}
</label>
<multiselect
v-model="assignedAgent"
:options="agentsList"
label="name"
track-by="id"
deselect-label=""
select-label=""
selected-label=""
:placeholder="$t('CONVERSATION_SIDEBAR.SELECT.PLACEHOLDER')"
:allow-empty="true"
/>
</div>
<div class="multiselect-wrap--small">
<label class="multiselect__label">
{{ $t('CONVERSATION_SIDEBAR.TEAM_LABEL') }}
</label>
<multiselect
v-model="assignedTeam"
:options="teamsList"
label="name"
track-by="id"
deselect-label=""
select-label=""
selected-label=""
:placeholder="$t('CONVERSATION_SIDEBAR.SELECT.PLACEHOLDER')"
:allow-empty="true"
/>
</div>
</div>
<div v-if="browser.browser_name" class="conversation--details"> <div v-if="browser.browser_name" class="conversation--details">
<contact-details-item <contact-details-item
v-if="location" v-if="location"
@ -67,6 +104,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import alertMixin from 'shared/mixins/alertMixin';
import ContactConversations from './ContactConversations.vue'; import ContactConversations from './ContactConversations.vue';
import ContactDetailsItem from './ContactDetailsItem.vue'; import ContactDetailsItem from './ContactDetailsItem.vue';
@ -83,6 +121,7 @@ export default {
ContactInfo, ContactInfo,
ConversationLabels, ConversationLabels,
}, },
mixins: [alertMixin],
props: { props: {
conversationId: { conversationId: {
type: [Number, String], type: [Number, String],
@ -96,6 +135,8 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
currentChat: 'getSelectedChat', currentChat: 'getSelectedChat',
agents: 'agents/getVerifiedAgents',
teams: 'teams/getTeams',
}), }),
currentConversationMetaData() { currentConversationMetaData() {
return this.$store.getters[ return this.$store.getters[
@ -159,6 +200,46 @@ export default {
contact() { contact() {
return this.$store.getters['contacts/getContact'](this.contactId); return this.$store.getters['contacts/getContact'](this.contactId);
}, },
agentsList() {
return [{ id: 0, name: 'None' }, ...this.agents];
},
teamsList() {
return [{ id: 0, name: 'None' }, ...this.teams];
},
assignedAgent: {
get() {
return this.currentChat.meta.assignee;
},
set(agent) {
const agentId = agent ? agent.id : 0;
this.$store.dispatch('setCurrentChatAssignee', agent);
this.$store
.dispatch('assignAgent', {
conversationId: this.currentChat.id,
agentId,
})
.then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_AGENT'));
});
},
},
assignedTeam: {
get() {
return this.currentChat.meta.team;
},
set(team) {
const teamId = team ? team.id : 0;
this.$store.dispatch('setCurrentChatTeam', team);
this.$store
.dispatch('assignTeam', {
conversationId: this.currentChat.id,
teamId,
})
.then(() => {
this.showAlert(this.$t('CONVERSATION.CHANGE_TEAM'));
});
},
},
}, },
watch: { watch: {
conversationId(newConversationId, prevConversationId) { conversationId(newConversationId, prevConversationId) {
@ -191,12 +272,10 @@ export default {
<style lang="scss" scoped> <style lang="scss" scoped>
@import '~dashboard/assets/scss/variables'; @import '~dashboard/assets/scss/variables';
@import '~dashboard/assets/scss/mixins';
.contact--panel { .contact--panel {
@include border-normal-left;
background: white; background: white;
border-left: 1px solid var(--color-border);
font-size: $font-size-small; font-size: $font-size-small;
overflow-y: auto; overflow-y: auto;
overflow: auto; overflow: auto;
@ -246,4 +325,16 @@ export default {
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
} }
.sub-block-title {
margin-bottom: var(--space-small);
}
.conversation--actions {
padding: 0 var(--space-normal) var(--space-small);
}
.multiselect__label {
margin-bottom: var(--space-smaller);
}
</style> </style>

View file

@ -103,18 +103,38 @@ const actions = {
} }
}, },
assignAgent: async ({ commit }, { conversationId, agentId }) => { assignAgent: async ({ dispatch }, { conversationId, agentId }) => {
try { try {
const response = await ConversationApi.assignAgent({ const response = await ConversationApi.assignAgent({
conversationId, conversationId,
agentId, agentId,
}); });
commit(types.default.ASSIGN_AGENT, response.data); dispatch('setCurrentChatAssignee', response.data);
} catch (error) { } catch (error) {
// Handle error // Handle error
} }
}, },
setCurrentChatAssignee({ commit }, assignee) {
commit(types.default.ASSIGN_AGENT, assignee);
},
assignTeam: async ({ dispatch }, { conversationId, teamId }) => {
try {
const response = await ConversationApi.assignTeam({
conversationId,
teamId,
});
dispatch('setCurrentChatTeam', response.data);
} catch (error) {
// Handle error
}
},
setCurrentChatTeam({ commit }, team) {
commit(types.default.ASSIGN_TEAM, team);
},
toggleStatus: async ({ commit }, data) => { toggleStatus: async ({ commit }, data) => {
try { try {
const response = await ConversationApi.toggleStatus(data); const response = await ConversationApi.toggleStatus(data);

View file

@ -61,7 +61,12 @@ export const mutations = {
[types.default.ASSIGN_AGENT](_state, assignee) { [types.default.ASSIGN_AGENT](_state, assignee) {
const [chat] = getSelectedChatConversation(_state); const [chat] = getSelectedChatConversation(_state);
chat.meta.assignee = assignee; Vue.set(chat.meta, 'assignee', assignee);
},
[types.default.ASSIGN_TEAM](_state, team) {
const [chat] = getSelectedChatConversation(_state);
Vue.set(chat.meta, 'team', team);
}, },
[types.default.RESOLVE_CONVERSATION](_state, status) { [types.default.RESOLVE_CONVERSATION](_state, status) {
@ -145,7 +150,7 @@ export const mutations = {
// Update assignee on action cable message // Update assignee on action cable message
[types.default.UPDATE_ASSIGNEE](_state, payload) { [types.default.UPDATE_ASSIGNEE](_state, payload) {
const [chat] = _state.allConversations.filter(c => c.id === payload.id); const [chat] = _state.allConversations.filter(c => c.id === payload.id);
chat.meta.assignee = payload.assignee; Vue.set(chat.meta, 'assignee', payload.assignee);
}, },
[types.default.UPDATE_CONVERSATION_CONTACT]( [types.default.UPDATE_CONVERSATION_CONTACT](

View file

@ -189,4 +189,52 @@ describe('#actions', () => {
expect(commit.mock.calls).toEqual([]); expect(commit.mock.calls).toEqual([]);
}); });
}); });
describe('#assignAgent', () => {
it('sends correct mutations if assignment is successful', async () => {
axios.post.mockResolvedValue({
data: { id: 1, name: 'User' },
});
await actions.assignAgent({ commit }, { conversationId: 1, agentId: 1 });
expect(commit).toHaveBeenCalledTimes(0);
expect(commit.mock.calls).toEqual([]);
});
});
describe('#setCurrentChatAssignee', () => {
it('sends correct mutations if assignment is successful', async () => {
axios.post.mockResolvedValue({
data: { id: 1, name: 'User' },
});
await actions.setCurrentChatAssignee({ commit }, { id: 1, name: 'User' });
expect(commit).toHaveBeenCalledTimes(1);
expect(commit.mock.calls).toEqual([
['ASSIGN_AGENT', { id: 1, name: 'User' }],
]);
});
});
describe('#assignTeam', () => {
it('sends correct mutations if assignment is successful', async () => {
axios.post.mockResolvedValue({
data: { id: 1, name: 'Team' },
});
await actions.assignTeam({ commit }, { conversationId: 1, teamId: 1 });
expect(commit).toHaveBeenCalledTimes(0);
expect(commit.mock.calls).toEqual([]);
});
});
describe('#setCurrentChatTeam', () => {
it('sends correct mutations if assignment is successful', async () => {
axios.post.mockResolvedValue({
data: { id: 1, name: 'Team' },
});
await actions.setCurrentChatTeam({ commit }, { id: 1, name: 'Team' });
expect(commit).toHaveBeenCalledTimes(1);
expect(commit.mock.calls).toEqual([
['ASSIGN_TEAM', { id: 1, name: 'Team' }],
]);
});
});
}); });

View file

@ -28,6 +28,7 @@ export default {
MUTE_CONVERSATION: 'MUTE_CONVERSATION', MUTE_CONVERSATION: 'MUTE_CONVERSATION',
UNMUTE_CONVERSATION: 'UNMUTE_CONVERSATION', UNMUTE_CONVERSATION: 'UNMUTE_CONVERSATION',
ASSIGN_AGENT: 'ASSIGN_AGENT', ASSIGN_AGENT: 'ASSIGN_AGENT',
ASSIGN_TEAM: 'ASSIGN_TEAM',
SET_CHAT_META: 'SET_CHAT_META', SET_CHAT_META: 'SET_CHAT_META',
ADD_MESSAGE: 'ADD_MESSAGE', ADD_MESSAGE: 'ADD_MESSAGE',
ADD_PENDING_MESSAGE: 'ADD_PENDING_MESSAGE', ADD_PENDING_MESSAGE: 'ADD_PENDING_MESSAGE',

View file

@ -8,6 +8,11 @@ json.meta do
json.partial! 'api/v1/models/agent.json.jbuilder', resource: conversation.assignee json.partial! 'api/v1/models/agent.json.jbuilder', resource: conversation.assignee
end end
end end
if conversation.team.present?
json.team do
json.partial! 'api/v1/models/team.json.jbuilder', resource: conversation.team
end
end
end end
json.id conversation.display_id json.id conversation.display_id