554 lines
17 KiB
Vue
554 lines
17 KiB
Vue
<script setup>
|
|
import { ref, provide, computed, watch, onMounted, defineEmits } from 'vue';
|
|
import { useStore } from 'vuex';
|
|
import { useRouter } from 'vue-router';
|
|
import {
|
|
useMapGetter,
|
|
useFunctionGetter,
|
|
} from 'dashboard/composables/store.js';
|
|
|
|
import ChatListHeader from './ChatListHeader.vue';
|
|
import ConversationList from './ConversationList.vue';
|
|
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
|
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
|
import ChatListFilters from './ChatListFilters.vue';
|
|
import ConversationActiveFiltersPreview from './widgets/conversation/ConversationActiveFiltersPreview.vue';
|
|
|
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useSnakeCase } from 'dashboard/composables/useTransformKeys';
|
|
import { useEmitter } from 'dashboard/composables/emitter';
|
|
import { useBulkActions } from 'dashboard/composables/chatlist/useBulkActions';
|
|
import { emitter } from 'shared/helpers/mitt';
|
|
import wootConstants from 'dashboard/constants/globals';
|
|
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
|
|
import {
|
|
getUserPermissions,
|
|
filterItemsByPermission,
|
|
} from 'dashboard/helper/permissionsHelper.js';
|
|
import { matchesFilters } from '../store/modules/conversations/helpers/filterHelpers';
|
|
import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js';
|
|
|
|
const props = defineProps({
|
|
conversationInbox: { type: [String, Number], default: 0 },
|
|
teamId: { type: [String, Number], default: 0 },
|
|
label: { type: String, default: '' },
|
|
conversationType: { type: String, default: '' },
|
|
foldersId: { type: [String, Number], default: 0 },
|
|
showConversationList: { default: true, type: Boolean },
|
|
isOnExpandedLayout: { default: false, type: Boolean },
|
|
});
|
|
|
|
const emit = defineEmits(['conversationLoad']);
|
|
const { uiSettings } = useUISettings();
|
|
const { t } = useI18n();
|
|
const store = useStore();
|
|
const router = useRouter();
|
|
|
|
const activeAssigneeTab = ref(wootConstants.ASSIGNEE_TYPE.ME);
|
|
const activeStatus = ref(wootConstants.STATUS_TYPE.OPEN);
|
|
const activeSortBy = ref(wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC);
|
|
const chatsOnView = ref([]);
|
|
const isContextMenuOpen = ref(false);
|
|
const chatListFiltersRef = ref(null);
|
|
|
|
// Provide trigger for label recalculation (parent → child pattern)
|
|
const recalculateLabelsKey = ref(0);
|
|
provide('recalculateLabelsKey', recalculateLabelsKey);
|
|
|
|
const currentUser = useMapGetter('getCurrentUser');
|
|
const chatLists = useMapGetter('getFilteredConversations');
|
|
const mineChatsList = useMapGetter('getMineChats');
|
|
const allChatList = useMapGetter('getAllStatusChats');
|
|
const unAssignedChatsList = useMapGetter('getUnAssignedChats');
|
|
const chatListLoading = useMapGetter('getChatListLoadingStatus');
|
|
const activeInbox = useMapGetter('getSelectedInbox');
|
|
const conversationStats = useMapGetter('conversationStats/getStats');
|
|
const appliedFilters = useMapGetter('getAppliedConversationFiltersV2');
|
|
const currentAccountId = useMapGetter('getCurrentAccountId');
|
|
const getTeamFn = useMapGetter('teams/getTeam');
|
|
const folders = useMapGetter('customViews/getConversationCustomViews');
|
|
|
|
const {
|
|
selectedConversations,
|
|
selectedInboxes,
|
|
selectConversation,
|
|
deSelectConversation,
|
|
selectAllConversations,
|
|
resetBulkActions,
|
|
isConversationSelected,
|
|
} = useBulkActions();
|
|
|
|
const hasAppliedFilters = computed(() => {
|
|
return appliedFilters.value.length !== 0;
|
|
});
|
|
|
|
const hasActiveFolders = computed(() => {
|
|
return Boolean(props.foldersId !== 0);
|
|
});
|
|
|
|
const activeFolder = computed(() => {
|
|
return chatListFiltersRef.value?.activeFolder;
|
|
});
|
|
|
|
const isFilterModalOpen = computed(() => {
|
|
return chatListFiltersRef.value?.showAdvancedFilters ?? false;
|
|
});
|
|
|
|
const hasAppliedFiltersOrActiveFolders = computed(() => {
|
|
return hasAppliedFilters.value || hasActiveFolders.value;
|
|
});
|
|
|
|
const userPermissions = computed(() => {
|
|
return getUserPermissions(currentUser.value, currentAccountId.value);
|
|
});
|
|
|
|
const assigneeTabItems = computed(() => {
|
|
return filterItemsByPermission(
|
|
ASSIGNEE_TYPE_TAB_PERMISSIONS,
|
|
userPermissions.value,
|
|
item => item.permissions
|
|
).map(({ key, count: countKey }) => ({
|
|
key,
|
|
name: t(`CHAT_LIST.ASSIGNEE_TYPE_TABS.${key}`),
|
|
count: conversationStats.value[countKey] || 0,
|
|
}));
|
|
});
|
|
|
|
const showAssigneeInConversationCard = computed(() => {
|
|
return (
|
|
hasAppliedFiltersOrActiveFolders.value ||
|
|
activeAssigneeTab.value === wootConstants.ASSIGNEE_TYPE.ALL
|
|
);
|
|
});
|
|
|
|
const currentPageFilterKey = computed(() => {
|
|
return hasAppliedFiltersOrActiveFolders.value
|
|
? 'appliedFilters'
|
|
: activeAssigneeTab.value;
|
|
});
|
|
|
|
const inbox = useFunctionGetter('inboxes/getInbox', activeInbox);
|
|
const currentPage = useFunctionGetter(
|
|
'conversationPage/getCurrentPageFilter',
|
|
activeAssigneeTab
|
|
);
|
|
const currentFiltersPage = useFunctionGetter(
|
|
'conversationPage/getCurrentPageFilter',
|
|
currentPageFilterKey
|
|
);
|
|
const hasCurrentPageEndReached = useFunctionGetter(
|
|
'conversationPage/getHasEndReached',
|
|
currentPageFilterKey
|
|
);
|
|
|
|
const activeAssigneeTabCount = computed(() => {
|
|
const count = assigneeTabItems.value.find(
|
|
item => item.key === activeAssigneeTab.value
|
|
).count;
|
|
return count;
|
|
});
|
|
|
|
const conversationListPagination = computed(() => {
|
|
const conversationsPerPage = 25;
|
|
const hasChatsOnView =
|
|
chatsOnView.value &&
|
|
Array.isArray(chatsOnView.value) &&
|
|
!chatsOnView.value.length;
|
|
const isNoFiltersOrFoldersAndChatListNotEmpty =
|
|
!hasAppliedFiltersOrActiveFolders.value && hasChatsOnView;
|
|
const isUnderPerPage =
|
|
chatsOnView.value.length < conversationsPerPage &&
|
|
activeAssigneeTabCount.value < conversationsPerPage &&
|
|
activeAssigneeTabCount.value > chatsOnView.value.length;
|
|
|
|
if (isNoFiltersOrFoldersAndChatListNotEmpty && isUnderPerPage) {
|
|
return 1;
|
|
}
|
|
|
|
return currentPage.value + 1;
|
|
});
|
|
|
|
const conversationFilters = computed(() => {
|
|
return {
|
|
inboxId: props.conversationInbox ? props.conversationInbox : undefined,
|
|
assigneeType: activeAssigneeTab.value,
|
|
status: activeStatus.value,
|
|
sortBy: activeSortBy.value,
|
|
page: conversationListPagination.value,
|
|
labels: props.label ? [props.label] : undefined,
|
|
teamId: props.teamId || undefined,
|
|
conversationType: props.conversationType || undefined,
|
|
};
|
|
});
|
|
|
|
const activeTeam = computed(() => {
|
|
if (props.teamId) {
|
|
return getTeamFn.value(props.teamId);
|
|
}
|
|
return {};
|
|
});
|
|
|
|
const pageTitle = computed(() => {
|
|
if (hasAppliedFilters.value) {
|
|
return t('CHAT_LIST.TAB_HEADING');
|
|
}
|
|
if (inbox.value.name) {
|
|
return inbox.value.name;
|
|
}
|
|
if (activeTeam.value.name) {
|
|
return activeTeam.value.name;
|
|
}
|
|
if (props.label) {
|
|
return `#${props.label}`;
|
|
}
|
|
if (props.conversationType === 'mention') {
|
|
return t('CHAT_LIST.MENTION_HEADING');
|
|
}
|
|
if (props.conversationType === 'participating') {
|
|
return t('CONVERSATION_PARTICIPANTS.SIDEBAR_MENU_TITLE');
|
|
}
|
|
if (props.conversationType === 'unattended') {
|
|
return t('CHAT_LIST.UNATTENDED_HEADING');
|
|
}
|
|
if (hasActiveFolders.value && activeFolder.value?.name) {
|
|
return activeFolder.value.name;
|
|
}
|
|
return t('CHAT_LIST.TAB_HEADING');
|
|
});
|
|
|
|
const conversationList = computed(() => {
|
|
let localConversationList = [];
|
|
|
|
if (!hasAppliedFiltersOrActiveFolders.value) {
|
|
const filters = conversationFilters.value;
|
|
if (activeAssigneeTab.value === 'me') {
|
|
localConversationList = [...mineChatsList.value(filters)];
|
|
} else if (activeAssigneeTab.value === 'unassigned') {
|
|
localConversationList = [...unAssignedChatsList.value(filters)];
|
|
} else {
|
|
localConversationList = [...allChatList.value(filters)];
|
|
}
|
|
} else {
|
|
localConversationList = [...chatLists.value];
|
|
}
|
|
|
|
if (activeFolder.value?.query?.payload) {
|
|
const { payload } = activeFolder.value.query;
|
|
localConversationList = localConversationList.filter(conversation => {
|
|
return matchesFilters(conversation, payload);
|
|
});
|
|
}
|
|
|
|
return localConversationList;
|
|
});
|
|
|
|
const showEndOfListMessage = computed(() => {
|
|
return !!(
|
|
conversationList.value.length &&
|
|
hasCurrentPageEndReached.value &&
|
|
!chatListLoading.value
|
|
);
|
|
});
|
|
|
|
const allConversationsSelected = computed(() => {
|
|
return (
|
|
conversationList.value.length === selectedConversations.value.length &&
|
|
conversationList.value.every(el =>
|
|
selectedConversations.value.includes(el.id)
|
|
)
|
|
);
|
|
});
|
|
|
|
const uniqueInboxes = computed(() => {
|
|
return [...new Set(selectedInboxes.value)];
|
|
});
|
|
|
|
// ---------------------- Methods -----------------------
|
|
function setFiltersFromUISettings() {
|
|
const { conversations_filter_by: filterBy = {} } = uiSettings.value;
|
|
const { status, order_by: orderBy } = filterBy;
|
|
activeStatus.value = status || wootConstants.STATUS_TYPE.OPEN;
|
|
activeSortBy.value = Object.values(wootConstants.SORT_BY_TYPE).includes(
|
|
orderBy
|
|
)
|
|
? orderBy
|
|
: wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC;
|
|
}
|
|
|
|
function emitConversationLoaded() {
|
|
emit('conversationLoad');
|
|
}
|
|
|
|
function fetchFilteredConversations(payload) {
|
|
const transformedPayload = useSnakeCase(payload);
|
|
let page = currentFiltersPage.value + 1;
|
|
store
|
|
.dispatch('fetchFilteredConversations', {
|
|
queryData: filterQueryGenerator(transformedPayload),
|
|
page,
|
|
})
|
|
.then(emitConversationLoaded);
|
|
}
|
|
|
|
function fetchSavedFilteredConversations(payload) {
|
|
const transformedPayload = useSnakeCase(payload);
|
|
let page = currentFiltersPage.value + 1;
|
|
store
|
|
.dispatch('fetchFilteredConversations', {
|
|
queryData: transformedPayload,
|
|
page,
|
|
})
|
|
.then(emitConversationLoaded);
|
|
}
|
|
|
|
function fetchConversations() {
|
|
store.dispatch('updateChatListFilters', conversationFilters.value);
|
|
store.dispatch('fetchAllConversations').then(emitConversationLoaded);
|
|
}
|
|
|
|
function onApplyFilter(payload) {
|
|
resetBulkActions();
|
|
store.dispatch('conversationPage/reset');
|
|
store.dispatch('emptyAllConversations');
|
|
fetchFilteredConversations(payload);
|
|
}
|
|
|
|
function onOpenAddFolders() {
|
|
const lastItemOfFolder = folders.value[folders.value.length - 1];
|
|
const lastItemId = lastItemOfFolder.id;
|
|
router.push({
|
|
name: 'folder_conversations',
|
|
params: { id: lastItemId },
|
|
});
|
|
}
|
|
|
|
function onOpenDeleteFolders() {
|
|
if (folders.value.length > 0) {
|
|
const lastItemOfFolder = folders.value[folders.value.length - 1];
|
|
const lastItemId = lastItemOfFolder.id;
|
|
router.push({
|
|
name: 'folder_conversations',
|
|
params: { id: lastItemId },
|
|
});
|
|
} else {
|
|
router.push({ name: 'home' });
|
|
fetchConversations();
|
|
}
|
|
}
|
|
|
|
function resetAndFetchData() {
|
|
resetBulkActions();
|
|
store.dispatch('conversationPage/reset');
|
|
store.dispatch('emptyAllConversations');
|
|
store.dispatch('clearConversationFilters');
|
|
if (hasActiveFolders.value && activeFolder.value?.query) {
|
|
const payload = activeFolder.value.query;
|
|
fetchSavedFilteredConversations(payload);
|
|
}
|
|
if (props.foldersId) {
|
|
return;
|
|
}
|
|
fetchConversations();
|
|
}
|
|
|
|
function loadMoreConversations() {
|
|
if (hasCurrentPageEndReached.value || chatListLoading.value) {
|
|
return;
|
|
}
|
|
|
|
if (!hasAppliedFiltersOrActiveFolders.value) {
|
|
fetchConversations();
|
|
} else if (hasActiveFolders.value && activeFolder.value?.query) {
|
|
const payload = activeFolder.value.query;
|
|
fetchSavedFilteredConversations(payload);
|
|
} else if (hasAppliedFilters.value) {
|
|
fetchFilteredConversations(appliedFilters.value);
|
|
}
|
|
}
|
|
|
|
function updateAssigneeTab(selectedTab) {
|
|
if (activeAssigneeTab.value !== selectedTab) {
|
|
resetBulkActions();
|
|
emitter.emit('clearSearchInput');
|
|
activeAssigneeTab.value = selectedTab;
|
|
if (!currentPage.value) {
|
|
fetchConversations();
|
|
}
|
|
// Trigger label position recalculation
|
|
recalculateLabelsKey.value += 1;
|
|
}
|
|
}
|
|
|
|
function onBasicFilterChange(value, type) {
|
|
if (type === 'status') {
|
|
activeStatus.value = value;
|
|
} else {
|
|
activeSortBy.value = value;
|
|
}
|
|
resetAndFetchData();
|
|
}
|
|
|
|
function allSelectedConversationsStatus(status) {
|
|
if (!selectedConversations.value.length) return false;
|
|
return selectedConversations.value.every(item => {
|
|
return store.getters.getConversationById(item)?.status === status;
|
|
});
|
|
}
|
|
|
|
function onContextMenuToggle(state) {
|
|
isContextMenuOpen.value = state;
|
|
}
|
|
|
|
function toggleSelectAll(check) {
|
|
selectAllConversations(check, conversationList);
|
|
}
|
|
|
|
useEmitter('fetch_conversation_stats', () => {
|
|
if (hasAppliedFiltersOrActiveFolders.value) return;
|
|
store.dispatch('conversationStats/get', conversationFilters.value);
|
|
});
|
|
|
|
onMounted(() => {
|
|
store.dispatch('setChatListFilters', conversationFilters.value);
|
|
setFiltersFromUISettings();
|
|
store.dispatch('setChatStatusFilter', activeStatus.value);
|
|
store.dispatch('setChatSortFilter', activeSortBy.value);
|
|
resetAndFetchData();
|
|
if (hasActiveFolders.value) {
|
|
store.dispatch('campaigns/get');
|
|
}
|
|
});
|
|
|
|
provide('selectConversation', selectConversation);
|
|
provide('deSelectConversation', deSelectConversation);
|
|
provide('toggleContextMenu', onContextMenuToggle);
|
|
provide('isConversationSelected', isConversationSelected);
|
|
|
|
watch(
|
|
computed(() => props.conversationInbox),
|
|
() => resetAndFetchData()
|
|
);
|
|
watch(
|
|
computed(() => props.teamId),
|
|
() => resetAndFetchData()
|
|
);
|
|
watch(
|
|
computed(() => props.label),
|
|
() => resetAndFetchData()
|
|
);
|
|
watch(
|
|
computed(() => props.conversationType),
|
|
() => resetAndFetchData()
|
|
);
|
|
watch(
|
|
computed(() => props.foldersId),
|
|
() => resetAndFetchData()
|
|
);
|
|
|
|
watch(activeFolder, (newVal, oldVal) => {
|
|
if (newVal !== oldVal) {
|
|
store.dispatch('customViews/setActiveConversationFolder', newVal || null);
|
|
}
|
|
resetAndFetchData();
|
|
});
|
|
|
|
watch(chatLists, () => {
|
|
chatsOnView.value = conversationList.value;
|
|
});
|
|
|
|
watch(conversationFilters, (newVal, oldVal) => {
|
|
if (newVal !== oldVal) {
|
|
store.dispatch('updateChatListFilters', newVal);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="flex flex-col flex-shrink-0 conversations-list-wrap bg-n-surface-1"
|
|
:class="[
|
|
{ hidden: !showConversationList },
|
|
isOnExpandedLayout ? 'basis-full' : 'w-[340px] 2xl:w-[412px]',
|
|
]"
|
|
>
|
|
<slot />
|
|
<ChatListHeader
|
|
:page-title="pageTitle"
|
|
:has-applied-filters="hasAppliedFilters"
|
|
:has-active-folders="hasActiveFolders"
|
|
:active-status="activeStatus"
|
|
:is-on-expanded-layout="isOnExpandedLayout"
|
|
:conversation-stats="conversationStats"
|
|
:is-list-loading="chatListLoading && !conversationList.length"
|
|
:is-filter-modal-open="isFilterModalOpen"
|
|
@filters-modal="chatListFiltersRef?.onToggleAdvanceFiltersModal"
|
|
@basic-filter-change="onBasicFilterChange"
|
|
/>
|
|
|
|
<ChatListFilters
|
|
ref="chatListFiltersRef"
|
|
:conversation-inbox="conversationInbox"
|
|
:team-id="teamId"
|
|
:label="label"
|
|
:folders-id="foldersId"
|
|
:active-status="activeStatus"
|
|
:active-assignee-tab="activeAssigneeTab"
|
|
:current-user-details="{ id: currentUser.id, name: currentUser.name }"
|
|
:has-active-folders="hasActiveFolders"
|
|
:has-applied-filters="hasAppliedFilters"
|
|
@apply-filter="onApplyFilter"
|
|
@open-add-folders="onOpenAddFolders"
|
|
@open-delete-folders="onOpenDeleteFolders"
|
|
/>
|
|
|
|
<ChatTypeTabs
|
|
v-if="!hasAppliedFiltersOrActiveFolders"
|
|
:items="assigneeTabItems"
|
|
:active-tab="activeAssigneeTab"
|
|
is-compact
|
|
@chat-tab-change="updateAssigneeTab"
|
|
/>
|
|
<ConversationActiveFiltersPreview
|
|
:active-folder="activeFolder"
|
|
:has-active-folders="hasActiveFolders"
|
|
:is-on-expanded-layout="isOnExpandedLayout"
|
|
@clear-filters="resetAndFetchData"
|
|
@open-filter="chatListFiltersRef?.onToggleAdvanceFiltersModal"
|
|
@save-folder="chatListFiltersRef?.onClickOpenAddFoldersModal"
|
|
@delete-folder="chatListFiltersRef?.onClickOpenDeleteFoldersModal"
|
|
/>
|
|
<ConversationBulkActions
|
|
v-if="selectedConversations.length"
|
|
:conversations="selectedConversations"
|
|
:all-conversations-selected="allConversationsSelected"
|
|
:selected-inboxes="uniqueInboxes"
|
|
:show-open-action="allSelectedConversationsStatus('open')"
|
|
:show-resolved-action="allSelectedConversationsStatus('resolved')"
|
|
:show-snoozed-action="allSelectedConversationsStatus('snoozed')"
|
|
@select-all-conversations="toggleSelectAll"
|
|
/>
|
|
|
|
<p
|
|
v-if="!chatListLoading && !conversationList.length"
|
|
class="flex items-center justify-center p-4 overflow-auto"
|
|
>
|
|
{{ $t('CHAT_LIST.LIST.404') }}
|
|
</p>
|
|
|
|
<ConversationList
|
|
:items="conversationList"
|
|
:is-loading="chatListLoading"
|
|
:show-end-of-list-message="showEndOfListMessage"
|
|
:is-context-menu-open="isContextMenuOpen"
|
|
:label="label"
|
|
:team-id="teamId"
|
|
:folders-id="foldersId"
|
|
:conversation-type="conversationType"
|
|
:show-assignee="showAssigneeInConversationCard"
|
|
:is-expanded-layout="isOnExpandedLayout"
|
|
@load-more="loadMoreConversations"
|
|
/>
|
|
</div>
|
|
</template>
|