chore: Enable Participating tab for conversations (#11714)
## Summary This PR enables the **Participating** conversation view in the main sidebar and keeps the behavior aligned with existing conversation views. ## What changed - Added **Participating** under Conversations in the new sidebar. - Added a guard in conversation realtime `addConversation` flow so generic `conversation.created` events are not injected while the user is on Participating view. - Added participating route mapping in conversation-list redirect helper so list redirects resolve correctly to `/participating/conversations`. ## Scope notes - Kept changes minimal and consistent with current `develop` behavior. - No additional update-event filtering was added beyond what existing views already do. --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com>
This commit is contained in:
@@ -250,6 +250,12 @@ const menuItems = computed(() => {
|
||||
activeOn: ['conversation_through_mentions'],
|
||||
to: accountScopedRoute('conversation_mentions'),
|
||||
},
|
||||
{
|
||||
name: 'Participating',
|
||||
label: t('SIDEBAR.PARTICIPATING_CONVERSATIONS'),
|
||||
activeOn: ['conversation_through_participating'],
|
||||
to: accountScopedRoute('conversation_participating'),
|
||||
},
|
||||
{
|
||||
name: 'Unattended',
|
||||
activeOn: ['conversation_through_unattended'],
|
||||
|
||||
@@ -56,6 +56,7 @@ import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHe
|
||||
import { conversationListPageURL } from '../helper/URLHelper';
|
||||
import {
|
||||
isOnMentionsView,
|
||||
isOnParticipatingView,
|
||||
isOnUnattendedView,
|
||||
} from '../store/modules/conversations/helpers/actionHelpers';
|
||||
import {
|
||||
@@ -113,6 +114,7 @@ const chatLists = useMapGetter('getFilteredConversations');
|
||||
const mineChatsList = useMapGetter('getMineChats');
|
||||
const allChatList = useMapGetter('getAllStatusChats');
|
||||
const unAssignedChatsList = useMapGetter('getUnAssignedChats');
|
||||
const participatingChatsList = useMapGetter('getParticipatingChats');
|
||||
const chatListLoading = useMapGetter('getChatListLoadingStatus');
|
||||
const activeInbox = useMapGetter('getSelectedInbox');
|
||||
const conversationStats = useMapGetter('conversationStats/getStats');
|
||||
@@ -296,13 +298,15 @@ const pageTitle = computed(() => {
|
||||
if (props.label) {
|
||||
return `#${props.label}`;
|
||||
}
|
||||
if (props.conversationType === 'mention') {
|
||||
if (props.conversationType === wootConstants.CONVERSATION_TYPE.MENTION) {
|
||||
return t('CHAT_LIST.MENTION_HEADING');
|
||||
}
|
||||
if (props.conversationType === 'participating') {
|
||||
if (
|
||||
props.conversationType === wootConstants.CONVERSATION_TYPE.PARTICIPATING
|
||||
) {
|
||||
return t('CONVERSATION_PARTICIPANTS.SIDEBAR_MENU_TITLE');
|
||||
}
|
||||
if (props.conversationType === 'unattended') {
|
||||
if (props.conversationType === wootConstants.CONVERSATION_TYPE.UNATTENDED) {
|
||||
return t('CHAT_LIST.UNATTENDED_HEADING');
|
||||
}
|
||||
if (hasActiveFolders.value) {
|
||||
@@ -311,12 +315,30 @@ const pageTitle = computed(() => {
|
||||
return t('CHAT_LIST.TAB_HEADING');
|
||||
});
|
||||
|
||||
function filterByAssigneeTab(conversations) {
|
||||
if (activeAssigneeTab.value === wootConstants.ASSIGNEE_TYPE.ME) {
|
||||
return conversations.filter(
|
||||
c => c.meta?.assignee?.id === currentUser.value?.id
|
||||
);
|
||||
}
|
||||
if (activeAssigneeTab.value === wootConstants.ASSIGNEE_TYPE.UNASSIGNED) {
|
||||
return conversations.filter(c => !c.meta?.assignee);
|
||||
}
|
||||
return [...conversations];
|
||||
}
|
||||
|
||||
const conversationList = computed(() => {
|
||||
let localConversationList = [];
|
||||
|
||||
if (!hasAppliedFiltersOrActiveFolders.value) {
|
||||
const filters = conversationFilters.value;
|
||||
if (activeAssigneeTab.value === 'me') {
|
||||
if (
|
||||
props.conversationType === wootConstants.CONVERSATION_TYPE.PARTICIPATING
|
||||
) {
|
||||
localConversationList = filterByAssigneeTab(
|
||||
participatingChatsList.value(filters)
|
||||
);
|
||||
} else if (activeAssigneeTab.value === 'me') {
|
||||
localConversationList = [...mineChatsList.value(filters)];
|
||||
} else if (activeAssigneeTab.value === 'unassigned') {
|
||||
localConversationList = [...unAssignedChatsList.value(filters)];
|
||||
@@ -637,9 +659,11 @@ function redirectToConversationList() {
|
||||
|
||||
let conversationType = '';
|
||||
if (isOnMentionsView({ route: { name } })) {
|
||||
conversationType = 'mention';
|
||||
conversationType = wootConstants.CONVERSATION_TYPE.MENTION;
|
||||
} else if (isOnParticipatingView({ route: { name } })) {
|
||||
conversationType = wootConstants.CONVERSATION_TYPE.PARTICIPATING;
|
||||
} else if (isOnUnattendedView({ route: { name } })) {
|
||||
conversationType = 'unattended';
|
||||
conversationType = wootConstants.CONVERSATION_TYPE.UNATTENDED;
|
||||
}
|
||||
router.push(
|
||||
conversationListPageURL({
|
||||
|
||||
@@ -45,6 +45,7 @@ const backButtonUrl = computed(() => {
|
||||
|
||||
const conversationTypeMap = {
|
||||
conversation_through_mentions: 'mention',
|
||||
conversation_through_participating: 'participating',
|
||||
conversation_through_unattended: 'unattended',
|
||||
};
|
||||
return conversationListPageURL({
|
||||
|
||||
@@ -12,6 +12,11 @@ export default {
|
||||
SNOOZED: 'snoozed',
|
||||
ALL: 'all',
|
||||
},
|
||||
CONVERSATION_TYPE: {
|
||||
MENTION: 'mention',
|
||||
PARTICIPATING: 'participating',
|
||||
UNATTENDED: 'unattended',
|
||||
},
|
||||
SORT_BY_TYPE: {
|
||||
LAST_ACTIVITY_AT_ASC: 'last_activity_at_asc',
|
||||
LAST_ACTIVITY_AT_DESC: 'last_activity_at_desc',
|
||||
|
||||
@@ -51,6 +51,7 @@ export const conversationListPageURL = ({
|
||||
} else if (conversationType) {
|
||||
const urlMap = {
|
||||
mention: 'mentions/conversations',
|
||||
participating: 'participating/conversations',
|
||||
unattended: 'unattended/conversations',
|
||||
};
|
||||
url = `accounts/${accountId}/${urlMap[conversationType]}`;
|
||||
|
||||
@@ -40,6 +40,15 @@ describe('#URL Helpers', () => {
|
||||
'/app/accounts/1/custom_view/1'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return url to participating conversations', () => {
|
||||
expect(
|
||||
conversationListPageURL({
|
||||
accountId: 1,
|
||||
conversationType: 'participating',
|
||||
})
|
||||
).toBe('/app/accounts/1/participating/conversations');
|
||||
});
|
||||
});
|
||||
describe('conversationUrl', () => {
|
||||
it('should return direct conversation URL if activeInbox is nil', () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createPendingMessage } from 'dashboard/helper/commons';
|
||||
import {
|
||||
buildConversationList,
|
||||
isOnMentionsView,
|
||||
isOnParticipatingView,
|
||||
isOnUnattendedView,
|
||||
isOnFoldersView,
|
||||
} from './helpers/actionHelpers';
|
||||
@@ -371,6 +372,7 @@ const actions = {
|
||||
!hasAppliedFilters &&
|
||||
!isOnFoldersView(rootState) &&
|
||||
!isOnMentionsView(rootState) &&
|
||||
!isOnParticipatingView(rootState) &&
|
||||
!isOnUnattendedView(rootState) &&
|
||||
isMatchingInboxFilter
|
||||
) {
|
||||
@@ -395,6 +397,7 @@ const actions = {
|
||||
const {
|
||||
meta: { sender },
|
||||
} = conversation;
|
||||
|
||||
commit(types.UPDATE_CONVERSATION, conversation);
|
||||
|
||||
dispatch('conversationLabels/setConversationLabel', {
|
||||
|
||||
@@ -102,6 +102,19 @@ const getters = {
|
||||
return isUnAssigned && shouldFilter;
|
||||
});
|
||||
},
|
||||
getParticipatingChats: (_state, _, __, rootGetters) => activeFilters => {
|
||||
const currentUserId = rootGetters.getCurrentUser?.id;
|
||||
const getWatchers = rootGetters['conversationWatchers/getByConversationId'];
|
||||
return _state.allConversations.filter(conversation => {
|
||||
const watchers = getWatchers(conversation.id);
|
||||
// Watchers are only loaded for the conversation open in the detail
|
||||
// panel. If loaded and current user is not in them, filter it out.
|
||||
if (watchers && !watchers.some(w => w.id === currentUserId)) {
|
||||
return false;
|
||||
}
|
||||
return applyPageFilters(conversation, activeFilters);
|
||||
});
|
||||
},
|
||||
getAllStatusChats: (_state, _, __, rootGetters) => activeFilters => {
|
||||
const currentUser = rootGetters.getCurrentUser;
|
||||
const currentUserId = rootGetters.getCurrentUser.id;
|
||||
|
||||
@@ -30,6 +30,14 @@ export const isOnUnattendedView = ({ route: { name: routeName } }) => {
|
||||
return UNATTENDED_ROUTES.includes(routeName);
|
||||
};
|
||||
|
||||
export const isOnParticipatingView = ({ route: { name: routeName } }) => {
|
||||
const PARTICIPATING_ROUTES = [
|
||||
'conversation_participating',
|
||||
'conversation_through_participating',
|
||||
];
|
||||
return PARTICIPATING_ROUTES.includes(routeName);
|
||||
};
|
||||
|
||||
export const isOnFoldersView = ({ route: { name: routeName } }) => {
|
||||
const FOLDER_ROUTES = [
|
||||
'folder_conversations',
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { isOnMentionsView, isOnFoldersView } from '../actionHelpers';
|
||||
import {
|
||||
isOnMentionsView,
|
||||
isOnFoldersView,
|
||||
isOnParticipatingView,
|
||||
} from '../actionHelpers';
|
||||
|
||||
describe('#isOnMentionsView', () => {
|
||||
it('return valid responses when passing the state', () => {
|
||||
@@ -24,3 +28,19 @@ describe('#isOnFoldersView', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isOnParticipatingView', () => {
|
||||
it('return valid responses when passing the state', () => {
|
||||
expect(
|
||||
isOnParticipatingView({ route: { name: 'conversation_participating' } })
|
||||
).toBe(true);
|
||||
expect(
|
||||
isOnParticipatingView({
|
||||
route: { name: 'conversation_through_participating' },
|
||||
})
|
||||
).toBe(true);
|
||||
expect(
|
||||
isOnParticipatingView({ route: { name: 'conversation_messages' } })
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -258,7 +258,11 @@ export const mutations = {
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
}
|
||||
} else {
|
||||
_state.allConversations.push(conversation);
|
||||
const { conversationType } = _state.conversationFilters || {};
|
||||
const { MENTION, PARTICIPATING } = wootConstants.CONVERSATION_TYPE;
|
||||
if (![MENTION, PARTICIPATING].includes(conversationType)) {
|
||||
_state.allConversations.push(conversation);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -183,6 +183,73 @@ describe('#getters', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('#getParticipatingChats', () => {
|
||||
const conversationList = [
|
||||
{ id: 1, inbox_id: 2, status: 1, meta: { assignee: { id: 1 } } },
|
||||
{ id: 2, inbox_id: 2, status: 1, meta: {} },
|
||||
{ id: 3, inbox_id: 3, status: 1, meta: { assignee: { id: 2 } } },
|
||||
];
|
||||
|
||||
it('returns all conversations when watchers are not loaded', () => {
|
||||
const state = {
|
||||
allConversations: conversationList,
|
||||
participatingConversationIds: {},
|
||||
};
|
||||
const rootGetters = {
|
||||
getCurrentUser: { id: 1 },
|
||||
'conversationWatchers/getByConversationId': () => undefined,
|
||||
};
|
||||
const result = getters.getParticipatingChats(
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
rootGetters
|
||||
)({ status: 1 });
|
||||
expect(result).toEqual(conversationList);
|
||||
});
|
||||
|
||||
it('filters out conversation when watchers loaded and user not participating', () => {
|
||||
const state = {
|
||||
allConversations: conversationList,
|
||||
participatingConversationIds: {},
|
||||
};
|
||||
const rootGetters = {
|
||||
getCurrentUser: { id: 1 },
|
||||
'conversationWatchers/getByConversationId': id => {
|
||||
if (id === 2) return [{ id: 3 }];
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
const result = getters.getParticipatingChats(
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
rootGetters
|
||||
)({ status: 1 });
|
||||
expect(result).toEqual([conversationList[0], conversationList[2]]);
|
||||
});
|
||||
|
||||
it('keeps conversation when watchers loaded and user is participating', () => {
|
||||
const state = {
|
||||
allConversations: conversationList,
|
||||
participatingConversationIds: {},
|
||||
};
|
||||
const rootGetters = {
|
||||
getCurrentUser: { id: 1 },
|
||||
'conversationWatchers/getByConversationId': id => {
|
||||
if (id === 1) return [{ id: 1 }, { id: 2 }];
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
const result = getters.getParticipatingChats(
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
rootGetters
|
||||
)({ status: 1 });
|
||||
expect(result).toEqual(conversationList);
|
||||
});
|
||||
});
|
||||
describe('#getConversationById', () => {
|
||||
it('get conversations based on id', () => {
|
||||
const state = {
|
||||
|
||||
@@ -786,9 +786,55 @@ describe('#mutations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should add conversation if not found', () => {
|
||||
it('should add conversation if not found on normal view', () => {
|
||||
const state = {
|
||||
allConversations: [],
|
||||
conversationFilters: {},
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
id: 1,
|
||||
status: 'open',
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, conversation);
|
||||
expect(state.allConversations).toEqual([conversation]);
|
||||
});
|
||||
|
||||
it('should not add conversation if not found on participating view', () => {
|
||||
const state = {
|
||||
allConversations: [],
|
||||
conversationFilters: { conversationType: 'participating' },
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
id: 1,
|
||||
status: 'open',
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, conversation);
|
||||
expect(state.allConversations).toEqual([]);
|
||||
});
|
||||
|
||||
it('should not add conversation if not found on mention view', () => {
|
||||
const state = {
|
||||
allConversations: [],
|
||||
conversationFilters: { conversationType: 'mention' },
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
id: 1,
|
||||
status: 'open',
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, conversation);
|
||||
expect(state.allConversations).toEqual([]);
|
||||
});
|
||||
|
||||
it('should add conversation if not found on unattended view', () => {
|
||||
const state = {
|
||||
allConversations: [],
|
||||
conversationFilters: { conversationType: 'unattended' },
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
|
||||
Reference in New Issue
Block a user