Feat: Assign conversations to teams. (#1849)
This commit is contained in:
parent
941d4219f0
commit
c99c63cd79
10 changed files with 231 additions and 12 deletions
|
@ -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`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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](
|
||||||
|
|
|
@ -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' }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue