diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue index d08147739..9fd25c481 100644 --- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue +++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue @@ -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'], diff --git a/app/javascript/dashboard/components/ChatList.vue b/app/javascript/dashboard/components/ChatList.vue index eeb2ad6e7..f311de4b2 100644 --- a/app/javascript/dashboard/components/ChatList.vue +++ b/app/javascript/dashboard/components/ChatList.vue @@ -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({ diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue index 55252d6da..4edfd643c 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue @@ -45,6 +45,7 @@ const backButtonUrl = computed(() => { const conversationTypeMap = { conversation_through_mentions: 'mention', + conversation_through_participating: 'participating', conversation_through_unattended: 'unattended', }; return conversationListPageURL({ diff --git a/app/javascript/dashboard/constants/globals.js b/app/javascript/dashboard/constants/globals.js index 21303efcb..4fbc8174b 100644 --- a/app/javascript/dashboard/constants/globals.js +++ b/app/javascript/dashboard/constants/globals.js @@ -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', diff --git a/app/javascript/dashboard/helper/URLHelper.js b/app/javascript/dashboard/helper/URLHelper.js index a4b3f32b4..76a5d8bd4 100644 --- a/app/javascript/dashboard/helper/URLHelper.js +++ b/app/javascript/dashboard/helper/URLHelper.js @@ -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]}`; diff --git a/app/javascript/dashboard/helper/specs/URLHelper.spec.js b/app/javascript/dashboard/helper/specs/URLHelper.spec.js index 224a1df8b..cd7479509 100644 --- a/app/javascript/dashboard/helper/specs/URLHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/URLHelper.spec.js @@ -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', () => { diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js index c6a197d85..7ee2561c1 100644 --- a/app/javascript/dashboard/store/modules/conversations/actions.js +++ b/app/javascript/dashboard/store/modules/conversations/actions.js @@ -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', { diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js index 9f5744fbb..333009707 100644 --- a/app/javascript/dashboard/store/modules/conversations/getters.js +++ b/app/javascript/dashboard/store/modules/conversations/getters.js @@ -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; diff --git a/app/javascript/dashboard/store/modules/conversations/helpers/actionHelpers.js b/app/javascript/dashboard/store/modules/conversations/helpers/actionHelpers.js index f1c594b26..8c5575c3a 100644 --- a/app/javascript/dashboard/store/modules/conversations/helpers/actionHelpers.js +++ b/app/javascript/dashboard/store/modules/conversations/helpers/actionHelpers.js @@ -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', diff --git a/app/javascript/dashboard/store/modules/conversations/helpers/specs/actionHelpers.spec.js b/app/javascript/dashboard/store/modules/conversations/helpers/specs/actionHelpers.spec.js index 2dcf0b26b..e37a5b225 100644 --- a/app/javascript/dashboard/store/modules/conversations/helpers/specs/actionHelpers.spec.js +++ b/app/javascript/dashboard/store/modules/conversations/helpers/specs/actionHelpers.spec.js @@ -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); + }); +}); diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js index d7137694e..bffc2204a 100644 --- a/app/javascript/dashboard/store/modules/conversations/index.js +++ b/app/javascript/dashboard/store/modules/conversations/index.js @@ -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); + } } }, diff --git a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js index 7b6c38456..69bf9c2ac 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js @@ -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 = { diff --git a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js index bd048dbba..fc1c61b35 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/mutations.spec.js @@ -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 = {