mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-30 10:42:38 +00:00
Porting changes from https://github.com/chatwoot/chatwoot/pull/10552 --------- Co-authored-by: Pranav <pranav@chatwoot.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Vishnu Narayanan <vishnu@chatwoot.com> Co-authored-by: Sojan <sojan@pepalo.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Pranav <pranavrajs@gmail.com>
921 lines
28 KiB
Vue
921 lines
28 KiB
Vue
<script setup>
|
|
// [TODO] This componet is too big and bulky to be in the same file, we can consider splitting this into multiple
|
|
// composables and components, useVirtualChatList, useChatlistFilters
|
|
import {
|
|
ref,
|
|
unref,
|
|
provide,
|
|
computed,
|
|
watch,
|
|
onMounted,
|
|
defineEmits,
|
|
} from 'vue';
|
|
import { useStore } from 'vuex';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import {
|
|
useMapGetter,
|
|
useFunctionGetter,
|
|
} from 'dashboard/composables/store.js';
|
|
|
|
// [VITE] [TODO] We are using vue-virtual-scroll for now, since that seemed the simplest way to migrate
|
|
// from the current one. But we should consider using tanstack virtual in the future
|
|
// https://tanstack.com/virtual/latest/docs/framework/vue/examples/variable
|
|
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller';
|
|
import ChatListHeader from './ChatListHeader.vue';
|
|
import ConversationFilter from 'next/filter/ConversationFilter.vue';
|
|
import SaveCustomView from 'next/filter/SaveCustomView.vue';
|
|
import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
|
|
import ConversationItem from './ConversationItem.vue';
|
|
import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCustomViews.vue';
|
|
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
|
|
import IntersectionObserver from './IntersectionObserver.vue';
|
|
|
|
import { useUISettings } from 'dashboard/composables/useUISettings';
|
|
import { useAlert } from 'dashboard/composables';
|
|
import { useChatListKeyboardEvents } from 'dashboard/composables/chatlist/useChatListKeyboardEvents';
|
|
import { useBulkActions } from 'dashboard/composables/chatlist/useBulkActions';
|
|
import { useFilter } from 'shared/composables/useFilter';
|
|
import { useTrack } from 'dashboard/composables';
|
|
import { useI18n } from 'vue-i18n';
|
|
import {
|
|
useCamelCase,
|
|
useSnakeCase,
|
|
} from 'dashboard/composables/useTransformKeys';
|
|
import { useEmitter } from 'dashboard/composables/emitter';
|
|
import { useEventListener } from '@vueuse/core';
|
|
|
|
import { emitter } from 'shared/helpers/mitt';
|
|
|
|
import wootConstants from 'dashboard/constants/globals';
|
|
import advancedFilterOptions from './widgets/conversation/advancedFilterItems';
|
|
import filterQueryGenerator from '../helper/filterQueryGenerator.js';
|
|
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
|
import countries from 'shared/constants/countries';
|
|
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
|
|
import { conversationListPageURL } from '../helper/URLHelper';
|
|
import {
|
|
isOnMentionsView,
|
|
isOnUnattendedView,
|
|
} from '../store/modules/conversations/helpers/actionHelpers';
|
|
import {
|
|
getUserPermissions,
|
|
filterItemsByPermission,
|
|
} from 'dashboard/helper/permissionsHelper.js';
|
|
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
|
|
import { ASSIGNEE_TYPE_TAB_PERMISSIONS } from 'dashboard/constants/permissions.js';
|
|
|
|
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
|
|
|
|
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 router = useRouter();
|
|
const store = useStore();
|
|
|
|
const conversationListRef = ref(null);
|
|
const conversationDynamicScroller = ref(null);
|
|
|
|
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 showAdvancedFilters = ref(false);
|
|
// chatsOnView is to store the chats that are currently visible on the screen,
|
|
// which mirrors the conversationList.
|
|
const chatsOnView = ref([]);
|
|
const foldersQuery = ref({});
|
|
const showAddFoldersModal = ref(false);
|
|
const showDeleteFoldersModal = ref(false);
|
|
const isContextMenuOpen = ref(false);
|
|
const appliedFilter = ref([]);
|
|
const advancedFilterTypes = ref(
|
|
advancedFilterOptions.map(filter => ({
|
|
...filter,
|
|
attributeName: t(`FILTER.ATTRIBUTES.${filter.attributeI18nKey}`),
|
|
}))
|
|
);
|
|
|
|
const currentUser = useMapGetter('getCurrentUser');
|
|
const chatLists = useMapGetter('getAllConversations');
|
|
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 folders = useMapGetter('customViews/getConversationCustomViews');
|
|
const agentList = useMapGetter('agents/getAgents');
|
|
const teamsList = useMapGetter('teams/getTeams');
|
|
const inboxesList = useMapGetter('inboxes/getInboxes');
|
|
const campaigns = useMapGetter('campaigns/getAllCampaigns');
|
|
const labels = useMapGetter('labels/getLabels');
|
|
const currentAccountId = useMapGetter('getCurrentAccountId');
|
|
// We can't useFunctionGetter here since it needs to be called on setup?
|
|
const getTeamFn = useMapGetter('teams/getTeam');
|
|
|
|
useChatListKeyboardEvents(conversationListRef);
|
|
const {
|
|
selectedConversations,
|
|
selectedInboxes,
|
|
selectConversation,
|
|
deSelectConversation,
|
|
selectAllConversations,
|
|
resetBulkActions,
|
|
isConversationSelected,
|
|
onAssignAgent,
|
|
onAssignLabels,
|
|
onAssignTeamsForBulk,
|
|
onUpdateConversations,
|
|
} = useBulkActions();
|
|
|
|
const {
|
|
initializeStatusAndAssigneeFilterToModal,
|
|
initializeInboxTeamAndLabelFilterToModal,
|
|
} = useFilter({
|
|
filteri18nKey: 'FILTER',
|
|
attributeModel: 'conversation_attribute',
|
|
});
|
|
|
|
// computed
|
|
const intersectionObserverOptions = computed(() => {
|
|
return {
|
|
root: conversationListRef.value,
|
|
rootMargin: '100px 0px 100px 0px',
|
|
};
|
|
});
|
|
|
|
const hasAppliedFilters = computed(() => {
|
|
return appliedFilters.value.length !== 0;
|
|
});
|
|
|
|
const activeFolder = computed(() => {
|
|
if (props.foldersId) {
|
|
const activeView = folders.value.filter(
|
|
view => view.id === Number(props.foldersId)
|
|
);
|
|
const [firstValue] = activeView;
|
|
return firstValue;
|
|
}
|
|
return undefined;
|
|
});
|
|
|
|
const activeFolderName = computed(() => {
|
|
return activeFolder.value?.name;
|
|
});
|
|
|
|
const hasActiveFolders = computed(() => {
|
|
return Boolean(activeFolder.value && props.foldersId !== 0);
|
|
});
|
|
|
|
const hasAppliedFiltersOrActiveFolders = computed(() => {
|
|
return hasAppliedFilters.value || hasActiveFolders.value;
|
|
});
|
|
|
|
const currentUserDetails = computed(() => {
|
|
const { id, name } = currentUser.value;
|
|
return { id, name };
|
|
});
|
|
|
|
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 conversationCustomAttributes = useFunctionGetter(
|
|
'attributes/getAttributesByModel',
|
|
'conversation_attribute'
|
|
);
|
|
|
|
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) {
|
|
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];
|
|
}
|
|
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.keys(wootConstants.SORT_BY_TYPE).find(
|
|
sortField => sortField === orderBy
|
|
) || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC;
|
|
}
|
|
|
|
function emitConversationLoaded() {
|
|
emit('conversationLoad');
|
|
// [VITE] removing this since the library has changed
|
|
// nextTick(() => {
|
|
// // Addressing a known issue in the virtual list library where dynamically added items
|
|
// // might not render correctly. This workaround involves a slight manual adjustment
|
|
// // to the scroll position, triggering the list to refresh its rendering.
|
|
// const virtualList = conversationListRef.value;
|
|
// const scrollToOffset = virtualList?.scrollToOffset;
|
|
// const currentOffset = virtualList?.getOffset() || 0;
|
|
// if (scrollToOffset) {
|
|
// scrollToOffset(currentOffset + 1);
|
|
// }
|
|
// });
|
|
}
|
|
|
|
function fetchFilteredConversations(payload) {
|
|
payload = useSnakeCase(payload);
|
|
let page = currentFiltersPage.value + 1;
|
|
store
|
|
.dispatch('fetchFilteredConversations', {
|
|
queryData: filterQueryGenerator(payload),
|
|
page,
|
|
})
|
|
.then(emitConversationLoaded);
|
|
|
|
showAdvancedFilters.value = false;
|
|
}
|
|
|
|
function fetchSavedFilteredConversations(payload) {
|
|
payload = useSnakeCase(payload);
|
|
let page = currentFiltersPage.value + 1;
|
|
store
|
|
.dispatch('fetchFilteredConversations', {
|
|
queryData: payload,
|
|
page,
|
|
})
|
|
.then(emitConversationLoaded);
|
|
}
|
|
|
|
function onApplyFilter(payload) {
|
|
payload = useSnakeCase(payload);
|
|
resetBulkActions();
|
|
foldersQuery.value = filterQueryGenerator(payload);
|
|
store.dispatch('conversationPage/reset');
|
|
store.dispatch('emptyAllConversations');
|
|
fetchFilteredConversations(payload);
|
|
}
|
|
|
|
function closeAdvanceFiltersModal() {
|
|
showAdvancedFilters.value = false;
|
|
appliedFilter.value = [];
|
|
}
|
|
|
|
function onUpdateSavedFilter(payload, folderName) {
|
|
const transformedPayload = useSnakeCase(payload);
|
|
const payloadData = {
|
|
...unref(activeFolder),
|
|
name: unref(folderName),
|
|
query: filterQueryGenerator(transformedPayload),
|
|
};
|
|
store.dispatch('customViews/update', payloadData);
|
|
closeAdvanceFiltersModal();
|
|
}
|
|
|
|
function onClickOpenAddFoldersModal() {
|
|
showAddFoldersModal.value = true;
|
|
}
|
|
|
|
function onCloseAddFoldersModal() {
|
|
showAddFoldersModal.value = false;
|
|
}
|
|
|
|
function onClickOpenDeleteFoldersModal() {
|
|
showDeleteFoldersModal.value = true;
|
|
}
|
|
|
|
function onCloseDeleteFoldersModal() {
|
|
showDeleteFoldersModal.value = false;
|
|
}
|
|
|
|
function setParamsForEditFolderModal() {
|
|
// Here we are setting the params for edit folder modal to show the existing values.
|
|
|
|
// For agent, team, inboxes,and campaigns we get only the id's from the query.
|
|
// So we are mapping the id's to the actual values.
|
|
|
|
// For labels we get the name of the label from the query.
|
|
// If we delete the label from the label list then we will not be able to show the label name.
|
|
|
|
// For custom attributes we get only attribute key.
|
|
// So we are mapping it to find the input type of the attribute to show in the edit folder modal.
|
|
return {
|
|
agents: agentList.value,
|
|
teams: teamsList.value,
|
|
inboxes: inboxesList.value,
|
|
labels: labels.value,
|
|
campaigns: campaigns.value,
|
|
languages: languages,
|
|
countries: countries,
|
|
filterTypes: advancedFilterTypes.value,
|
|
allCustomAttributes: conversationCustomAttributes.value,
|
|
};
|
|
}
|
|
|
|
function initializeExistingFilterToModal() {
|
|
const statusFilter = initializeStatusAndAssigneeFilterToModal(
|
|
activeStatus.value,
|
|
currentUserDetails.value,
|
|
activeAssigneeTab.value
|
|
);
|
|
// TODO: Remove the usage of useCamelCase after migrating useFilter to camelcase
|
|
if (statusFilter) {
|
|
appliedFilter.value = [...appliedFilter.value, useCamelCase(statusFilter)];
|
|
}
|
|
|
|
// TODO: Remove the usage of useCamelCase after migrating useFilter to camelcase
|
|
const otherFilters = initializeInboxTeamAndLabelFilterToModal(
|
|
props.conversationInbox,
|
|
inbox.value,
|
|
props.teamId,
|
|
activeTeam.value,
|
|
props.label
|
|
).map(useCamelCase);
|
|
|
|
appliedFilter.value = [...appliedFilter.value, ...otherFilters];
|
|
}
|
|
|
|
function initializeFolderToFilterModal(newActiveFolder) {
|
|
// Here we are setting the params for edit folder modal.
|
|
// To show the existing values. when we click on edit folder button.
|
|
|
|
// Here we get the query from the active folder.
|
|
// And we are mapping the query to the actual values.
|
|
// To show in the edit folder modal by the help of generateValuesForEditCustomViews helper.
|
|
const query = unref(newActiveFolder)?.query?.payload;
|
|
if (!Array.isArray(query)) return;
|
|
|
|
const newFilters = query.map(filter => {
|
|
const transformed = useCamelCase(filter);
|
|
const values = Array.isArray(transformed.values)
|
|
? generateValuesForEditCustomViews(
|
|
useSnakeCase(filter),
|
|
setParamsForEditFolderModal()
|
|
)
|
|
: [];
|
|
|
|
return {
|
|
attributeKey: transformed.attributeKey,
|
|
attributeModel: transformed.attributeModel,
|
|
customAttributeType: transformed.customAttributeType,
|
|
filterOperator: transformed.filterOperator,
|
|
queryOperator: transformed.queryOperator ?? 'and',
|
|
values,
|
|
};
|
|
});
|
|
|
|
appliedFilter.value = [...appliedFilter.value, ...newFilters];
|
|
}
|
|
|
|
function initalizeAppliedFiltersToModal() {
|
|
appliedFilter.value = [...appliedFilters.value];
|
|
}
|
|
|
|
function onToggleAdvanceFiltersModal() {
|
|
if (showAdvancedFilters.value === true) {
|
|
closeAdvanceFiltersModal();
|
|
return;
|
|
}
|
|
|
|
if (!hasAppliedFilters.value && !hasActiveFolders.value) {
|
|
initializeExistingFilterToModal();
|
|
}
|
|
if (hasActiveFolders.value) {
|
|
initializeFolderToFilterModal(activeFolder.value);
|
|
}
|
|
if (hasAppliedFilters.value) {
|
|
initalizeAppliedFiltersToModal();
|
|
}
|
|
|
|
showAdvancedFilters.value = true;
|
|
}
|
|
|
|
function fetchConversations() {
|
|
store.dispatch('updateChatListFilters', conversationFilters.value);
|
|
store.dispatch('fetchAllConversations').then(emitConversationLoaded);
|
|
}
|
|
|
|
function resetAndFetchData() {
|
|
appliedFilter.value = [];
|
|
resetBulkActions();
|
|
store.dispatch('conversationPage/reset');
|
|
store.dispatch('emptyAllConversations');
|
|
store.dispatch('clearConversationFilters');
|
|
if (hasActiveFolders.value) {
|
|
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) {
|
|
const payload = activeFolder.value.query;
|
|
fetchSavedFilteredConversations(payload);
|
|
} else if (hasAppliedFilters.value) {
|
|
fetchFilteredConversations(appliedFilters.value);
|
|
}
|
|
}
|
|
|
|
// Add a method to handle scroll events
|
|
function handleScroll() {
|
|
const scroller = conversationDynamicScroller.value;
|
|
if (scroller && scroller.hasScrollbar) {
|
|
const { scrollTop, scrollHeight, clientHeight } = scroller.$el;
|
|
if (scrollHeight - (scrollTop + clientHeight) < 100) {
|
|
loadMoreConversations();
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateAssigneeTab(selectedTab) {
|
|
if (activeAssigneeTab.value !== selectedTab) {
|
|
resetBulkActions();
|
|
emitter.emit('clearSearchInput');
|
|
activeAssigneeTab.value = selectedTab;
|
|
if (!currentPage.value) {
|
|
fetchConversations();
|
|
}
|
|
}
|
|
}
|
|
|
|
function onBasicFilterChange(value, type) {
|
|
if (type === 'status') {
|
|
activeStatus.value = value;
|
|
} else {
|
|
activeSortBy.value = value;
|
|
}
|
|
resetAndFetchData();
|
|
}
|
|
|
|
function openLastSavedItemInFolder() {
|
|
const lastItemOfFolder = folders.value[folders.value.length - 1];
|
|
const lastItemId = lastItemOfFolder.id;
|
|
router.push({
|
|
name: 'folder_conversations',
|
|
params: { id: lastItemId },
|
|
});
|
|
}
|
|
|
|
function openLastItemAfterDeleteInFolder() {
|
|
if (folders.value.length > 0) {
|
|
openLastSavedItemInFolder();
|
|
} else {
|
|
router.push({ name: 'home' });
|
|
fetchConversations();
|
|
}
|
|
}
|
|
|
|
async function assignPriority(priority, conversationId = null) {
|
|
store.dispatch('setCurrentChatPriority', {
|
|
priority,
|
|
conversationId,
|
|
});
|
|
store.dispatch('assignPriority', { conversationId, priority }).then(() => {
|
|
useTrack(CONVERSATION_EVENTS.CHANGE_PRIORITY, {
|
|
newValue: priority,
|
|
from: 'Context menu',
|
|
});
|
|
useAlert(
|
|
t('CONVERSATION.PRIORITY.CHANGE_PRIORITY.SUCCESSFUL', {
|
|
priority,
|
|
conversationId,
|
|
})
|
|
);
|
|
});
|
|
}
|
|
|
|
async function markAsUnread(conversationId) {
|
|
try {
|
|
await store.dispatch('markMessagesUnread', {
|
|
id: conversationId,
|
|
});
|
|
const {
|
|
params: { accountId, inbox_id: inboxId, label, teamId },
|
|
name,
|
|
} = useRoute();
|
|
let conversationType = '';
|
|
if (isOnMentionsView({ route: { name } })) {
|
|
conversationType = 'mention';
|
|
} else if (isOnUnattendedView({ route: { name } })) {
|
|
conversationType = 'unattended';
|
|
}
|
|
router.push(
|
|
conversationListPageURL({
|
|
accountId,
|
|
conversationType: conversationType,
|
|
customViewId: props.foldersId,
|
|
inboxId,
|
|
label,
|
|
teamId,
|
|
})
|
|
);
|
|
} catch (error) {
|
|
// Ignore error
|
|
}
|
|
}
|
|
async function onAssignTeam(team, conversationId = null) {
|
|
try {
|
|
await store.dispatch('assignTeam', {
|
|
conversationId,
|
|
teamId: team.id,
|
|
});
|
|
useAlert(
|
|
t('CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.SUCCESFUL', {
|
|
team: team.name,
|
|
conversationId,
|
|
})
|
|
);
|
|
} catch (error) {
|
|
useAlert(t('CONVERSATION.CARD_CONTEXT_MENU.API.TEAM_ASSIGNMENT.FAILED'));
|
|
}
|
|
}
|
|
|
|
function toggleConversationStatus(conversationId, status, snoozedUntil) {
|
|
store
|
|
.dispatch('toggleStatus', {
|
|
conversationId,
|
|
status,
|
|
snoozedUntil,
|
|
})
|
|
.then(() => {
|
|
useAlert(t('CONVERSATION.CHANGE_STATUS'));
|
|
});
|
|
}
|
|
|
|
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', () => {
|
|
store.dispatch('conversationStats/get', conversationFilters.value);
|
|
});
|
|
|
|
useEventListener(conversationDynamicScroller, 'scroll', handleScroll);
|
|
|
|
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('assignAgent', onAssignAgent);
|
|
provide('assignTeam', onAssignTeam);
|
|
provide('assignLabels', onAssignLabels);
|
|
provide('updateConversationStatus', toggleConversationStatus);
|
|
provide('toggleContextMenu', onContextMenuToggle);
|
|
provide('markAsUnread', markAsUnread);
|
|
provide('assignPriority', assignPriority);
|
|
provide('isConversationSelected', isConversationSelected);
|
|
|
|
watch(activeTeam, () => resetAndFetchData());
|
|
|
|
watch(
|
|
computed(() => props.conversationInbox),
|
|
() => resetAndFetchData()
|
|
);
|
|
watch(
|
|
computed(() => props.label),
|
|
() => resetAndFetchData()
|
|
);
|
|
watch(
|
|
computed(() => props.conversationType),
|
|
() => 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 bg-n-solid-1 conversations-list-wrap"
|
|
:class="[
|
|
{ hidden: !showConversationList },
|
|
isOnExpandedLayout ? 'basis-full' : 'w-[360px] 2xl:w-[420px]',
|
|
]"
|
|
>
|
|
<slot />
|
|
<ChatListHeader
|
|
:page-title="pageTitle"
|
|
:has-applied-filters="hasAppliedFilters"
|
|
:has-active-folders="hasActiveFolders"
|
|
:active-status="activeStatus"
|
|
:is-on-expanded-layout="isOnExpandedLayout"
|
|
@add-folders="onClickOpenAddFoldersModal"
|
|
@delete-folders="onClickOpenDeleteFoldersModal"
|
|
@filters-modal="onToggleAdvanceFiltersModal"
|
|
@reset-filters="resetAndFetchData"
|
|
@basic-filter-change="onBasicFilterChange"
|
|
/>
|
|
|
|
<Teleport v-if="showAddFoldersModal" to="#saveFilterTeleportTarget">
|
|
<SaveCustomView
|
|
v-model="appliedFilter"
|
|
:custom-views-query="foldersQuery"
|
|
:open-last-saved-item="openLastSavedItemInFolder"
|
|
@close="onCloseAddFoldersModal"
|
|
/>
|
|
</Teleport>
|
|
|
|
<DeleteCustomViews
|
|
v-if="showDeleteFoldersModal"
|
|
v-model:show="showDeleteFoldersModal"
|
|
:active-custom-view="activeFolder"
|
|
:custom-views-id="foldersId"
|
|
:open-last-item-after-delete="openLastItemAfterDeleteInFolder"
|
|
@close="onCloseDeleteFoldersModal"
|
|
/>
|
|
|
|
<ChatTypeTabs
|
|
v-if="!hasAppliedFiltersOrActiveFolders"
|
|
:items="assigneeTabItems"
|
|
:active-tab="activeAssigneeTab"
|
|
is-compact
|
|
@chat-tab-change="updateAssigneeTab"
|
|
/>
|
|
|
|
<p
|
|
v-if="!chatListLoading && !conversationList.length"
|
|
class="flex items-center justify-center p-4 overflow-auto"
|
|
>
|
|
{{ $t('CHAT_LIST.LIST.404') }}
|
|
</p>
|
|
<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"
|
|
@assign-agent="onAssignAgent"
|
|
@update-conversations="onUpdateConversations"
|
|
@assign-labels="onAssignLabels"
|
|
@assign-team="onAssignTeamsForBulk"
|
|
/>
|
|
<div
|
|
ref="conversationListRef"
|
|
class="flex-1 overflow-hidden conversations-list hover:overflow-y-auto"
|
|
:class="{ 'overflow-hidden': isContextMenuOpen }"
|
|
>
|
|
<DynamicScroller
|
|
ref="conversationDynamicScroller"
|
|
:items="conversationList"
|
|
:min-item-size="24"
|
|
class="w-full h-full overflow-auto"
|
|
>
|
|
<template #default="{ item, index, active }">
|
|
<!--
|
|
If we encounter resizing issues, we can set the `watchData` prop to true
|
|
this will deeply watch the entire object instead of just size dependencies
|
|
But it can impact performance
|
|
-->
|
|
<DynamicScrollerItem
|
|
:item="item"
|
|
:active="active"
|
|
:data-index="index"
|
|
:size-dependencies="[
|
|
item.messages,
|
|
item.labels,
|
|
item.uuid,
|
|
item.inbox_id,
|
|
]"
|
|
>
|
|
<ConversationItem
|
|
:source="item"
|
|
:label="label"
|
|
:team-id="teamId"
|
|
:folders-id="foldersId"
|
|
:conversation-type="conversationType"
|
|
:show-assignee="showAssigneeInConversationCard"
|
|
@select-conversation="selectConversation"
|
|
@de-select-conversation="deSelectConversation"
|
|
/>
|
|
</DynamicScrollerItem>
|
|
</template>
|
|
<template #after>
|
|
<div v-if="chatListLoading" class="text-center">
|
|
<span class="mt-4 mb-4 spinner" />
|
|
</div>
|
|
<p
|
|
v-else-if="showEndOfListMessage"
|
|
class="p-4 text-center text-slate-400 dark:text-slate-300"
|
|
>
|
|
{{ $t('CHAT_LIST.EOF') }}
|
|
</p>
|
|
<IntersectionObserver
|
|
v-else
|
|
:options="intersectionObserverOptions"
|
|
@observed="loadMoreConversations"
|
|
/>
|
|
</template>
|
|
</DynamicScroller>
|
|
</div>
|
|
<Teleport v-if="showAdvancedFilters" to="#conversationFilterTeleportTarget">
|
|
<ConversationFilter
|
|
v-model="appliedFilter"
|
|
:folder-name="activeFolderName"
|
|
:is-folder-view="hasActiveFolders"
|
|
@apply-filter="onApplyFilter"
|
|
@update-folder="onUpdateSavedFilter"
|
|
@close="closeAdvanceFiltersModal"
|
|
/>
|
|
</Teleport>
|
|
</div>
|
|
</template>
|