feat: Add a view for mentions (#3505)

- Added a new table mentions for saving user mentions
- Added a filter conversation_type in the API
- Added a view to see the mentions
This commit is contained in:
Pranav Raj S
2021-12-08 21:50:14 -08:00
committed by GitHub
parent 1db82f235d
commit 2be71e73dc
28 changed files with 389 additions and 98 deletions

View File

@@ -6,7 +6,15 @@ class ConversationApi extends ApiClient {
super('conversations', { accountScoped: true });
}
get({ inboxId, status, assigneeType, page, labels, teamId }) {
get({
inboxId,
status,
assigneeType,
page,
labels,
teamId,
conversationType,
}) {
return axios.get(this.url, {
params: {
inbox_id: inboxId,
@@ -15,6 +23,7 @@ class ConversationApi extends ApiClient {
assignee_type: assigneeType,
page,
labels,
conversation_type: conversationType,
},
});
}
@@ -74,7 +83,7 @@ class ConversationApi extends ApiClient {
return axios.post(`${this.url}/${conversationId}/unmute`);
}
meta({ inboxId, status, assigneeType, labels, teamId }) {
meta({ inboxId, status, assigneeType, labels, teamId, conversationType }) {
return axios.get(`${this.url}/meta`, {
params: {
inbox_id: inboxId,
@@ -82,6 +91,7 @@ class ConversationApi extends ApiClient {
assignee_type: assigneeType,
labels,
team_id: teamId,
conversation_type: conversationType,
},
});
}

View File

@@ -51,6 +51,7 @@ l<template>
:active-label="label"
:team-id="teamId"
:chat="chat"
:conversation-type="conversationType"
:show-assignee="showAssigneeInConversationCard"
/>
@@ -133,6 +134,10 @@ export default {
type: String,
default: '',
},
conversationType: {
type: String,
default: '',
},
},
data() {
return {
@@ -203,6 +208,9 @@ export default {
page: this.currentPage + 1,
labels: this.label ? [this.label] : undefined,
teamId: this.teamId ? this.teamId : undefined,
conversationType: this.conversationType
? this.conversationType
: undefined,
};
},
pageTitle() {
@@ -215,6 +223,9 @@ export default {
if (this.label) {
return `#${this.label}`;
}
if (this.conversationType === 'mention') {
return this.$t('CHAT_LIST.MENTION_HEADING');
}
return this.$t('CHAT_LIST.TAB_HEADING');
},
conversationList() {
@@ -251,6 +262,9 @@ export default {
label() {
this.resetAndFetchData();
},
conversationType() {
this.resetAndFetchData();
},
},
mounted() {
this.$store.dispatch('setChatFilter', this.activeStatus);

View File

@@ -12,6 +12,8 @@ const conversations = accountId => ({
'conversations_through_label',
'team_conversations',
'conversations_through_team',
'conversation_mentions',
'conversation_through_mentions',
],
menuItems: [
{
@@ -22,6 +24,13 @@ const conversations = accountId => ({
toolTip: 'Conversation from all subscribed inboxes',
toStateName: 'home',
},
{
icon: 'mention',
label: 'MENTIONED_CONVERSATIONS',
key: 'conversation_mentions',
toState: frontendURL(`accounts/${accountId}/mentions/conversations`),
toStateName: 'conversation_mentions',
},
],
});

View File

@@ -1,9 +1,9 @@
import { mount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import SidemenuIcon from '../SidemenuIcon';
describe('SidemenuIcon', () => {
test('matches snapshot', () => {
const wrapper = mount(SidemenuIcon);
const wrapper = shallowMount(SidemenuIcon);
expect(wrapper.vm).toBeTruthy();
expect(wrapper.element).toMatchSnapshot();
});

View File

@@ -133,6 +133,10 @@ export default {
type: Boolean,
default: false,
},
conversationType: {
type: String,
default: '',
},
},
computed: {
@@ -243,6 +247,7 @@ export default {
id: chat.id,
label: this.activeLabel,
teamId: this.teamId,
conversationType: this.conversationType,
});
router.push({ path: frontendURL(path) });
},

View File

@@ -5,7 +5,7 @@ import VTooltip from 'v-tooltip';
import Button from 'dashboard/components/buttons/Button';
import i18n from 'dashboard/i18n';
import FluentIcon from 'shared/components/FluentIcon/DashboardIcon';
import MoreActions from '../MoreActions';
const localVue = createLocalVue();
@@ -13,6 +13,7 @@ localVue.use(Vuex);
localVue.use(VueI18n);
localVue.use(VTooltip);
localVue.component('fluent-icon', FluentIcon);
localVue.component('woot-button', Button);
const i18nConfig = new VueI18n({

View File

@@ -11,17 +11,19 @@ export const conversationUrl = ({
id,
label,
teamId,
conversationType = '',
}) => {
let url = `accounts/${accountId}/conversations/${id}`;
if (activeInbox) {
return `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}`;
url = `accounts/${accountId}/inbox/${activeInbox}/conversations/${id}`;
} else if (label) {
url = `accounts/${accountId}/label/${label}/conversations/${id}`;
} else if (teamId) {
url = `accounts/${accountId}/team/${teamId}/conversations/${id}`;
} else if (conversationType === 'mention') {
url = `accounts/${accountId}/mentions/conversations/${id}`;
}
if (label) {
return `accounts/${accountId}/label/${label}/conversations/${id}`;
}
if (teamId) {
return `accounts/${accountId}/team/${teamId}/conversations/${id}`;
}
return `accounts/${accountId}/conversations/${id}`;
return url;
};
export const accountIdFromPathname = pathname => {

View File

@@ -21,6 +21,7 @@ class ActionCableConnector extends BaseActionCableConnector {
'presence.update': this.onPresenceUpdate,
'contact.deleted': this.onContactDelete,
'contact.updated': this.onContactUpdate,
'conversation.mentioned': this.onConversationMentioned,
};
}
@@ -97,6 +98,10 @@ class ActionCableConnector extends BaseActionCableConnector {
});
};
onConversationMentioned = data => {
this.app.$store.dispatch('addMentions', data);
};
clearTimer = conversationId => {
const timerEvent = this.CancelTyping[conversationId];

View File

@@ -7,6 +7,7 @@
"404": "There are no active conversations in this group."
},
"TAB_HEADING": "Conversations",
"MENTION_HEADING": "Mentions",
"SEARCH": {
"INPUT": "Search for People, Chats, Saved Replies .."
},

View File

@@ -136,6 +136,7 @@
"SIDEBAR": {
"CONVERSATIONS": "Conversations",
"ALL_CONVERSATIONS": "All Conversations",
"MENTIONED_CONVERSATIONS": "Mentions",
"REPORTS": "Reports",
"SETTINGS": "Settings",
"CONTACTS": "Contacts",

View File

@@ -4,6 +4,7 @@
:conversation-inbox="inboxId"
:label="label"
:team-id="teamId"
:conversation-type="conversationType"
@conversation-load="onConversationLoad"
>
<pop-over-search />
@@ -49,6 +50,10 @@ export default {
type: String,
default: '',
},
conversationType: {
type: String,
default: '',
},
},
data() {
return {

View File

@@ -13,15 +13,6 @@ export default {
return { inboxId: 0 };
},
},
{
path: frontendURL('accounts/:accountId/inbox/:inbox_id'),
name: 'inbox_dashboard',
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => {
return { inboxId: route.params.inbox_id };
},
},
{
path: frontendURL('accounts/:accountId/conversations/:conversation_id'),
name: 'inbox_conversation',
@@ -31,6 +22,15 @@ export default {
return { inboxId: 0, conversationId: route.params.conversation_id };
},
},
{
path: frontendURL('accounts/:accountId/inbox/:inbox_id'),
name: 'inbox_dashboard',
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => {
return { inboxId: route.params.inbox_id };
},
},
{
path: frontendURL(
'accounts/:accountId/inbox/:inbox_id/conversations/:conversation_id'
@@ -83,5 +83,24 @@ export default {
teamId: route.params.teamId,
}),
},
{
path: frontendURL('accounts/:accountId/mentions/conversations'),
name: 'conversation_mentions',
roles: ['administrator', 'agent'],
component: ConversationView,
props: () => ({ conversationType: 'mention' }),
},
{
path: frontendURL(
'accounts/:accountId/mentions/conversations/:conversationId'
),
name: 'conversation_through_mentions',
roles: ['administrator', 'agent'],
component: ConversationView,
props: route => ({
conversationId: route.params.conversationId,
conversationType: 'mention',
}),
},
],
};

View File

@@ -4,43 +4,11 @@ import ConversationApi from '../../../api/inbox/conversation';
import MessageApi from '../../../api/inbox/message';
import { MESSAGE_STATUS, MESSAGE_TYPE } from 'shared/constants/messages';
import { createPendingMessage } from 'dashboard/helper/commons';
import {
buildConversationList,
isOnMentionsView,
} from './helpers/actionHelpers';
const setPageFilter = ({ dispatch, filter, page, markEndReached }) => {
dispatch('conversationPage/setCurrentPage', { filter, page }, { root: true });
if (markEndReached) {
dispatch('conversationPage/setEndReached', { filter }, { root: true });
}
};
const setContacts = (commit, chatList) => {
commit(
`contacts/${types.SET_CONTACTS}`,
chatList.map(chat => chat.meta.sender)
);
};
const buildConversationList = (
context,
requestPayload,
responseData,
filterType
) => {
const { payload: conversationList, meta: metaData } = responseData;
context.commit(types.SET_ALL_CONVERSATION, conversationList);
context.dispatch('conversationStats/set', metaData);
context.dispatch(
'conversationLabels/setBulkConversationLabels',
conversationList
);
context.commit(types.CLEAR_LIST_LOADING_STATUS);
setContacts(context.commit, conversationList);
setPageFilter({
dispatch: context.dispatch,
filter: filterType,
page: requestPayload.page,
markEndReached: !conversationList.length,
});
};
// actions
const actions = {
getConversation: async ({ commit }, conversationId) => {
@@ -233,21 +201,32 @@ const actions = {
}
},
addConversation({ commit, state, dispatch }, conversation) {
addConversation({ commit, state, dispatch, rootState }, conversation) {
const { currentInbox, appliedFilters } = state;
const {
inbox_id: inboxId,
meta: { sender },
} = conversation;
const hasAppliedFilters = !!appliedFilters.length;
const isMatchingInboxFilter =
!currentInbox || Number(currentInbox) === inboxId;
if (!hasAppliedFilters && isMatchingInboxFilter) {
if (
!hasAppliedFilters &&
!isOnMentionsView(rootState) &&
isMatchingInboxFilter
) {
commit(types.ADD_CONVERSATION, conversation);
dispatch('contacts/setContact', sender);
}
},
addMentions({ dispatch, rootState }, conversation) {
if (isOnMentionsView(rootState)) {
dispatch('updateConversation', conversation);
}
},
updateConversation({ commit, dispatch }, conversation) {
const {
meta: { sender },

View File

@@ -0,0 +1,46 @@
import types from '../../../mutation-types';
export const setPageFilter = ({ dispatch, filter, page, markEndReached }) => {
dispatch('conversationPage/setCurrentPage', { filter, page }, { root: true });
if (markEndReached) {
dispatch('conversationPage/setEndReached', { filter }, { root: true });
}
};
export const setContacts = (commit, chatList) => {
commit(
`contacts/${types.SET_CONTACTS}`,
chatList.map(chat => chat.meta.sender)
);
};
export const isOnMentionsView = ({ route: { name: routeName } }) => {
const MENTION_ROUTES = [
'conversation_mentions',
'conversation_through_mentions',
];
return MENTION_ROUTES.includes(routeName);
};
export const buildConversationList = (
context,
requestPayload,
responseData,
filterType
) => {
const { payload: conversationList, meta: metaData } = responseData;
context.commit(types.SET_ALL_CONVERSATION, conversationList);
context.dispatch('conversationStats/set', metaData);
context.dispatch(
'conversationLabels/setBulkConversationLabels',
conversationList
);
context.commit(types.CLEAR_LIST_LOADING_STATUS);
setContacts(context.commit, conversationList);
setPageFilter({
dispatch: context.dispatch,
filter: filterType,
page: requestPayload.page,
markEndReached: !conversationList.length,
});
};

View File

@@ -0,0 +1,12 @@
import { isOnMentionsView } from '../actionHelpers';
describe('#isOnMentionsView', () => {
it('return valid responses when passing the state', () => {
expect(isOnMentionsView({ route: { name: 'conversation_mentions' } })).toBe(
true
);
expect(isOnMentionsView({ route: { name: 'conversation_messages' } })).toBe(
false
);
});
});

View File

@@ -59,7 +59,10 @@ describe('#actions', () => {
messages: [],
meta: { sender: { id: 1, name: 'john-doe' } },
};
actions.updateConversation({ commit, dispatch }, conversation);
actions.updateConversation(
{ commit, rootState: { route: { name: 'home' } }, dispatch },
conversation
);
expect(commit.mock.calls).toEqual([
[types.UPDATE_CONVERSATION, conversation],
]);
@@ -86,6 +89,7 @@ describe('#actions', () => {
actions.addConversation(
{
commit,
rootState: { route: { name: 'home' } },
dispatch,
state: { currentInbox: 1, appliedFilters: [] },
},
@@ -105,6 +109,27 @@ describe('#actions', () => {
actions.addConversation(
{
commit,
rootState: { route: { name: 'home' } },
dispatch,
state: { currentInbox: 1, appliedFilters: [{ id: 'random-filter' }] },
},
conversation
);
expect(commit.mock.calls).toEqual([]);
expect(dispatch.mock.calls).toEqual([]);
});
it('doesnot send mutation if the view is conversation mentions', () => {
const conversation = {
id: 1,
messages: [],
meta: { sender: { id: 1, name: 'john-doe' } },
inbox_id: 1,
};
actions.addConversation(
{
commit,
rootState: { route: { name: 'conversation_mentions' } },
dispatch,
state: { currentInbox: 1, appliedFilters: [{ id: 'random-filter' }] },
},
@@ -124,6 +149,7 @@ describe('#actions', () => {
actions.addConversation(
{
commit,
rootState: { route: { name: 'home' } },
dispatch,
state: { currentInbox: 1, appliedFilters: [] },
},
@@ -151,7 +177,12 @@ describe('#actions', () => {
inbox_id: 1,
};
actions.addConversation(
{ commit, dispatch, state: { appliedFilters: [] } },
{
commit,
rootState: { route: { name: 'home' } },
dispatch,
state: { appliedFilters: [] },
},
conversation
);
expect(commit.mock.calls).toEqual([
@@ -379,3 +410,27 @@ describe('#deleteMessage', () => {
});
});
});
describe('#addMentions', () => {
it('does not send mutations if the view is not mentions', () => {
actions.addMentions(
{ commit, dispatch, rootState: { route: { name: 'home' } } },
{ id: 1 }
);
expect(commit.mock.calls).toEqual([]);
expect(dispatch.mock.calls).toEqual([]);
});
it('send mutations if the view is mentions', () => {
actions.addMentions(
{
dispatch,
rootState: { route: { name: 'conversation_mentions' } },
},
{ id: 1, meta: { sender: { id: 1 } } }
);
expect(dispatch.mock.calls).toEqual([
['updateConversation', { id: 1, meta: { sender: { id: 1 } } }],
]);
});
});