Merge branch 'develop' into fix/hc-editor
This commit is contained in:
@@ -63,10 +63,37 @@ export const actions = {
|
||||
});
|
||||
}
|
||||
},
|
||||
update: async ({ commit }, updateObj) => {
|
||||
update: async ({ commit }, { options, ...updateObj }) => {
|
||||
if (options?.silent !== true) {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await AccountAPI.update('', updateObj);
|
||||
commit(types.default.EDIT_ACCOUNT, response.data);
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
delete: async ({ commit }, { id }) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
await AccountAPI.update('', updateObj);
|
||||
await AccountAPI.delete(id);
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
toggleDeletion: async (
|
||||
{ commit },
|
||||
{ action_type } = { action_type: 'delete' }
|
||||
) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
await EnterpriseAccountAPI.toggleDeletion(action_type);
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false });
|
||||
|
||||
@@ -12,6 +12,7 @@ export const state = {
|
||||
isCreating: false,
|
||||
isDeleting: false,
|
||||
isUpdating: false,
|
||||
isUpdatingAvatar: false,
|
||||
isFetchingAgentBot: false,
|
||||
isSettingAgentBot: false,
|
||||
isDisconnecting: false,
|
||||
@@ -48,10 +49,23 @@ export const actions = {
|
||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
create: async ({ commit }, agentBotObj) => {
|
||||
|
||||
create: async ({ commit }, botData) => {
|
||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isCreating: true });
|
||||
try {
|
||||
const response = await AgentBotsAPI.create(agentBotObj);
|
||||
// Create FormData for file upload
|
||||
const formData = new FormData();
|
||||
formData.append('name', botData.name);
|
||||
formData.append('description', botData.description);
|
||||
formData.append('bot_type', botData.bot_type || 'webhook');
|
||||
formData.append('outgoing_url', botData.outgoing_url);
|
||||
|
||||
// Add avatar file if available
|
||||
if (botData.avatar) {
|
||||
formData.append('avatar', botData.avatar);
|
||||
}
|
||||
|
||||
const response = await AgentBotsAPI.create(formData);
|
||||
commit(types.ADD_AGENT_BOT, response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
@@ -61,10 +75,22 @@ export const actions = {
|
||||
}
|
||||
return null;
|
||||
},
|
||||
update: async ({ commit }, { id, ...agentBotObj }) => {
|
||||
|
||||
update: async ({ commit }, { id, data }) => {
|
||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
const response = await AgentBotsAPI.update(id, agentBotObj);
|
||||
// Create FormData for file upload
|
||||
const formData = new FormData();
|
||||
formData.append('name', data.name);
|
||||
formData.append('description', data.description);
|
||||
formData.append('bot_type', data.bot_type || 'webhook');
|
||||
formData.append('outgoing_url', data.outgoing_url);
|
||||
|
||||
if (data.avatar) {
|
||||
formData.append('avatar', data.avatar);
|
||||
}
|
||||
|
||||
const response = await AgentBotsAPI.update(id, formData);
|
||||
commit(types.EDIT_AGENT_BOT, response.data);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
@@ -72,6 +98,7 @@ export const actions = {
|
||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdating: false });
|
||||
}
|
||||
},
|
||||
|
||||
delete: async ({ commit }, id) => {
|
||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isDeleting: true });
|
||||
try {
|
||||
@@ -83,6 +110,20 @@ export const actions = {
|
||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isDeleting: false });
|
||||
}
|
||||
},
|
||||
|
||||
deleteAgentBotAvatar: async ({ commit }, id) => {
|
||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdatingAvatar: true });
|
||||
try {
|
||||
await AgentBotsAPI.deleteAgentBotAvatar(id);
|
||||
// Update the thumbnail to empty string after deletion
|
||||
commit(types.UPDATE_AGENT_BOT_AVATAR, { id, thumbnail: '' });
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isUpdatingAvatar: false });
|
||||
}
|
||||
},
|
||||
|
||||
show: async ({ commit }, id) => {
|
||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isFetchingItem: true });
|
||||
try {
|
||||
@@ -131,6 +172,17 @@ export const actions = {
|
||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isDisconnecting: false });
|
||||
}
|
||||
},
|
||||
|
||||
resetAccessToken: async ({ commit }, botId) => {
|
||||
try {
|
||||
const response = await AgentBotsAPI.resetAccessToken(botId);
|
||||
commit(types.EDIT_AGENT_BOT, response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
@@ -150,6 +202,12 @@ export const mutations = {
|
||||
[inboxId]: agentBotId,
|
||||
};
|
||||
},
|
||||
[types.UPDATE_AGENT_BOT_AVATAR]($state, { id, thumbnail }) {
|
||||
const botIndex = $state.records.findIndex(bot => bot.id === id);
|
||||
if (botIndex !== -1) {
|
||||
$state.records[botIndex].thumbnail = thumbnail || '';
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -2,6 +2,8 @@ import types from '../mutation-types';
|
||||
import authAPI from '../../api/auth';
|
||||
|
||||
import { setUser, clearCookiesOnLogout } from '../utils/api';
|
||||
import SessionStorage from 'shared/helpers/sessionStorage';
|
||||
import { SESSION_STORAGE_KEYS } from 'dashboard/constants/sessionStorage';
|
||||
|
||||
const initialState = {
|
||||
currentUser: {
|
||||
@@ -133,6 +135,16 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
updatePassword: async ({ commit }, params) => {
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
try {
|
||||
const response = await authAPI.profilePasswordUpdate(params);
|
||||
commit(types.SET_CURRENT_USER, response.data);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
deleteAvatar: async ({ commit }) => {
|
||||
try {
|
||||
const response = await authAPI.deleteAvatar();
|
||||
@@ -145,8 +157,15 @@ export const actions = {
|
||||
updateUISettings: async ({ commit }, params) => {
|
||||
try {
|
||||
commit(types.SET_CURRENT_USER_UI_SETTINGS, params);
|
||||
const response = await authAPI.updateUISettings(params);
|
||||
commit(types.SET_CURRENT_USER, response.data);
|
||||
|
||||
const isImpersonating = SessionStorage.get(
|
||||
SESSION_STORAGE_KEYS.IMPERSONATION_USER
|
||||
);
|
||||
|
||||
if (!isImpersonating) {
|
||||
const response = await authAPI.updateUISettings(params);
|
||||
commit(types.SET_CURRENT_USER, response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
@@ -204,6 +223,16 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
resetAccessToken: async ({ commit }) => {
|
||||
try {
|
||||
const response = await authAPI.resetAccessToken();
|
||||
commit(types.SET_CURRENT_USER, response.data);
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
resendConfirmation: async () => {
|
||||
try {
|
||||
await authAPI.resendConfirmation();
|
||||
|
||||
@@ -3,6 +3,8 @@ import types from '../mutation-types';
|
||||
import CampaignsAPI from '../../api/campaigns';
|
||||
import AnalyticsHelper from '../../helper/AnalyticsHelper';
|
||||
import { CAMPAIGNS_EVENTS } from '../../helper/AnalyticsHelper/events';
|
||||
import { CAMPAIGN_TYPES } from 'shared/constants/campaign';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
|
||||
export const state = {
|
||||
records: [],
|
||||
@@ -16,10 +18,35 @@ export const getters = {
|
||||
getUIFlags(_state) {
|
||||
return _state.uiFlags;
|
||||
},
|
||||
getCampaigns: _state => campaignType => {
|
||||
return _state.records
|
||||
.filter(record => record.campaign_type === campaignType)
|
||||
.sort((a1, a2) => a1.id - a2.id);
|
||||
getCampaigns:
|
||||
_state =>
|
||||
(campaignType, inboxChannelTypes = null) => {
|
||||
let filteredRecords = _state.records.filter(
|
||||
record => record.campaign_type === campaignType
|
||||
);
|
||||
|
||||
if (inboxChannelTypes && Array.isArray(inboxChannelTypes)) {
|
||||
filteredRecords = filteredRecords.filter(record => {
|
||||
return (
|
||||
record.inbox &&
|
||||
inboxChannelTypes.includes(record.inbox.channel_type)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return filteredRecords.sort((a1, a2) => a1.id - a2.id);
|
||||
},
|
||||
getSMSCampaigns: (_state, _getters) => {
|
||||
const smsChannelTypes = [INBOX_TYPES.SMS, INBOX_TYPES.TWILIO];
|
||||
return _getters.getCampaigns(CAMPAIGN_TYPES.ONE_OFF, smsChannelTypes);
|
||||
},
|
||||
getWhatsAppCampaigns: (_state, _getters) => {
|
||||
const whatsappChannelTypes = [INBOX_TYPES.WHATSAPP];
|
||||
return _getters.getCampaigns(CAMPAIGN_TYPES.ONE_OFF, whatsappChannelTypes);
|
||||
},
|
||||
getLiveChatCampaigns: (_state, _getters) => {
|
||||
const liveChatChannelTypes = [INBOX_TYPES.WEB];
|
||||
return _getters.getCampaigns(CAMPAIGN_TYPES.ONGOING, liveChatChannelTypes);
|
||||
},
|
||||
getAllCampaigns: _state => {
|
||||
return _state.records;
|
||||
|
||||
@@ -75,6 +75,21 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
active: async ({ commit }, { page = 1, sortAttr } = {}) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const {
|
||||
data: { payload, meta },
|
||||
} = await ContactAPI.active(page, sortAttr);
|
||||
commit(types.CLEAR_CONTACTS);
|
||||
commit(types.SET_CONTACTS, payload);
|
||||
commit(types.SET_CONTACT_META, meta);
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
|
||||
show: async ({ commit }, { id }) => {
|
||||
commit(types.SET_CONTACT_UI_FLAG, { isFetchingItem: true });
|
||||
try {
|
||||
|
||||
@@ -5,12 +5,14 @@ export const initialState = {
|
||||
contactRecords: [],
|
||||
conversationRecords: [],
|
||||
messageRecords: [],
|
||||
articleRecords: [],
|
||||
uiFlags: {
|
||||
isFetching: false,
|
||||
isSearchCompleted: false,
|
||||
contact: { isFetching: false },
|
||||
conversation: { isFetching: false },
|
||||
message: { isFetching: false },
|
||||
article: { isFetching: false },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,6 +29,9 @@ export const getters = {
|
||||
getMessageRecords(state) {
|
||||
return state.messageRecords;
|
||||
},
|
||||
getArticleRecords(state) {
|
||||
return state.articleRecords;
|
||||
},
|
||||
getUIFlags(state) {
|
||||
return state.uiFlags;
|
||||
},
|
||||
@@ -65,6 +70,7 @@ export const actions = {
|
||||
dispatch('contactSearch', { q }),
|
||||
dispatch('conversationSearch', { q }),
|
||||
dispatch('messageSearch', { q }),
|
||||
dispatch('articleSearch', { q }),
|
||||
]);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
@@ -108,6 +114,17 @@ export const actions = {
|
||||
commit(types.MESSAGE_SEARCH_SET_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
async articleSearch({ commit }, { q, page = 1 }) {
|
||||
commit(types.ARTICLE_SEARCH_SET_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const { data } = await SearchAPI.articles({ q, page });
|
||||
commit(types.ARTICLE_SEARCH_SET, data.payload.articles);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
} finally {
|
||||
commit(types.ARTICLE_SEARCH_SET_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
async clearSearchResults({ commit }) {
|
||||
commit(types.CLEAR_SEARCH_RESULTS);
|
||||
},
|
||||
@@ -126,6 +143,9 @@ export const mutations = {
|
||||
[types.MESSAGE_SEARCH_SET](state, records) {
|
||||
state.messageRecords = [...state.messageRecords, ...records];
|
||||
},
|
||||
[types.ARTICLE_SEARCH_SET](state, records) {
|
||||
state.articleRecords = [...state.articleRecords, ...records];
|
||||
},
|
||||
[types.SEARCH_CONVERSATIONS_SET_UI_FLAG](state, uiFlags) {
|
||||
state.uiFlags = { ...state.uiFlags, ...uiFlags };
|
||||
},
|
||||
@@ -141,10 +161,14 @@ export const mutations = {
|
||||
[types.MESSAGE_SEARCH_SET_UI_FLAG](state, uiFlags) {
|
||||
state.uiFlags.message = { ...state.uiFlags.message, ...uiFlags };
|
||||
},
|
||||
[types.ARTICLE_SEARCH_SET_UI_FLAG](state, uiFlags) {
|
||||
state.uiFlags.article = { ...state.uiFlags.article, ...uiFlags };
|
||||
},
|
||||
[types.CLEAR_SEARCH_RESULTS](state) {
|
||||
state.contactRecords = [];
|
||||
state.conversationRecords = [];
|
||||
state.messageRecords = [];
|
||||
state.articleRecords = [];
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
import types from '../mutation-types';
|
||||
import ConversationApi from '../../api/inbox/conversation';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
|
||||
const state = {
|
||||
mineCount: 0,
|
||||
unAssignedCount: 0,
|
||||
allCount: 0,
|
||||
updatedOn: null,
|
||||
};
|
||||
|
||||
export const getters = {
|
||||
getStats: $state => $state,
|
||||
};
|
||||
|
||||
// Create a debounced version of the actual API call function
|
||||
const fetchMetaData = async (commit, params) => {
|
||||
try {
|
||||
const response = await ConversationApi.meta(params);
|
||||
const {
|
||||
data: { meta },
|
||||
} = response;
|
||||
commit(types.SET_CONV_TAB_META, meta);
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const debouncedFetchMetaData = debounce(fetchMetaData, 500, false, 1000);
|
||||
const longDebouncedFetchMetaData = debounce(fetchMetaData, 500, false, 5000);
|
||||
|
||||
export const actions = {
|
||||
get: async ({ commit, state: $state }, params) => {
|
||||
const currentTime = new Date();
|
||||
const lastUpdatedTime = new Date($state.updatedOn);
|
||||
|
||||
// Skip large accounts from making too many requests
|
||||
if (currentTime - lastUpdatedTime < 10000 && $state.allCount > 1000) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Skipping conversation meta fetch');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await ConversationApi.meta(params);
|
||||
const {
|
||||
data: { meta },
|
||||
} = response;
|
||||
commit(types.SET_CONV_TAB_META, meta);
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
if ($state.allCount > 100) {
|
||||
longDebouncedFetchMetaData(commit, params);
|
||||
} else {
|
||||
debouncedFetchMetaData(commit, params);
|
||||
}
|
||||
},
|
||||
set({ commit }, meta) {
|
||||
|
||||
@@ -327,6 +327,16 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
deleteConversation: async ({ commit, dispatch }, conversationId) => {
|
||||
try {
|
||||
await ConversationApi.delete(conversationId);
|
||||
commit(types.DELETE_CONVERSATION, conversationId);
|
||||
dispatch('conversationStats/get', {}, { root: true });
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
|
||||
addConversation({ commit, state, dispatch, rootState }, conversation) {
|
||||
const { currentInbox, appliedFilters } = state;
|
||||
const {
|
||||
@@ -489,6 +499,15 @@ const actions = {
|
||||
commit(types.SET_CONTEXT_MENU_CHAT_ID, chatId);
|
||||
},
|
||||
|
||||
getInboxCaptainAssistantById: async ({ commit }, conversationId) => {
|
||||
try {
|
||||
const response = await ConversationApi.getInboxAssistant(conversationId);
|
||||
commit(types.SET_INBOX_CAPTAIN_ASSISTANT, response.data);
|
||||
} catch (error) {
|
||||
// Handle error
|
||||
}
|
||||
},
|
||||
|
||||
...messageReadActions,
|
||||
...messageTranslateActions,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
||||
import { applyPageFilters, sortComparator } from './helpers';
|
||||
import { applyPageFilters, applyRoleFilter, sortComparator } from './helpers';
|
||||
import filterQueryGenerator from 'dashboard/helper/filterQueryGenerator';
|
||||
import { matchesFilters } from './helpers/filterHelpers';
|
||||
import {
|
||||
getUserPermissions,
|
||||
getUserRole,
|
||||
} from '../../../helper/permissionsHelper';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
|
||||
export const getSelectedChatConversation = ({
|
||||
@@ -13,6 +18,36 @@ const getters = {
|
||||
getAllConversations: ({ allConversations, chatSortFilter: sortKey }) => {
|
||||
return allConversations.sort((a, b) => sortComparator(a, b, sortKey));
|
||||
},
|
||||
getFilteredConversations: (
|
||||
{ allConversations, chatSortFilter, appliedFilters },
|
||||
_,
|
||||
__,
|
||||
rootGetters
|
||||
) => {
|
||||
const currentUser = rootGetters.getCurrentUser;
|
||||
const currentUserId = rootGetters.getCurrentUser.id;
|
||||
const currentAccountId = rootGetters.getCurrentAccountId;
|
||||
|
||||
const permissions = getUserPermissions(currentUser, currentAccountId);
|
||||
const userRole = getUserRole(currentUser, currentAccountId);
|
||||
|
||||
return allConversations
|
||||
.filter(conversation => {
|
||||
const matchesFilterResult = matchesFilters(
|
||||
conversation,
|
||||
appliedFilters
|
||||
);
|
||||
const allowedForRole = applyRoleFilter(
|
||||
conversation,
|
||||
userRole,
|
||||
permissions,
|
||||
currentUserId
|
||||
);
|
||||
|
||||
return matchesFilterResult && allowedForRole;
|
||||
})
|
||||
.sort((a, b) => sortComparator(a, b, chatSortFilter));
|
||||
},
|
||||
getSelectedChat: ({ selectedChatId, allConversations }) => {
|
||||
const selectedChat = allConversations.find(
|
||||
conversation => conversation.id === selectedChatId
|
||||
@@ -27,18 +62,12 @@ const getters = {
|
||||
const selectedChat = _getters.getSelectedChat;
|
||||
const { messages = [] } = selectedChat;
|
||||
const lastEmail = [...messages].reverse().find(message => {
|
||||
const {
|
||||
content_attributes: contentAttributes = {},
|
||||
message_type: messageType,
|
||||
} = message;
|
||||
const { email = {} } = contentAttributes;
|
||||
const isIncomingOrOutgoing =
|
||||
messageType === MESSAGE_TYPE.OUTGOING ||
|
||||
messageType === MESSAGE_TYPE.INCOMING;
|
||||
if (email.from && isIncomingOrOutgoing) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
const { message_type: messageType } = message;
|
||||
if (message.private) return false;
|
||||
|
||||
return [MESSAGE_TYPE.OUTGOING, MESSAGE_TYPE.INCOMING].includes(
|
||||
messageType
|
||||
);
|
||||
});
|
||||
|
||||
return lastEmail;
|
||||
@@ -73,10 +102,24 @@ const getters = {
|
||||
return isUnAssigned && shouldFilter;
|
||||
});
|
||||
},
|
||||
getAllStatusChats: _state => activeFilters => {
|
||||
getAllStatusChats: (_state, _, __, rootGetters) => activeFilters => {
|
||||
const currentUser = rootGetters.getCurrentUser;
|
||||
const currentUserId = rootGetters.getCurrentUser.id;
|
||||
const currentAccountId = rootGetters.getCurrentAccountId;
|
||||
|
||||
const permissions = getUserPermissions(currentUser, currentAccountId);
|
||||
const userRole = getUserRole(currentUser, currentAccountId);
|
||||
|
||||
return _state.allConversations.filter(conversation => {
|
||||
const shouldFilter = applyPageFilters(conversation, activeFilters);
|
||||
return shouldFilter;
|
||||
const allowedForRole = applyRoleFilter(
|
||||
conversation,
|
||||
userRole,
|
||||
permissions,
|
||||
currentUserId
|
||||
);
|
||||
|
||||
return shouldFilter && allowedForRole;
|
||||
});
|
||||
},
|
||||
getChatListLoadingStatus: ({ listLoadingStatus }) => listLoadingStatus,
|
||||
@@ -114,6 +157,10 @@ const getters = {
|
||||
getContextMenuChatId: _state => {
|
||||
return _state.contextMenuChatId;
|
||||
},
|
||||
|
||||
getCopilotAssistant: _state => {
|
||||
return _state.copilotAssistant;
|
||||
},
|
||||
};
|
||||
|
||||
export default getters;
|
||||
|
||||
@@ -62,6 +62,51 @@ export const applyPageFilters = (conversation, filters) => {
|
||||
return shouldFilter;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters conversations based on user role and permissions
|
||||
*
|
||||
* @param {Object} conversation - The conversation object to check permissions for
|
||||
* @param {string} role - The user's role (administrator, agent, etc.)
|
||||
* @param {Array<string>} permissions - List of permission strings the user has
|
||||
* @param {number|string} currentUserId - The ID of the current user
|
||||
* @returns {boolean} - Whether the user has permissions to access this conversation
|
||||
*/
|
||||
export const applyRoleFilter = (
|
||||
conversation,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
) => {
|
||||
// the role === "agent" check is typically not correct on it's own
|
||||
// the backend handles this by checking the custom_role_id at the user model
|
||||
// here however, the `getUserRole` returns "custom_role" if the id is present,
|
||||
// so we can check the role === "agent" directly
|
||||
if (['administrator', 'agent'].includes(role)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for full conversation management permission
|
||||
if (permissions.includes('conversation_manage')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const conversationAssignee = conversation.meta.assignee;
|
||||
const isUnassigned = !conversationAssignee;
|
||||
const isAssignedToUser = conversationAssignee?.id === currentUserId;
|
||||
|
||||
// Check unassigned management permission
|
||||
if (permissions.includes('conversation_unassigned_manage')) {
|
||||
return isUnassigned || isAssignedToUser;
|
||||
}
|
||||
|
||||
// Check participating conversation management permission
|
||||
if (permissions.includes('conversation_participating_manage')) {
|
||||
return isAssignedToUser;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const SORT_OPTIONS = {
|
||||
last_activity_at_asc: ['sortOnLastActivityAt', 'asc'],
|
||||
last_activity_at_desc: ['sortOnLastActivityAt', 'desc'],
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* Conversation Filter Helpers
|
||||
* ---------------------------
|
||||
* This file contains helper functions for filtering conversations in the frontend.
|
||||
* The filtering logic is designed to align with the backend SQL behavior to ensure
|
||||
* consistent results across the application.
|
||||
*
|
||||
* Key components:
|
||||
* 1. getValueFromConversation: Retrieves values from conversation objects, handling
|
||||
* both top-level properties and nested attributes.
|
||||
* 2. matchesCondition: Evaluates a single filter condition against a value.
|
||||
* 3. matchesFilters: Evaluates a complete filter chain against a conversation.
|
||||
* 4. buildJsonLogicRule: Transforms evaluated filters into a JSON Logic frule that
|
||||
* respects SQL-like operator precedence.
|
||||
*
|
||||
* Filter Structure:
|
||||
* -----------------
|
||||
* Each filter has the following structure:
|
||||
* {
|
||||
* attributeKey: 'status', // The attribute to filter on
|
||||
* filterOperator: 'equal_to', // The operator to use (equal_to, contains, etc.)
|
||||
* values: ['open'], // The values to compare against
|
||||
* queryOperator: 'and' // How this filter connects to the next one (and/or)
|
||||
* }
|
||||
*
|
||||
* Operator Precedence:
|
||||
* --------------------
|
||||
* The filter evaluation respects SQL-like operator precedence using JSON Logic:
|
||||
* https://www.postgresql.org/docs/17/sql-syntax-lexical.html#SQL-PRECEDENCE
|
||||
* 1. First evaluates individual conditions
|
||||
* 2. Then applies AND operators (groups consecutive AND conditions)
|
||||
* 3. Finally applies OR operators (connects AND groups with OR operations)
|
||||
*
|
||||
* This means that a filter chain like "A AND B OR C" is evaluated as "(A AND B) OR C",
|
||||
* and "A OR B AND C" is evaluated as "A OR (B AND C)".
|
||||
*
|
||||
* The implementation uses json-logic-js to apply these rules. The JsonLogic format is designed
|
||||
* to allow you to share rules (logic) between front-end and back-end code
|
||||
* Here we use json-logic-js to transform filter conditions into a nested JSON Logic structure that preserves proper
|
||||
* operator precedence, effectively mimicking SQL-like operator precedence.
|
||||
*
|
||||
* Conversation Object Structure:
|
||||
* -----------------------------
|
||||
* The conversation object can have:
|
||||
* 1. Top-level properties (status, priority, display_id, etc.)
|
||||
* 2. Nested properties in additional_attributes (browser_language, referer, etc.)
|
||||
* 3. Nested properties in custom_attributes (conversation_type, etc.)
|
||||
*/
|
||||
import jsonLogic from 'json-logic-js';
|
||||
import { coerceToDate } from '@chatwoot/utils';
|
||||
|
||||
/**
|
||||
* Gets a value from a conversation based on the attribute key
|
||||
* @param {Object} conversation - The conversation object
|
||||
* @param {String} attributeKey - The attribute key to get the value for
|
||||
* @returns {*} - The value of the attribute
|
||||
*
|
||||
* This function handles various attribute locations:
|
||||
* 1. Direct properties on the conversation object (status, priority, etc.)
|
||||
* 2. Properties in conversation.additional_attributes (browser_language, referer, etc.)
|
||||
* 3. Properties in conversation.custom_attributes (conversation_type, etc.)
|
||||
*/
|
||||
const getValueFromConversation = (conversation, attributeKey) => {
|
||||
switch (attributeKey) {
|
||||
case 'status':
|
||||
case 'priority':
|
||||
case 'display_id':
|
||||
case 'labels':
|
||||
case 'created_at':
|
||||
case 'last_activity_at':
|
||||
return conversation[attributeKey];
|
||||
case 'assignee_id':
|
||||
return conversation.meta?.assignee?.id;
|
||||
case 'inbox_id':
|
||||
return conversation.inbox_id;
|
||||
case 'team_id':
|
||||
return conversation.meta?.team?.id;
|
||||
case 'browser_language':
|
||||
case 'country_code':
|
||||
case 'referer':
|
||||
return conversation.additional_attributes?.[attributeKey];
|
||||
default:
|
||||
// Check if it's a custom attribute
|
||||
if (
|
||||
conversation.custom_attributes &&
|
||||
conversation.custom_attributes[attributeKey]
|
||||
) {
|
||||
return conversation.custom_attributes[attributeKey];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolves the value from an input candidate
|
||||
* @param {*} candidate - The input value to resolve
|
||||
* @returns {*} - If the candidate is an object with an id property, returns the id;
|
||||
* otherwise returns the candidate unchanged
|
||||
*
|
||||
* This helper function is used to normalize values, particularly when dealing with
|
||||
* objects that represent entities like users, teams, or inboxes where we want to
|
||||
* compare by ID rather than by the whole object.
|
||||
*/
|
||||
const resolveValue = candidate => {
|
||||
if (
|
||||
typeof candidate === 'object' &&
|
||||
candidate !== null &&
|
||||
'id' in candidate
|
||||
) {
|
||||
return candidate.id;
|
||||
}
|
||||
|
||||
return candidate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if two values are equal in the context of filtering
|
||||
* @param {*} filterValue - The filterValue value
|
||||
* @param {*} conversationValue - The conversationValue value
|
||||
* @returns {Boolean} - Returns true if the values are considered equal according to filtering rules
|
||||
*
|
||||
* This function handles various equality scenarios:
|
||||
* 1. When both values are arrays: checks if all items in filterValue exist in conversationValue
|
||||
* 2. When filterValue is an array but conversationValue is not: checks if conversationValue is included in filterValue
|
||||
* 3. Otherwise: performs strict equality comparison
|
||||
*/
|
||||
const equalTo = (filterValue, conversationValue) => {
|
||||
if (Array.isArray(filterValue)) {
|
||||
if (filterValue.includes('all')) return true;
|
||||
if (filterValue === 'all') return true;
|
||||
|
||||
if (Array.isArray(conversationValue)) {
|
||||
// For array values like labels, check if any of the filter values exist in the array
|
||||
return filterValue.every(val => conversationValue.includes(val));
|
||||
}
|
||||
|
||||
if (!Array.isArray(conversationValue)) {
|
||||
return filterValue.includes(conversationValue);
|
||||
}
|
||||
}
|
||||
|
||||
return conversationValue === filterValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the filterValue value is contained within the conversationValue value
|
||||
* @param {*} filterValue - The value to look for
|
||||
* @param {*} conversationValue - The value to search within
|
||||
* @returns {Boolean} - Returns true if filterValue is contained within conversationValue
|
||||
*
|
||||
* This function performs case-insensitive string containment checks.
|
||||
* It only works with string values and returns false for non-string types.
|
||||
*/
|
||||
const contains = (filterValue, conversationValue) => {
|
||||
if (typeof conversationValue === 'string') {
|
||||
return conversationValue.toLowerCase().includes(filterValue.toLowerCase());
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compares two date values using a comparison function
|
||||
* @param {*} conversationValue - The conversation value to compare
|
||||
* @param {*} filterValue - The filter value to compare against
|
||||
* @param {Function} compareFn - The comparison function to apply
|
||||
* @returns {Boolean} - Returns true if the comparison succeeds, false otherwise
|
||||
*/
|
||||
const compareDates = (conversationValue, filterValue, compareFn) => {
|
||||
const conversationDate = coerceToDate(conversationValue);
|
||||
|
||||
// In saved views, the filterValue might be returned as an Array
|
||||
// In conversation list, when filtering, the filterValue will be returned as a string
|
||||
const valueToCompare = Array.isArray(filterValue)
|
||||
? filterValue[0]
|
||||
: filterValue;
|
||||
const filterDate = coerceToDate(valueToCompare);
|
||||
|
||||
if (conversationDate === null || filterDate === null) return false;
|
||||
return compareFn(conversationDate, filterDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a value matches a filter condition
|
||||
* @param {*} conversationValue - The value to check
|
||||
* @param {Object} filter - The filter condition
|
||||
* @returns {Boolean} - Returns true if the value matches the filter
|
||||
*/
|
||||
const matchesCondition = (conversationValue, filter) => {
|
||||
const { filter_operator: filterOperator, values } = filter;
|
||||
|
||||
// Handle null/undefined values
|
||||
if (conversationValue === null || conversationValue === undefined) {
|
||||
return filterOperator === 'is_not_present';
|
||||
}
|
||||
|
||||
const filterValue = Array.isArray(values)
|
||||
? values.map(resolveValue)
|
||||
: resolveValue(values);
|
||||
|
||||
switch (filterOperator) {
|
||||
case 'equal_to':
|
||||
return equalTo(filterValue, conversationValue);
|
||||
|
||||
case 'not_equal_to':
|
||||
return !equalTo(filterValue, conversationValue);
|
||||
|
||||
case 'contains':
|
||||
return contains(filterValue, conversationValue);
|
||||
|
||||
case 'does_not_contain':
|
||||
return !contains(filterValue, conversationValue);
|
||||
|
||||
case 'is_present':
|
||||
return true; // We already handled null/undefined above
|
||||
|
||||
case 'is_not_present':
|
||||
return false; // We already handled null/undefined above
|
||||
|
||||
case 'is_greater_than':
|
||||
return compareDates(conversationValue, filterValue, (a, b) => a > b);
|
||||
|
||||
case 'is_less_than':
|
||||
return compareDates(conversationValue, filterValue, (a, b) => a < b);
|
||||
|
||||
case 'days_before': {
|
||||
const today = new Date();
|
||||
const daysInMilliseconds = filterValue * 24 * 60 * 60 * 1000;
|
||||
const targetDate = new Date(today.getTime() - daysInMilliseconds);
|
||||
return conversationValue < targetDate.getTime();
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an array of evaluated filters into a JSON Logic rule
|
||||
* that respects SQL-like operator precedence (AND before OR)
|
||||
*
|
||||
* This function transforms the linear sequence of filter results and operators
|
||||
* into a nested JSON Logic structure that correctly implements SQL-like precedence:
|
||||
* - AND operators are evaluated before OR operators
|
||||
* - Consecutive AND conditions are grouped together
|
||||
* - These AND groups are then connected with OR operators
|
||||
*
|
||||
* For example:
|
||||
* - "A AND B AND C" becomes { "and": [A, B, C] }
|
||||
* - "A OR B OR C" becomes { "or": [A, B, C] }
|
||||
* - "A AND B OR C" becomes { "or": [{ "and": [A, B] }, C] }
|
||||
* - "A OR B AND C" becomes { "or": [A, { "and": [B, C] }] }
|
||||
*
|
||||
* FILTER CHAIN: A --AND--> B --OR--> C --AND--> D --AND--> E --OR--> F
|
||||
* | | | | | |
|
||||
* v v v v v v
|
||||
* EVALUATED: true false true false true false
|
||||
* \ / \ \ / /
|
||||
* \ / \ \ / /
|
||||
* \ / \ \ / /
|
||||
* \ / \ \ / /
|
||||
* \ / \ \ / /
|
||||
* AND GROUPS: [true,false] [true,false,true] [false]
|
||||
* | | |
|
||||
* v v v
|
||||
* JSON LOGIC: {"and":[true,false]} {"and":[true,false,true]} false
|
||||
* \ | /
|
||||
* \ | /
|
||||
* \ | /
|
||||
* \ | /
|
||||
* \ | /
|
||||
* FINAL RULE: {"or":[{"and":[true,false]},{"and":[true,false,true]},false]}
|
||||
*
|
||||
* {
|
||||
* "or": [
|
||||
* { "and": [true, false] },
|
||||
* { "and": [true, false, true] },
|
||||
* { "and": [false] }
|
||||
* ]
|
||||
* }
|
||||
* @param {Array} evaluatedFilters - Array of evaluated filter conditions with results and operators
|
||||
* @returns {Object} - JSON Logic rule
|
||||
*/
|
||||
const buildJsonLogicRule = evaluatedFilters => {
|
||||
// Step 1: Group consecutive AND conditions into logical units
|
||||
// This implements the higher precedence of AND over OR
|
||||
const andGroups = [];
|
||||
let currentAndGroup = [evaluatedFilters[0].result];
|
||||
|
||||
for (let i = 0; i < evaluatedFilters.length - 1; i += 1) {
|
||||
if (evaluatedFilters[i].operator === 'and') {
|
||||
// When we see an AND operator, we add the next filter to the current AND group
|
||||
// This builds up chains of AND conditions that will be evaluated together
|
||||
currentAndGroup.push(evaluatedFilters[i + 1].result);
|
||||
} else {
|
||||
// When we see an OR operator, it marks the boundary between AND groups
|
||||
// We finalize the current AND group and start a new one
|
||||
|
||||
// If the AND group has only one item, don't wrap it in an "and" operator
|
||||
// Otherwise, create a proper "and" JSON Logic expression
|
||||
andGroups.push(
|
||||
currentAndGroup.length === 1
|
||||
? currentAndGroup[0] // Single item doesn't need an "and" wrapper
|
||||
: { and: currentAndGroup } // Multiple items need to be AND-ed together
|
||||
);
|
||||
|
||||
// Start a new AND group with the next filter's result
|
||||
currentAndGroup = [evaluatedFilters[i + 1].result];
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Add the final AND group that wasn't followed by an OR
|
||||
if (currentAndGroup.length > 0) {
|
||||
andGroups.push(
|
||||
currentAndGroup.length === 1
|
||||
? currentAndGroup[0] // Single item doesn't need an "and" wrapper
|
||||
: { and: currentAndGroup } // Multiple items need to be AND-ed together
|
||||
);
|
||||
}
|
||||
|
||||
// Step 3: Combine all AND groups with OR operators
|
||||
// If we have multiple AND groups, they are separated by OR operators
|
||||
// in the original filter chain, so we combine them with an "or" operation
|
||||
if (andGroups.length > 1) {
|
||||
return { or: andGroups };
|
||||
}
|
||||
|
||||
// If there's only one AND group (which might be a single condition
|
||||
// or multiple AND-ed conditions), just return it directly
|
||||
return andGroups[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Evaluates each filter against the conversation and prepares the results array
|
||||
* @param {Object} conversation - The conversation to evaluate
|
||||
* @param {Array} filters - Filters to apply
|
||||
* @returns {Array} - Array of evaluated filter results with operators
|
||||
*/
|
||||
const evaluateFilters = (conversation, filters) => {
|
||||
return filters.map((filter, index) => {
|
||||
const value = getValueFromConversation(conversation, filter.attribute_key);
|
||||
const result = matchesCondition(value, filter);
|
||||
|
||||
// This part determines the logical operator that connects this filter to the next one:
|
||||
// - If this is not the last filter (index < filters.length - 1), use the filter's query_operator
|
||||
// or default to 'and' if query_operator is not specified
|
||||
// - If this is the last filter, set operator to null since there's no next filter to connect to
|
||||
const isLastFilter = index === filters.length - 1;
|
||||
const operator = isLastFilter ? null : filter.query_operator || 'and';
|
||||
|
||||
return { result, operator };
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a conversation matches the given filters
|
||||
* @param {Object} conversation - The conversation object to check
|
||||
* @param {Array} filters - Array of filter conditions
|
||||
* @returns {Boolean} - Returns true if conversation matches filters, false otherwise
|
||||
*/
|
||||
export const matchesFilters = (conversation, filters) => {
|
||||
// If no filters, return true
|
||||
if (!filters || filters.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle single filter case
|
||||
if (filters.length === 1) {
|
||||
const value = getValueFromConversation(
|
||||
conversation,
|
||||
filters[0].attribute_key
|
||||
);
|
||||
|
||||
return matchesCondition(value, filters[0]);
|
||||
}
|
||||
|
||||
// Evaluate all conditions and prepare for jsonLogic
|
||||
const evaluatedFilters = evaluateFilters(conversation, filters);
|
||||
return jsonLogic.apply(buildJsonLogicRule(evaluatedFilters));
|
||||
};
|
||||
+1603
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ const state = {
|
||||
conversationLastSeen: null,
|
||||
syncConversationsMessages: {},
|
||||
conversationFilters: {},
|
||||
copilotAssistant: {},
|
||||
};
|
||||
|
||||
// mutations
|
||||
@@ -77,10 +78,7 @@ export const mutations = {
|
||||
}
|
||||
},
|
||||
[types.SET_ALL_ATTACHMENTS](_state, { id, data }) {
|
||||
const attachments = _state.attachments[id] || [];
|
||||
|
||||
attachments.push(...data);
|
||||
_state.attachments[id] = [...attachments];
|
||||
_state.attachments[id] = [...data];
|
||||
},
|
||||
[types.SET_MISSING_MESSAGES](_state, { id, data }) {
|
||||
const [chat] = _state.allConversations.filter(c => c.id === id);
|
||||
@@ -206,18 +204,26 @@ export const mutations = {
|
||||
_state.allConversations.push(conversation);
|
||||
},
|
||||
|
||||
[types.DELETE_CONVERSATION](_state, conversationId) {
|
||||
_state.allConversations = _state.allConversations.filter(
|
||||
c => c.id !== conversationId
|
||||
);
|
||||
},
|
||||
|
||||
[types.UPDATE_CONVERSATION](_state, conversation) {
|
||||
const { allConversations } = _state;
|
||||
const currentConversationIndex = allConversations.findIndex(
|
||||
c => c.id === conversation.id
|
||||
);
|
||||
if (currentConversationIndex > -1) {
|
||||
const { messages, ...conversationAttributes } = conversation;
|
||||
const currentConversation = {
|
||||
...allConversations[currentConversationIndex],
|
||||
...conversationAttributes,
|
||||
};
|
||||
allConversations[currentConversationIndex] = currentConversation;
|
||||
const index = allConversations.findIndex(c => c.id === conversation.id);
|
||||
|
||||
if (index > -1) {
|
||||
const selectedConversation = allConversations[index];
|
||||
|
||||
// ignore out of order events
|
||||
if (conversation.updated_at < selectedConversation.updated_at) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { messages, ...updates } = conversation;
|
||||
allConversations[index] = { ...selectedConversation, ...updates };
|
||||
if (_state.selectedChatId === conversation.id) {
|
||||
emitter.emit(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS);
|
||||
emitter.emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
|
||||
@@ -309,6 +315,9 @@ export const mutations = {
|
||||
[types.UPDATE_CHAT_LIST_FILTERS](_state, data) {
|
||||
_state.conversationFilters = { ..._state.conversationFilters, ...data };
|
||||
},
|
||||
[types.SET_INBOX_CAPTAIN_ASSISTANT](_state, data) {
|
||||
_state.copilotAssistant = data.assistant;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { applyRoleFilter } from '../helpers';
|
||||
|
||||
describe('Conversation Helpers', () => {
|
||||
describe('#applyRoleFilter', () => {
|
||||
// Test data for conversations
|
||||
const conversationWithAssignee = {
|
||||
meta: {
|
||||
assignee: {
|
||||
id: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const conversationWithDifferentAssignee = {
|
||||
meta: {
|
||||
assignee: {
|
||||
id: 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const conversationWithoutAssignee = {
|
||||
meta: {
|
||||
assignee: null,
|
||||
},
|
||||
};
|
||||
|
||||
// Test for administrator role
|
||||
it('always returns true for administrator role regardless of permissions', () => {
|
||||
const role = 'administrator';
|
||||
const permissions = [];
|
||||
const currentUserId = 1;
|
||||
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithDifferentAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithoutAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// Test for agent role
|
||||
it('always returns true for agent role regardless of permissions', () => {
|
||||
const role = 'agent';
|
||||
const permissions = [];
|
||||
const currentUserId = 1;
|
||||
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithDifferentAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithoutAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// Test for custom role with 'conversation_manage' permission
|
||||
it('returns true for any user with conversation_manage permission', () => {
|
||||
const role = 'custom_role';
|
||||
const permissions = ['conversation_manage'];
|
||||
const currentUserId = 1;
|
||||
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithDifferentAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithoutAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
// Test for custom role with 'conversation_unassigned_manage' permission
|
||||
describe('with conversation_unassigned_manage permission', () => {
|
||||
const role = 'custom_role';
|
||||
const permissions = ['conversation_unassigned_manage'];
|
||||
const currentUserId = 1;
|
||||
|
||||
it('returns true for conversations assigned to the user', () => {
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true for unassigned conversations', () => {
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithoutAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for conversations assigned to other users', () => {
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithDifferentAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Test for custom role with 'conversation_participating_manage' permission
|
||||
describe('with conversation_participating_manage permission', () => {
|
||||
const role = 'custom_role';
|
||||
const permissions = ['conversation_participating_manage'];
|
||||
const currentUserId = 1;
|
||||
|
||||
it('returns true for conversations assigned to the user', () => {
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for unassigned conversations', () => {
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithoutAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false for conversations assigned to other users', () => {
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithDifferentAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Test for user with no relevant permissions
|
||||
it('returns false for custom role without any relevant permissions', () => {
|
||||
const role = 'custom_role';
|
||||
const permissions = ['some_other_permission'];
|
||||
const currentUserId = 1;
|
||||
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithDifferentAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithoutAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
// Test edge cases for meta.assignee
|
||||
describe('handles edge cases with meta.assignee', () => {
|
||||
const role = 'custom_role';
|
||||
const permissions = ['conversation_unassigned_manage'];
|
||||
const currentUserId = 1;
|
||||
|
||||
it('treats undefined assignee as unassigned', () => {
|
||||
const conversationWithUndefinedAssignee = {
|
||||
meta: {
|
||||
assignee: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithUndefinedAssignee,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('handles empty meta object', () => {
|
||||
const conversationWithEmptyMeta = {
|
||||
meta: {},
|
||||
};
|
||||
|
||||
expect(
|
||||
applyRoleFilter(
|
||||
conversationWithEmptyMeta,
|
||||
role,
|
||||
permissions,
|
||||
currentUserId
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -116,4 +116,24 @@ export const actions = {
|
||||
isSwitching,
|
||||
});
|
||||
},
|
||||
|
||||
sendCnameInstructions: async (_, { portalSlug, email }) => {
|
||||
try {
|
||||
await portalAPIs.sendCnameInstructions(portalSlug, email);
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
}
|
||||
},
|
||||
|
||||
sslStatus: async ({ commit }, { portalSlug }) => {
|
||||
try {
|
||||
commit(types.SET_UI_FLAG, { isFetchingSSLStatus: true });
|
||||
const { data } = await portalAPIs.sslStatus(portalSlug);
|
||||
commit(types.SET_SSL_SETTINGS, { portalSlug, sslSettings: data });
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.SET_UI_FLAG, { isFetchingSSLStatus: false });
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export const getters = {
|
||||
isFetchingPortals: state => state.uiFlags.isFetching,
|
||||
isCreatingPortal: state => state.uiFlags.isCreating,
|
||||
isSwitchingPortal: state => state.uiFlags.isSwitching,
|
||||
isFetchingSSLStatus: state => state.uiFlags.isFetchingSSLStatus,
|
||||
portalBySlug:
|
||||
(...getterArguments) =>
|
||||
portalId => {
|
||||
|
||||
@@ -6,6 +6,7 @@ export const defaultPortalFlags = {
|
||||
isFetching: false,
|
||||
isUpdating: false,
|
||||
isDeleting: false,
|
||||
isFetchingSSLStatus: false,
|
||||
};
|
||||
|
||||
const state = {
|
||||
|
||||
@@ -13,6 +13,7 @@ export const types = {
|
||||
REMOVE_PORTAL_ID: 'removePortalId',
|
||||
SET_HELP_PORTAL_UI_FLAG: 'setHelpCenterUIFlag',
|
||||
SET_PORTAL_SWITCHING_FLAG: 'setPortalSwitchingFlag',
|
||||
SET_SSL_SETTINGS: 'setSSLSettings',
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
@@ -110,4 +111,18 @@ export const mutations = {
|
||||
[types.SET_PORTAL_SWITCHING_FLAG]($state, { isSwitching }) {
|
||||
$state.uiFlags.isSwitching = isSwitching;
|
||||
},
|
||||
|
||||
[types.SET_SSL_SETTINGS]($state, { portalSlug, sslSettings }) {
|
||||
const portal = $state.portals.byId[portalSlug];
|
||||
$state.portals.byId = {
|
||||
...$state.portals.byId,
|
||||
[portalSlug]: {
|
||||
...portal,
|
||||
ssl_settings: {
|
||||
...portal.ssl_settings,
|
||||
...sslSettings,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -135,6 +135,36 @@ describe('#actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#sslStatus', () => {
|
||||
it('commits SET_SSL_SETTINGS with data from API', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: { status: 'active', verification_errors: [] },
|
||||
});
|
||||
await actions.sslStatus({ commit }, { portalSlug: 'domain' });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_UI_FLAG, { isFetchingSSLStatus: true }],
|
||||
[
|
||||
types.SET_SSL_SETTINGS,
|
||||
{
|
||||
portalSlug: 'domain',
|
||||
sslSettings: { status: 'active', verification_errors: [] },
|
||||
},
|
||||
],
|
||||
[types.SET_UI_FLAG, { isFetchingSSLStatus: false }],
|
||||
]);
|
||||
});
|
||||
it('throws error and does not commit when API fails', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'error' });
|
||||
await expect(
|
||||
actions.sslStatus({ commit }, { portalSlug: 'domain' })
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_UI_FLAG, { isFetchingSSLStatus: true }],
|
||||
[types.SET_UI_FLAG, { isFetchingSSLStatus: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.delete.mockResolvedValue({});
|
||||
|
||||
@@ -89,6 +89,25 @@ describe('#mutations', () => {
|
||||
isFetching: true,
|
||||
isUpdating: false,
|
||||
isDeleting: false,
|
||||
isFetchingSSLStatus: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('[types.SET_SSL_SETTINGS]', () => {
|
||||
it('merges new ssl settings into existing portal.ssl_settings', () => {
|
||||
state.portals.byId.domain = {
|
||||
slug: 'domain',
|
||||
ssl_settings: { cf_status: 'pending' },
|
||||
};
|
||||
mutations[types.SET_SSL_SETTINGS](state, {
|
||||
portalSlug: 'domain',
|
||||
sslSettings: { status: 'active', verification_errors: ['error'] },
|
||||
});
|
||||
expect(state.portals.byId.domain.ssl_settings).toEqual({
|
||||
cf_status: 'pending',
|
||||
status: 'active',
|
||||
verification_errors: ['error'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,33 +5,12 @@ import InboxesAPI from '../../api/inboxes';
|
||||
import WebChannel from '../../api/channel/webChannel';
|
||||
import FBChannel from '../../api/channel/fbChannel';
|
||||
import TwilioChannel from '../../api/channel/twilioChannel';
|
||||
import WhatsappChannel from '../../api/channel/whatsappChannel';
|
||||
import { throwErrorMessage } from '../utils/api';
|
||||
import AnalyticsHelper from '../../helper/AnalyticsHelper';
|
||||
import camelcaseKeys from 'camelcase-keys';
|
||||
import { ACCOUNT_EVENTS } from '../../helper/AnalyticsHelper/events';
|
||||
|
||||
const buildInboxData = inboxParams => {
|
||||
const formData = new FormData();
|
||||
const { channel = {}, ...inboxProperties } = inboxParams;
|
||||
Object.keys(inboxProperties).forEach(key => {
|
||||
formData.append(key, inboxProperties[key]);
|
||||
});
|
||||
const { selectedFeatureFlags, ...channelParams } = channel;
|
||||
// selectedFeatureFlags needs to be empty when creating a website channel
|
||||
if (selectedFeatureFlags) {
|
||||
if (selectedFeatureFlags.length) {
|
||||
selectedFeatureFlags.forEach(featureFlag => {
|
||||
formData.append(`channel[selected_feature_flags][]`, featureFlag);
|
||||
});
|
||||
} else {
|
||||
formData.append('channel[selected_feature_flags][]', '');
|
||||
}
|
||||
}
|
||||
Object.keys(channelParams).forEach(key => {
|
||||
formData.append(`channel[${key}]`, channel[key]);
|
||||
});
|
||||
return formData;
|
||||
};
|
||||
import { channelActions, buildInboxData } from './inboxes/channelActions';
|
||||
|
||||
export const state = {
|
||||
records: [],
|
||||
@@ -65,15 +44,52 @@ export const getters = {
|
||||
const messagesTemplates =
|
||||
whatsAppMessageTemplates || apiInboxMessageTemplates;
|
||||
|
||||
// filtering out the whatsapp templates with media
|
||||
if (messagesTemplates instanceof Array) {
|
||||
return messagesTemplates.filter(template => {
|
||||
return !template.components.some(
|
||||
i => i.format === 'IMAGE' || i.format === 'VIDEO'
|
||||
);
|
||||
});
|
||||
return messagesTemplates;
|
||||
},
|
||||
getFilteredWhatsAppTemplates: $state => inboxId => {
|
||||
const [inbox] = $state.records.filter(
|
||||
record => record.id === Number(inboxId)
|
||||
);
|
||||
|
||||
const {
|
||||
message_templates: whatsAppMessageTemplates,
|
||||
additional_attributes: additionalAttributes,
|
||||
} = inbox || {};
|
||||
|
||||
const { message_templates: apiInboxMessageTemplates } =
|
||||
additionalAttributes || {};
|
||||
const templates = whatsAppMessageTemplates || apiInboxMessageTemplates;
|
||||
|
||||
if (!templates || !Array.isArray(templates)) {
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
|
||||
return templates.filter(template => {
|
||||
// Ensure template has required properties
|
||||
if (!template || !template.status || !template.components) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only show approved templates
|
||||
if (template.status.toLowerCase() !== 'approved') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter out interactive templates (LIST, PRODUCT, CATALOG), location templates, and call permission templates
|
||||
const hasUnsupportedComponents = template.components.some(
|
||||
component =>
|
||||
['LIST', 'PRODUCT', 'CATALOG', 'CALL_PERMISSION_REQUEST'].includes(
|
||||
component.type
|
||||
) ||
|
||||
(component.type === 'HEADER' && component.format === 'LOCATION')
|
||||
);
|
||||
|
||||
if (hasUnsupportedComponents) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
getNewConversationInboxes($state) {
|
||||
return $state.records.filter(inbox => {
|
||||
@@ -117,11 +133,30 @@ export const getters = {
|
||||
(item.channel_type === INBOX_TYPES.TWILIO && item.medium === 'sms')
|
||||
);
|
||||
},
|
||||
getWhatsAppInboxes($state) {
|
||||
return $state.records.filter(
|
||||
item => item.channel_type === INBOX_TYPES.WHATSAPP
|
||||
);
|
||||
},
|
||||
dialogFlowEnabledInboxes($state) {
|
||||
return $state.records.filter(
|
||||
item => item.channel_type !== INBOX_TYPES.EMAIL
|
||||
);
|
||||
},
|
||||
getFacebookInboxByInstagramId: $state => instagramId => {
|
||||
return $state.records.find(
|
||||
item =>
|
||||
item.instagram_id === instagramId &&
|
||||
item.channel_type === INBOX_TYPES.FB
|
||||
);
|
||||
},
|
||||
getInstagramInboxByInstagramId: $state => instagramId => {
|
||||
return $state.records.find(
|
||||
item =>
|
||||
item.instagram_id === instagramId &&
|
||||
item.channel_type === INBOX_TYPES.INSTAGRAM
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const sendAnalyticsEvent = channelType => {
|
||||
@@ -206,6 +241,25 @@ export const actions = {
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
createWhatsAppEmbeddedSignup: async ({ commit }, params) => {
|
||||
try {
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true });
|
||||
const response = await WhatsappChannel.createEmbeddedSignup(params);
|
||||
commit(types.default.ADD_INBOXES, response.data);
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||
sendAnalyticsEvent('whatsapp');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
...channelActions,
|
||||
// TODO: Extract other create channel methods to separate files to reduce file size
|
||||
// - createChannel
|
||||
// - createWebsiteChannel
|
||||
// - createTwilioChannel
|
||||
// - createFBChannel
|
||||
updateInbox: async ({ commit }, { id, formData = true, ...inboxParams }) => {
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
@@ -268,6 +322,13 @@ export const actions = {
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
syncTemplates: async (_, inboxId) => {
|
||||
try {
|
||||
await InboxesAPI.syncTemplates(inboxId);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as types from '../../mutation-types';
|
||||
import InboxesAPI from '../../../api/inboxes';
|
||||
import AnalyticsHelper from '../../../helper/AnalyticsHelper';
|
||||
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||
|
||||
export const buildInboxData = inboxParams => {
|
||||
const formData = new FormData();
|
||||
const { channel = {}, ...inboxProperties } = inboxParams;
|
||||
Object.keys(inboxProperties).forEach(key => {
|
||||
formData.append(key, inboxProperties[key]);
|
||||
});
|
||||
const { selectedFeatureFlags, ...channelParams } = channel;
|
||||
// selectedFeatureFlags needs to be empty when creating a website channel
|
||||
if (selectedFeatureFlags) {
|
||||
if (selectedFeatureFlags.length) {
|
||||
selectedFeatureFlags.forEach(featureFlag => {
|
||||
formData.append(`channel[selected_feature_flags][]`, featureFlag);
|
||||
});
|
||||
} else {
|
||||
formData.append('channel[selected_feature_flags][]', '');
|
||||
}
|
||||
}
|
||||
Object.keys(channelParams).forEach(key => {
|
||||
formData.append(`channel[${key}]`, channel[key]);
|
||||
});
|
||||
return formData;
|
||||
};
|
||||
|
||||
const sendAnalyticsEvent = channelType => {
|
||||
AnalyticsHelper.track(ACCOUNT_EVENTS.ADDED_AN_INBOX, {
|
||||
channelType,
|
||||
});
|
||||
};
|
||||
|
||||
export const channelActions = {
|
||||
createVoiceChannel: async ({ commit }, params) => {
|
||||
try {
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: true });
|
||||
const response = await InboxesAPI.create({
|
||||
name: params.name,
|
||||
channel: { ...params.voice, type: 'voice' },
|
||||
});
|
||||
commit(types.default.ADD_INBOXES, response.data);
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||
sendAnalyticsEvent('voice');
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
commit(types.default.SET_INBOXES_UI_FLAG, { isCreating: false });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -26,6 +26,9 @@ export const getters = {
|
||||
.filter(record => record.show_on_sidebar)
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
},
|
||||
getLabelById: _state => id => {
|
||||
return _state.records.find(record => record.id === Number(id));
|
||||
},
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
/* eslint no-console: 0 */
|
||||
import * as types from '../mutation-types';
|
||||
import { STATUS } from '../constants';
|
||||
import Report from '../../api/reports';
|
||||
import { downloadCsvFile, generateFileName } from '../../helper/downloadHelper';
|
||||
import AnalyticsHelper from '../../helper/AnalyticsHelper';
|
||||
import { REPORTS_EVENTS } from '../../helper/AnalyticsHelper/events';
|
||||
import {
|
||||
reconcileHeatmapData,
|
||||
clampDataBetweenTimeline,
|
||||
} from 'shared/helpers/ReportsDataHelper';
|
||||
import { clampDataBetweenTimeline } from 'shared/helpers/ReportsDataHelper';
|
||||
import liveReports from '../../api/liveReports';
|
||||
|
||||
const state = {
|
||||
fetchingStatus: false,
|
||||
accountSummaryFetchingStatus: STATUS.FINISHED,
|
||||
botSummaryFetchingStatus: STATUS.FINISHED,
|
||||
accountReport: {
|
||||
isFetching: {
|
||||
conversations_count: false,
|
||||
@@ -57,10 +58,12 @@ const state = {
|
||||
isFetchingAccountConversationMetric: false,
|
||||
isFetchingAccountConversationsHeatmap: false,
|
||||
isFetchingAgentConversationMetric: false,
|
||||
isFetchingTeamConversationMetric: false,
|
||||
},
|
||||
accountConversationMetric: {},
|
||||
accountConversationHeatmap: [],
|
||||
agentConversationMetric: [],
|
||||
teamConversationMetric: [],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -74,6 +77,12 @@ const getters = {
|
||||
getBotSummary(_state) {
|
||||
return _state.botSummary;
|
||||
},
|
||||
getAccountSummaryFetchingStatus(_state) {
|
||||
return _state.accountSummaryFetchingStatus;
|
||||
},
|
||||
getBotSummaryFetchingStatus(_state) {
|
||||
return _state.botSummaryFetchingStatus;
|
||||
},
|
||||
getAccountConversationMetric(_state) {
|
||||
return _state.overview.accountConversationMetric;
|
||||
},
|
||||
@@ -83,6 +92,9 @@ const getters = {
|
||||
getAgentConversationMetric(_state) {
|
||||
return _state.overview.agentConversationMetric;
|
||||
},
|
||||
getTeamConversationMetric(_state) {
|
||||
return _state.overview.teamConversationMetric;
|
||||
},
|
||||
getOverviewUIFlags($state) {
|
||||
return $state.overview.uiFlags;
|
||||
},
|
||||
@@ -114,16 +126,12 @@ export const actions = {
|
||||
let { data } = heatmapData;
|
||||
data = clampDataBetweenTimeline(data, reportObj.from, reportObj.to);
|
||||
|
||||
data = reconcileHeatmapData(
|
||||
data,
|
||||
state.overview.accountConversationHeatmap
|
||||
);
|
||||
|
||||
commit(types.default.SET_HEATMAP_DATA, data);
|
||||
commit(types.default.TOGGLE_HEATMAP_LOADING, false);
|
||||
});
|
||||
},
|
||||
fetchAccountSummary({ commit }, reportObj) {
|
||||
commit(types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FETCHING);
|
||||
Report.getSummary(
|
||||
reportObj.from,
|
||||
reportObj.to,
|
||||
@@ -134,12 +142,14 @@ export const actions = {
|
||||
)
|
||||
.then(accountSummary => {
|
||||
commit(types.default.SET_ACCOUNT_SUMMARY, accountSummary.data);
|
||||
commit(types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FINISHED);
|
||||
})
|
||||
.catch(() => {
|
||||
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, false);
|
||||
commit(types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FAILED);
|
||||
});
|
||||
},
|
||||
fetchBotSummary({ commit }, reportObj) {
|
||||
commit(types.default.SET_BOT_SUMMARY_STATUS, STATUS.FETCHING);
|
||||
Report.getBotSummary({
|
||||
from: reportObj.from,
|
||||
to: reportObj.to,
|
||||
@@ -148,14 +158,16 @@ export const actions = {
|
||||
})
|
||||
.then(botSummary => {
|
||||
commit(types.default.SET_BOT_SUMMARY, botSummary.data);
|
||||
commit(types.default.SET_BOT_SUMMARY_STATUS, STATUS.FINISHED);
|
||||
})
|
||||
.catch(() => {
|
||||
commit(types.default.TOGGLE_ACCOUNT_REPORT_LOADING, false);
|
||||
commit(types.default.SET_BOT_SUMMARY_STATUS, STATUS.FAILED);
|
||||
});
|
||||
},
|
||||
fetchAccountConversationMetric({ commit }, reportObj) {
|
||||
fetchAccountConversationMetric({ commit }, params = {}) {
|
||||
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, true);
|
||||
Report.getConversationMetric(reportObj.type)
|
||||
liveReports
|
||||
.getConversationMetric(params)
|
||||
.then(accountConversationMetric => {
|
||||
commit(
|
||||
types.default.SET_ACCOUNT_CONVERSATION_METRIC,
|
||||
@@ -167,9 +179,10 @@ export const actions = {
|
||||
commit(types.default.TOGGLE_ACCOUNT_CONVERSATION_METRIC_LOADING, false);
|
||||
});
|
||||
},
|
||||
fetchAgentConversationMetric({ commit }, reportObj) {
|
||||
fetchAgentConversationMetric({ commit }) {
|
||||
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, true);
|
||||
Report.getConversationMetric(reportObj.type, reportObj.page)
|
||||
liveReports
|
||||
.getGroupedConversations({ groupBy: 'assignee_id' })
|
||||
.then(agentConversationMetric => {
|
||||
commit(
|
||||
types.default.SET_AGENT_CONVERSATION_METRIC,
|
||||
@@ -181,6 +194,18 @@ export const actions = {
|
||||
commit(types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING, false);
|
||||
});
|
||||
},
|
||||
fetchTeamConversationMetric({ commit }) {
|
||||
commit(types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING, true);
|
||||
liveReports
|
||||
.getGroupedConversations({ groupBy: 'team_id' })
|
||||
.then(teamMetric => {
|
||||
commit(types.default.SET_TEAM_CONVERSATION_METRIC, teamMetric.data);
|
||||
commit(types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING, false);
|
||||
})
|
||||
.catch(() => {
|
||||
commit(types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING, false);
|
||||
});
|
||||
},
|
||||
downloadAgentReports(_, reportObj) {
|
||||
return Report.getAgentReports(reportObj)
|
||||
.then(response => {
|
||||
@@ -234,7 +259,7 @@ export const actions = {
|
||||
});
|
||||
},
|
||||
downloadAccountConversationHeatmap(_, reportObj) {
|
||||
Report.getConversationTrafficCSV()
|
||||
Report.getConversationTrafficCSV({ daysBefore: reportObj.daysBefore })
|
||||
.then(response => {
|
||||
downloadCsvFile(
|
||||
generateFileName({
|
||||
@@ -265,6 +290,12 @@ const mutations = {
|
||||
[types.default.TOGGLE_ACCOUNT_REPORT_LOADING](_state, { metric, value }) {
|
||||
_state.accountReport.isFetching[metric] = value;
|
||||
},
|
||||
[types.default.SET_BOT_SUMMARY_STATUS](_state, status) {
|
||||
_state.botSummaryFetchingStatus = status;
|
||||
},
|
||||
[types.default.SET_ACCOUNT_SUMMARY_STATUS](_state, status) {
|
||||
_state.accountSummaryFetchingStatus = status;
|
||||
},
|
||||
[types.default.TOGGLE_HEATMAP_LOADING](_state, flag) {
|
||||
_state.overview.uiFlags.isFetchingAccountConversationsHeatmap = flag;
|
||||
},
|
||||
@@ -286,6 +317,12 @@ const mutations = {
|
||||
[types.default.TOGGLE_AGENT_CONVERSATION_METRIC_LOADING](_state, flag) {
|
||||
_state.overview.uiFlags.isFetchingAgentConversationMetric = flag;
|
||||
},
|
||||
[types.default.SET_TEAM_CONVERSATION_METRIC](_state, metricData) {
|
||||
_state.overview.teamConversationMetric = metricData;
|
||||
},
|
||||
[types.default.TOGGLE_TEAM_CONVERSATION_METRIC_LOADING](_state, flag) {
|
||||
_state.overview.uiFlags.isFetchingTeamConversationMetric = flag;
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
||||
@@ -39,10 +39,13 @@ describe('#actions', () => {
|
||||
|
||||
describe('#update', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.patch.mockResolvedValue();
|
||||
axios.patch.mockResolvedValue({
|
||||
data: { id: 1, name: 'John' },
|
||||
});
|
||||
await actions.update({ commit, getters }, accountData);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.EDIT_ACCOUNT, { id: 1, name: 'John' }],
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
@@ -80,4 +83,41 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#toggleDeletion', () => {
|
||||
it('sends correct actions with delete action if API is success', async () => {
|
||||
axios.post.mockResolvedValue({});
|
||||
await actions.toggleDeletion({ commit }, { action_type: 'delete' });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
expect(axios.post.mock.calls[0][1]).toEqual({
|
||||
action_type: 'delete',
|
||||
});
|
||||
});
|
||||
|
||||
it('sends correct actions with undelete action if API is success', async () => {
|
||||
axios.post.mockResolvedValue({});
|
||||
await actions.toggleDeletion({ commit }, { action_type: 'undelete' });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
expect(axios.post.mock.calls[0][1]).toEqual({
|
||||
action_type: 'undelete',
|
||||
});
|
||||
});
|
||||
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.toggleDeletion({ commit }, { action_type: 'delete' })
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: true }],
|
||||
[types.default.SET_ACCOUNT_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import { actions } from '../../agentBots';
|
||||
import types from '../../../mutation-types';
|
||||
import { agentBotRecords } from './fixtures';
|
||||
import { agentBotRecords, agentBotData } from './fixtures';
|
||||
|
||||
const commit = vi.fn();
|
||||
global.axios = axios;
|
||||
@@ -30,16 +30,22 @@ describe('#actions', () => {
|
||||
describe('#create', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.post.mockResolvedValue({ data: agentBotRecords[0] });
|
||||
await actions.create({ commit }, agentBotRecords[0]);
|
||||
await actions.create({ commit }, agentBotData);
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_AGENT_BOT_UI_FLAG, { isCreating: true }],
|
||||
[types.ADD_AGENT_BOT, agentBotRecords[0]],
|
||||
[types.SET_AGENT_BOT_UI_FLAG, { isCreating: false }],
|
||||
]);
|
||||
|
||||
expect(axios.post.mock.calls.length).toBe(1);
|
||||
const formDataArg = axios.post.mock.calls[0][1];
|
||||
expect(formDataArg instanceof FormData).toBe(true);
|
||||
});
|
||||
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(actions.create({ commit })).rejects.toThrow(Error);
|
||||
await expect(actions.create({ commit }, {})).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_AGENT_BOT_UI_FLAG, { isCreating: true }],
|
||||
[types.SET_AGENT_BOT_UI_FLAG, { isCreating: false }],
|
||||
@@ -50,17 +56,29 @@ describe('#actions', () => {
|
||||
describe('#update', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.patch.mockResolvedValue({ data: agentBotRecords[0] });
|
||||
await actions.update({ commit }, agentBotRecords[0]);
|
||||
await actions.update(
|
||||
{ commit },
|
||||
{
|
||||
id: agentBotRecords[0].id,
|
||||
data: agentBotData,
|
||||
}
|
||||
);
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_AGENT_BOT_UI_FLAG, { isUpdating: true }],
|
||||
[types.EDIT_AGENT_BOT, agentBotRecords[0]],
|
||||
[types.SET_AGENT_BOT_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
|
||||
expect(axios.patch.mock.calls.length).toBe(1);
|
||||
const formDataArg = axios.patch.mock.calls[0][1];
|
||||
expect(formDataArg instanceof FormData).toBe(true);
|
||||
});
|
||||
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.patch.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.update({ commit }, agentBotRecords[0])
|
||||
actions.update({ commit }, { id: 1, data: {} })
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_AGENT_BOT_UI_FLAG, { isUpdating: true }],
|
||||
@@ -68,7 +86,6 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#delete', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.delete.mockResolvedValue({ data: agentBotRecords[0] });
|
||||
@@ -153,4 +170,21 @@ describe('#actions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('#resetAccessToken', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const mockResponse = {
|
||||
data: { ...agentBotRecords[0], access_token: 'new_token_123' },
|
||||
};
|
||||
axios.post.mockResolvedValue(mockResponse);
|
||||
const result = await actions.resetAccessToken(
|
||||
{ commit },
|
||||
agentBotRecords[0].id
|
||||
);
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.EDIT_AGENT_BOT, mockResponse.data],
|
||||
]);
|
||||
expect(result).toBe(mockResponse.data);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,35 @@
|
||||
export const agentBotRecords = [
|
||||
{
|
||||
account_id: 1,
|
||||
id: 11,
|
||||
name: 'Agent Bot 11',
|
||||
description: 'Agent Bot Description',
|
||||
type: 'csml',
|
||||
bot_type: 'webhook',
|
||||
thumbnail: 'https://example.com/thumbnail.jpg',
|
||||
bot_config: {},
|
||||
outgoing_url: 'https://example.com/outgoing',
|
||||
access_token: 'hN8QwG769RqBXmme',
|
||||
system_bot: false,
|
||||
},
|
||||
|
||||
{
|
||||
account_id: 1,
|
||||
id: 12,
|
||||
name: 'Agent Bot 12',
|
||||
description: 'Agent Bot Description 12',
|
||||
type: 'csml',
|
||||
bot_type: 'webhook',
|
||||
thumbnail: 'https://example.com/thumbnail.jpg',
|
||||
bot_config: {},
|
||||
outgoing_url: 'https://example.com/outgoing',
|
||||
access_token: 'hN8QwG769RqBXmme',
|
||||
system_bot: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const agentBotData = {
|
||||
name: 'Test Bot',
|
||||
description: 'Test Description',
|
||||
outgoing_url: 'https://test.com',
|
||||
bot_type: 'webhook',
|
||||
avatar: new File([''], 'filename'),
|
||||
};
|
||||
|
||||
@@ -51,4 +51,16 @@ describe('#mutations', () => {
|
||||
expect(state.agentBotInbox).toEqual({ 3: 2 });
|
||||
});
|
||||
});
|
||||
describe('#UPDATE_AGENT_BOT_AVATAR', () => {
|
||||
it('update agent bot avatar', () => {
|
||||
const state = { records: [agentBotRecords[0]] };
|
||||
mutations[types.UPDATE_AGENT_BOT_AVATAR](state, {
|
||||
id: 11,
|
||||
thumbnail: 'https://example.com/thumbnail.jpg',
|
||||
});
|
||||
expect(state.records[0].thumbnail).toEqual(
|
||||
'https://example.com/thumbnail.jpg'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -228,4 +228,20 @@ describe('#actions', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#resetAccessToken', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const mockResponse = {
|
||||
data: { id: 1, name: 'John', access_token: 'new_token_123' },
|
||||
headers: { expiry: 581842904 },
|
||||
};
|
||||
axios.post.mockResolvedValue(mockResponse);
|
||||
const result = await actions.resetAccessToken({ commit });
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_CURRENT_USER, mockResponse.data],
|
||||
]);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,11 @@ export default [
|
||||
url: 'https://github.com',
|
||||
time_on_page: 10,
|
||||
},
|
||||
inbox: {
|
||||
id: 1,
|
||||
channel_type: 'Channel::WebWidget',
|
||||
name: 'Web Widget',
|
||||
},
|
||||
created_at: '2021-05-03T04:53:36.354Z',
|
||||
updated_at: '2021-05-03T04:53:36.354Z',
|
||||
},
|
||||
@@ -24,6 +29,11 @@ export default [
|
||||
url: 'https://chatwoot.com',
|
||||
time_on_page: '20',
|
||||
},
|
||||
inbox: {
|
||||
id: 2,
|
||||
channel_type: 'Channel::TwilioSms',
|
||||
name: 'Twilio SMS',
|
||||
},
|
||||
created_at: '2021-05-03T08:15:35.828Z',
|
||||
updated_at: '2021-05-03T08:15:35.828Z',
|
||||
},
|
||||
@@ -39,7 +49,52 @@ export default [
|
||||
url: 'https://noshow.com',
|
||||
time_on_page: 10,
|
||||
},
|
||||
inbox: {
|
||||
id: 3,
|
||||
channel_type: 'Channel::WebWidget',
|
||||
name: 'Web Widget 2',
|
||||
},
|
||||
created_at: '2021-05-03T10:22:51.025Z',
|
||||
updated_at: '2021-05-03T10:22:51.025Z',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: 'WhatsApp Campaign',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
campaign_type: 'one_off',
|
||||
message: 'Hello {{name}}, your order is ready!',
|
||||
enabled: true,
|
||||
trigger_rules: {},
|
||||
inbox: {
|
||||
id: 4,
|
||||
channel_type: 'Channel::Whatsapp',
|
||||
name: 'WhatsApp Business',
|
||||
},
|
||||
template_params: {
|
||||
name: 'order_ready',
|
||||
namespace: 'business_namespace',
|
||||
language: 'en_US',
|
||||
processed_params: { name: 'John' },
|
||||
},
|
||||
created_at: '2021-05-03T12:15:35.828Z',
|
||||
updated_at: '2021-05-03T12:15:35.828Z',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: 'SMS Promotion',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
campaign_type: 'one_off',
|
||||
message: 'Get 20% off your next order!',
|
||||
enabled: true,
|
||||
trigger_rules: {},
|
||||
inbox: {
|
||||
id: 5,
|
||||
channel_type: 'Channel::Sms',
|
||||
name: 'SMS Channel',
|
||||
},
|
||||
created_at: '2021-05-03T14:15:35.828Z',
|
||||
updated_at: '2021-05-03T14:15:35.828Z',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -13,20 +13,58 @@ describe('#getters', () => {
|
||||
it('get one_off campaigns', () => {
|
||||
const state = { records: campaigns };
|
||||
expect(getters.getCampaigns(state)('one_off')).toEqual([
|
||||
{
|
||||
id: 2,
|
||||
title: 'Onboarding Campaign',
|
||||
description: null,
|
||||
account_id: 1,
|
||||
campaign_type: 'one_off',
|
||||
campaigns[1],
|
||||
campaigns[3],
|
||||
campaigns[4],
|
||||
]);
|
||||
});
|
||||
|
||||
trigger_rules: {
|
||||
url: 'https://chatwoot.com',
|
||||
time_on_page: '20',
|
||||
},
|
||||
created_at: '2021-05-03T08:15:35.828Z',
|
||||
updated_at: '2021-05-03T08:15:35.828Z',
|
||||
},
|
||||
it('get campaigns by channel type', () => {
|
||||
const state = { records: campaigns };
|
||||
expect(
|
||||
getters.getCampaigns(state)('one_off', ['Channel::Whatsapp'])
|
||||
).toEqual([campaigns[3]]);
|
||||
});
|
||||
|
||||
it('get campaigns by multiple channel types', () => {
|
||||
const state = { records: campaigns };
|
||||
expect(
|
||||
getters.getCampaigns(state)('one_off', [
|
||||
'Channel::TwilioSms',
|
||||
'Channel::Sms',
|
||||
])
|
||||
).toEqual([campaigns[1], campaigns[4]]);
|
||||
});
|
||||
|
||||
it('get SMS campaigns', () => {
|
||||
const state = { records: campaigns };
|
||||
const mockGetters = {
|
||||
getCampaigns: getters.getCampaigns(state),
|
||||
};
|
||||
expect(getters.getSMSCampaigns(state, mockGetters)).toEqual([
|
||||
campaigns[1],
|
||||
campaigns[4],
|
||||
]);
|
||||
});
|
||||
|
||||
it('get WhatsApp campaigns', () => {
|
||||
const state = { records: campaigns };
|
||||
const mockGetters = {
|
||||
getCampaigns: getters.getCampaigns(state),
|
||||
};
|
||||
expect(getters.getWhatsAppCampaigns(state, mockGetters)).toEqual([
|
||||
campaigns[3],
|
||||
]);
|
||||
});
|
||||
|
||||
it('get Live Chat campaigns', () => {
|
||||
const state = { records: campaigns };
|
||||
const mockGetters = {
|
||||
getCampaigns: getters.getCampaigns(state),
|
||||
};
|
||||
expect(getters.getLiveChatCampaigns(state, mockGetters)).toEqual([
|
||||
campaigns[0],
|
||||
campaigns[2],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -70,6 +70,30 @@ describe('#actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#active', () => {
|
||||
it('sends correct mutations if API is success', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: { payload: contactList, meta: { count: 100, current_page: 1 } },
|
||||
});
|
||||
await actions.active({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_CONTACT_UI_FLAG, { isFetching: true }],
|
||||
[types.CLEAR_CONTACTS],
|
||||
[types.SET_CONTACTS, contactList],
|
||||
[types.SET_CONTACT_META, { count: 100, current_page: 1 }],
|
||||
[types.SET_CONTACT_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct mutations if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.active({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_CONTACT_UI_FLAG, { isFetching: true }],
|
||||
[types.SET_CONTACT_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#update', () => {
|
||||
it('sends correct mutations if API is success', async () => {
|
||||
axios.patch.mockResolvedValue({ data: { payload: contactList[0] } });
|
||||
|
||||
@@ -75,6 +75,7 @@ describe('#actions', () => {
|
||||
q: 'test',
|
||||
});
|
||||
expect(dispatch).toHaveBeenCalledWith('messageSearch', { q: 'test' });
|
||||
expect(dispatch).toHaveBeenCalledWith('articleSearch', { q: 'test' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,6 +151,30 @@ describe('#actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#articleSearch', () => {
|
||||
it('should handle successful article search', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: { payload: { articles: [{ id: 1 }] } },
|
||||
});
|
||||
|
||||
await actions.articleSearch({ commit }, { q: 'test', page: 1 });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.ARTICLE_SEARCH_SET_UI_FLAG, { isFetching: true }],
|
||||
[types.ARTICLE_SEARCH_SET, [{ id: 1 }]],
|
||||
[types.ARTICLE_SEARCH_SET_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle failed article search', async () => {
|
||||
axios.get.mockRejectedValue({});
|
||||
await actions.articleSearch({ commit }, { q: 'test' });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.ARTICLE_SEARCH_SET_UI_FLAG, { isFetching: true }],
|
||||
[types.ARTICLE_SEARCH_SET_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#clearSearchResults', () => {
|
||||
it('should commit clear search results mutation', () => {
|
||||
actions.clearSearchResults({ commit });
|
||||
|
||||
@@ -37,6 +37,15 @@ describe('#getters', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('getArticleRecords', () => {
|
||||
const state = {
|
||||
articleRecords: [{ id: 1, title: 'Article 1' }],
|
||||
};
|
||||
expect(getters.getArticleRecords(state)).toEqual([
|
||||
{ id: 1, title: 'Article 1' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('getUIFlags', () => {
|
||||
const state = {
|
||||
uiFlags: {
|
||||
@@ -45,6 +54,7 @@ describe('#getters', () => {
|
||||
contact: { isFetching: true },
|
||||
message: { isFetching: false },
|
||||
conversation: { isFetching: false },
|
||||
article: { isFetching: false },
|
||||
},
|
||||
};
|
||||
expect(getters.getUIFlags(state)).toEqual({
|
||||
@@ -53,6 +63,7 @@ describe('#getters', () => {
|
||||
contact: { isFetching: true },
|
||||
message: { isFetching: false },
|
||||
conversation: { isFetching: false },
|
||||
article: { isFetching: false },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -101,17 +101,39 @@ describe('#mutations', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ARTICLE_SEARCH_SET', () => {
|
||||
it('should append new article records to existing ones', () => {
|
||||
const state = { articleRecords: [{ id: 1 }] };
|
||||
mutations[types.ARTICLE_SEARCH_SET](state, [{ id: 2 }]);
|
||||
expect(state.articleRecords).toEqual([{ id: 1 }, { id: 2 }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ARTICLE_SEARCH_SET_UI_FLAG', () => {
|
||||
it('set article search UI flags correctly', () => {
|
||||
const state = {
|
||||
uiFlags: {
|
||||
article: { isFetching: true },
|
||||
},
|
||||
};
|
||||
mutations[types.ARTICLE_SEARCH_SET_UI_FLAG](state, { isFetching: false });
|
||||
expect(state.uiFlags.article).toEqual({ isFetching: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CLEAR_SEARCH_RESULTS', () => {
|
||||
it('should clear all search records', () => {
|
||||
const state = {
|
||||
contactRecords: [{ id: 1 }],
|
||||
conversationRecords: [{ id: 1 }],
|
||||
messageRecords: [{ id: 1 }],
|
||||
articleRecords: [{ id: 1 }],
|
||||
};
|
||||
mutations[types.CLEAR_SEARCH_RESULTS](state);
|
||||
expect(state.contactRecords).toEqual([]);
|
||||
expect(state.conversationRecords).toEqual([]);
|
||||
expect(state.messageRecords).toEqual([]);
|
||||
expect(state.articleRecords).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,22 +6,41 @@ const commit = vi.fn();
|
||||
global.axios = axios;
|
||||
vi.mock('axios');
|
||||
|
||||
vi.mock('@chatwoot/utils', () => ({
|
||||
debounce: vi.fn(fn => {
|
||||
return fn;
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('#actions', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers(); // Set up fake timers
|
||||
commit.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers(); // Reset to real timers after each test
|
||||
});
|
||||
|
||||
describe('#get', () => {
|
||||
it('sends correct mutations if API is success', async () => {
|
||||
axios.get.mockResolvedValue({ data: { meta: { mine_count: 1 } } });
|
||||
await actions.get(
|
||||
{ commit, state: { updatedOn: null } },
|
||||
actions.get(
|
||||
{ commit, state: { allCount: 0 } },
|
||||
{ inboxId: 1, assigneeTpe: 'me', status: 'open' }
|
||||
);
|
||||
|
||||
await vi.runAllTimersAsync();
|
||||
await vi.waitFor(() => expect(commit).toHaveBeenCalled());
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_CONV_TAB_META, { mine_count: 1 }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.get(
|
||||
{ commit, state: { updatedOn: null } },
|
||||
actions.get(
|
||||
{ commit, state: { allCount: 0 } },
|
||||
{ inboxId: 1, assigneeTpe: 'me', status: 'open' }
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
|
||||
@@ -513,6 +513,28 @@ describe('#deleteMessage', () => {
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
});
|
||||
|
||||
describe('#deleteConversation', () => {
|
||||
it('send correct actions if API is success', async () => {
|
||||
axios.delete.mockResolvedValue({
|
||||
data: { id: 1 },
|
||||
});
|
||||
await actions.deleteConversation({ commit, dispatch }, 1);
|
||||
expect(commit.mock.calls).toEqual([[types.DELETE_CONVERSATION, 1]]);
|
||||
expect(dispatch.mock.calls).toEqual([
|
||||
['conversationStats/get', {}, { root: true }],
|
||||
]);
|
||||
});
|
||||
|
||||
it('send no actions if API is error', async () => {
|
||||
axios.delete.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(
|
||||
actions.deleteConversation({ commit, dispatch }, 1)
|
||||
).rejects.toThrow(Error);
|
||||
expect(commit.mock.calls).toEqual([]);
|
||||
expect(dispatch.mock.calls).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#updateCustomAttributes', () => {
|
||||
it('update conversation custom attributes', async () => {
|
||||
axios.post.mockResolvedValue({
|
||||
@@ -687,4 +709,23 @@ describe('#addMentions', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getInboxCaptainAssistantById', () => {
|
||||
it('fetches inbox assistant by id', async () => {
|
||||
axios.get.mockResolvedValue({
|
||||
data: {
|
||||
id: 1,
|
||||
name: 'Assistant',
|
||||
description: 'Assistant description',
|
||||
},
|
||||
});
|
||||
await actions.getInboxCaptainAssistantById({ commit }, 1);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[
|
||||
types.SET_INBOX_CAPTAIN_ASSISTANT,
|
||||
{ id: 1, name: 'Assistant', description: 'Assistant description' },
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,4 +308,325 @@ describe('#getters', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getCopilotAssistant', () => {
|
||||
it('get copilot assistant', () => {
|
||||
const state = {
|
||||
copilotAssistant: {
|
||||
id: 1,
|
||||
name: 'Assistant',
|
||||
description: 'Assistant description',
|
||||
},
|
||||
};
|
||||
expect(getters.getCopilotAssistant(state)).toEqual({
|
||||
id: 1,
|
||||
name: 'Assistant',
|
||||
description: 'Assistant description',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getFilteredConversations', () => {
|
||||
const mockConversations = [
|
||||
{
|
||||
id: 1,
|
||||
status: 'open',
|
||||
meta: { assignee: { id: 1 } },
|
||||
last_activity_at: 1000,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
status: 'open',
|
||||
meta: {},
|
||||
last_activity_at: 2000,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
status: 'resolved',
|
||||
meta: { assignee: { id: 2 } },
|
||||
last_activity_at: 3000,
|
||||
},
|
||||
];
|
||||
|
||||
const mockRootGetters = {
|
||||
getCurrentUser: {
|
||||
id: 1,
|
||||
accounts: [{ id: 1, role: 'agent', permissions: [] }],
|
||||
},
|
||||
getCurrentAccountId: 1,
|
||||
};
|
||||
|
||||
it('filters conversations based on role permissions for administrator', () => {
|
||||
const state = {
|
||||
allConversations: mockConversations,
|
||||
chatSortFilter: 'last_activity_at_desc',
|
||||
appliedFilters: [],
|
||||
};
|
||||
|
||||
const rootGetters = {
|
||||
...mockRootGetters,
|
||||
getCurrentUser: {
|
||||
...mockRootGetters.getCurrentUser,
|
||||
accounts: [{ id: 1, role: 'administrator', permissions: [] }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = getters.getFilteredConversations(
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
rootGetters
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
mockConversations[2],
|
||||
mockConversations[1],
|
||||
mockConversations[0],
|
||||
]);
|
||||
});
|
||||
|
||||
it('filters conversations based on role permissions for agent', () => {
|
||||
const state = {
|
||||
allConversations: mockConversations,
|
||||
chatSortFilter: 'last_activity_at_desc',
|
||||
appliedFilters: [],
|
||||
};
|
||||
|
||||
const rootGetters = {
|
||||
...mockRootGetters,
|
||||
getCurrentUser: {
|
||||
...mockRootGetters.getCurrentUser,
|
||||
accounts: [{ id: 1, role: 'agent', permissions: [] }],
|
||||
},
|
||||
};
|
||||
|
||||
const result = getters.getFilteredConversations(
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
rootGetters
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
mockConversations[2],
|
||||
mockConversations[1],
|
||||
mockConversations[0],
|
||||
]);
|
||||
});
|
||||
|
||||
it('filters conversations for custom role with conversation_manage permission', () => {
|
||||
const state = {
|
||||
allConversations: mockConversations,
|
||||
chatSortFilter: 'last_activity_at_desc',
|
||||
appliedFilters: [],
|
||||
};
|
||||
|
||||
const rootGetters = {
|
||||
...mockRootGetters,
|
||||
getCurrentUser: {
|
||||
...mockRootGetters.getCurrentUser,
|
||||
accounts: [
|
||||
{
|
||||
id: 1,
|
||||
custom_role_id: 5,
|
||||
permissions: ['conversation_manage'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = getters.getFilteredConversations(
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
rootGetters
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
mockConversations[2],
|
||||
mockConversations[1],
|
||||
mockConversations[0],
|
||||
]);
|
||||
});
|
||||
|
||||
it('filters conversations for custom role with conversation_unassigned_manage permission', () => {
|
||||
const state = {
|
||||
allConversations: mockConversations,
|
||||
chatSortFilter: 'last_activity_at_desc',
|
||||
appliedFilters: [],
|
||||
};
|
||||
|
||||
const rootGetters = {
|
||||
...mockRootGetters,
|
||||
getCurrentUser: {
|
||||
...mockRootGetters.getCurrentUser,
|
||||
accounts: [
|
||||
{
|
||||
id: 1,
|
||||
custom_role_id: 5,
|
||||
permissions: ['conversation_unassigned_manage'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = getters.getFilteredConversations(
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
rootGetters
|
||||
);
|
||||
|
||||
// Should include conversation assigned to user (id: 1) and unassigned conversation
|
||||
expect(result).toEqual([mockConversations[1], mockConversations[0]]);
|
||||
});
|
||||
|
||||
it('filters conversations for custom role with conversation_participating_manage permission', () => {
|
||||
const state = {
|
||||
allConversations: mockConversations,
|
||||
chatSortFilter: 'last_activity_at_desc',
|
||||
appliedFilters: [],
|
||||
};
|
||||
|
||||
const rootGetters = {
|
||||
...mockRootGetters,
|
||||
getCurrentUser: {
|
||||
...mockRootGetters.getCurrentUser,
|
||||
accounts: [
|
||||
{
|
||||
id: 1,
|
||||
custom_role_id: 5,
|
||||
permissions: ['conversation_participating_manage'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = getters.getFilteredConversations(
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
rootGetters
|
||||
);
|
||||
|
||||
// Should only include conversation assigned to user (id: 1)
|
||||
expect(result).toEqual([mockConversations[0]]);
|
||||
});
|
||||
|
||||
it('filters conversations for custom role with no permissions', () => {
|
||||
const state = {
|
||||
allConversations: mockConversations,
|
||||
chatSortFilter: 'last_activity_at_desc',
|
||||
appliedFilters: [],
|
||||
};
|
||||
|
||||
const rootGetters = {
|
||||
...mockRootGetters,
|
||||
getCurrentUser: {
|
||||
...mockRootGetters.getCurrentUser,
|
||||
accounts: [
|
||||
{
|
||||
id: 1,
|
||||
custom_role_id: 5,
|
||||
permissions: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = getters.getFilteredConversations(
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
rootGetters
|
||||
);
|
||||
|
||||
// Should return empty array as user has no permissions
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('applies filters and role permissions together', () => {
|
||||
const state = {
|
||||
allConversations: mockConversations,
|
||||
chatSortFilter: 'last_activity_at_desc',
|
||||
appliedFilters: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['open'],
|
||||
query_operator: 'and',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const rootGetters = {
|
||||
...mockRootGetters,
|
||||
getCurrentUser: {
|
||||
...mockRootGetters.getCurrentUser,
|
||||
accounts: [
|
||||
{
|
||||
id: 1,
|
||||
custom_role_id: 5,
|
||||
permissions: ['conversation_participating_manage'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const result = getters.getFilteredConversations(
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
rootGetters
|
||||
);
|
||||
|
||||
// Should only include open conversation assigned to user (id: 1)
|
||||
expect(result).toEqual([mockConversations[0]]);
|
||||
});
|
||||
|
||||
it('returns empty array when no conversations match filters', () => {
|
||||
const state = {
|
||||
allConversations: mockConversations,
|
||||
chatSortFilter: 'last_activity_at_desc',
|
||||
appliedFilters: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['pending'],
|
||||
query_operator: 'and',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = getters.getFilteredConversations(
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
mockRootGetters
|
||||
);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('sorts filtered conversations according to chatSortFilter', () => {
|
||||
const state = {
|
||||
allConversations: mockConversations,
|
||||
chatSortFilter: 'last_activity_at_asc',
|
||||
appliedFilters: [],
|
||||
};
|
||||
|
||||
const result = getters.getFilteredConversations(
|
||||
state,
|
||||
{},
|
||||
{},
|
||||
mockRootGetters
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
mockConversations[0],
|
||||
mockConversations[1],
|
||||
mockConversations[2],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe } from 'vitest';
|
||||
import types from '../../../mutation-types';
|
||||
import { mutations } from '../../conversations';
|
||||
|
||||
@@ -553,4 +554,408 @@ describe('#mutations', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_INBOX_CAPTAIN_ASSISTANT', () => {
|
||||
it('set inbox captain assistant', () => {
|
||||
const state = { copilotAssistant: {} };
|
||||
const data = {
|
||||
assistant: {
|
||||
id: 1,
|
||||
name: 'Assistant',
|
||||
description: 'Assistant description',
|
||||
},
|
||||
};
|
||||
mutations[types.SET_INBOX_CAPTAIN_ASSISTANT](state, data);
|
||||
expect(state.copilotAssistant).toEqual(data.assistant);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ALL_MESSAGES_LOADED', () => {
|
||||
it('should set allMessagesLoaded to true on selected chat', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, allMessagesLoaded: false }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
mutations[types.SET_ALL_MESSAGES_LOADED](state);
|
||||
expect(state.allConversations[0].allMessagesLoaded).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CLEAR_ALL_MESSAGES_LOADED', () => {
|
||||
it('should set allMessagesLoaded to false on selected chat', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, allMessagesLoaded: true }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
mutations[types.CLEAR_ALL_MESSAGES_LOADED](state);
|
||||
expect(state.allConversations[0].allMessagesLoaded).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_PREVIOUS_CONVERSATIONS', () => {
|
||||
it('should prepend messages to conversation messages array', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [{ id: 'msg2' }] }],
|
||||
};
|
||||
const payload = { id: 1, data: [{ id: 'msg1' }] };
|
||||
|
||||
mutations[types.SET_PREVIOUS_CONVERSATIONS](state, payload);
|
||||
expect(state.allConversations[0].messages).toEqual([
|
||||
{ id: 'msg1' },
|
||||
{ id: 'msg2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should not modify messages if data is empty', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [{ id: 'msg2' }] }],
|
||||
};
|
||||
const payload = { id: 1, data: [] };
|
||||
|
||||
mutations[types.SET_PREVIOUS_CONVERSATIONS](state, payload);
|
||||
expect(state.allConversations[0].messages).toEqual([{ id: 'msg2' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_MISSING_MESSAGES', () => {
|
||||
it('should replace message array with new data', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [{ id: 'old' }] }],
|
||||
};
|
||||
const payload = { id: 1, data: [{ id: 'new' }] };
|
||||
|
||||
mutations[types.SET_MISSING_MESSAGES](state, payload);
|
||||
expect(state.allConversations[0].messages).toEqual([{ id: 'new' }]);
|
||||
});
|
||||
|
||||
it('should do nothing if conversation is not found', () => {
|
||||
const state = {
|
||||
allConversations: [],
|
||||
};
|
||||
const payload = { id: 1, data: [{ id: 'new' }] };
|
||||
|
||||
mutations[types.SET_MISSING_MESSAGES](state, payload);
|
||||
expect(state.allConversations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ASSIGN_AGENT', () => {
|
||||
it('should assign agent to selected conversation', () => {
|
||||
const assignee = { id: 1, name: 'Agent' };
|
||||
const state = {
|
||||
allConversations: [{ id: 1, meta: {} }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
|
||||
mutations[types.ASSIGN_AGENT](state, assignee);
|
||||
expect(state.allConversations[0].meta.assignee).toEqual(assignee);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ASSIGN_PRIORITY', () => {
|
||||
it('should assign priority to conversation', () => {
|
||||
const priority = { title: 'Urgent', value: 'urgent' };
|
||||
const state = {
|
||||
allConversations: [{ id: 1 }],
|
||||
};
|
||||
|
||||
mutations[types.ASSIGN_PRIORITY](state, {
|
||||
priority,
|
||||
conversationId: 1,
|
||||
});
|
||||
expect(state.allConversations[0].priority).toEqual(priority);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#MUTE_CONVERSATION', () => {
|
||||
it('should mute selected conversation', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, muted: false }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
|
||||
mutations[types.MUTE_CONVERSATION](state);
|
||||
expect(state.allConversations[0].muted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UNMUTE_CONVERSATION', () => {
|
||||
it('should unmute selected conversation', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, muted: true }],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
|
||||
mutations[types.UNMUTE_CONVERSATION](state);
|
||||
expect(state.allConversations[0].muted).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UPDATE_CONVERSATION', () => {
|
||||
it('should update existing conversation', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{
|
||||
id: 1,
|
||||
status: 'open',
|
||||
updated_at: 100,
|
||||
messages: [{ id: 'msg1' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
id: 1,
|
||||
status: 'resolved',
|
||||
updated_at: 200,
|
||||
messages: [{ id: 'msg2' }],
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, conversation);
|
||||
expect(state.allConversations[0]).toEqual({
|
||||
id: 1,
|
||||
status: 'resolved',
|
||||
updated_at: 200,
|
||||
messages: [{ id: 'msg1' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add conversation if not found', () => {
|
||||
const state = {
|
||||
allConversations: [],
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
id: 1,
|
||||
status: 'open',
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, conversation);
|
||||
expect(state.allConversations).toEqual([conversation]);
|
||||
});
|
||||
|
||||
it('should emit events if updating selected conversation', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{
|
||||
id: 1,
|
||||
status: 'open',
|
||||
updated_at: 100,
|
||||
},
|
||||
],
|
||||
selectedChatId: 1,
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
id: 1,
|
||||
status: 'resolved',
|
||||
updated_at: 200,
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, conversation);
|
||||
expect(emitter.emit).toHaveBeenCalledWith('FETCH_LABEL_SUGGESTIONS');
|
||||
expect(emitter.emit).toHaveBeenCalledWith('SCROLL_TO_MESSAGE');
|
||||
});
|
||||
|
||||
it('should ignore updates with older timestamps', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{
|
||||
id: 1,
|
||||
status: 'open',
|
||||
updated_at: 200,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
id: 1,
|
||||
status: 'resolved',
|
||||
updated_at: 100,
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, conversation);
|
||||
expect(state.allConversations[0].status).toEqual('open');
|
||||
});
|
||||
|
||||
it('should allow updates with same timestamps', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{
|
||||
id: 1,
|
||||
status: 'open',
|
||||
updated_at: 100,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const conversation = {
|
||||
id: 1,
|
||||
status: 'resolved',
|
||||
updated_at: 100,
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION](state, conversation);
|
||||
expect(state.allConversations[0].status).toEqual('resolved');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UPDATE_CONVERSATION_CONTACT', () => {
|
||||
it('should update conversation contact data', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{ id: 1, meta: { sender: { id: 1, name: 'Old Name' } } },
|
||||
],
|
||||
};
|
||||
|
||||
const payload = {
|
||||
conversationId: 1,
|
||||
id: 1,
|
||||
name: 'New Name',
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION_CONTACT](state, payload);
|
||||
// The mutation extracts all properties except conversationId
|
||||
const { conversationId, ...contact } = payload;
|
||||
expect(state.allConversations[0].meta.sender).toEqual(contact);
|
||||
});
|
||||
|
||||
it('should do nothing if conversation is not found', () => {
|
||||
const state = {
|
||||
allConversations: [],
|
||||
};
|
||||
|
||||
const payload = {
|
||||
conversationId: 1,
|
||||
id: 1,
|
||||
name: 'New Name',
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_CONVERSATION_CONTACT](state, payload);
|
||||
expect(state.allConversations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_ACTIVE_INBOX', () => {
|
||||
it('should set current inbox as integer', () => {
|
||||
const state = {
|
||||
currentInbox: null,
|
||||
};
|
||||
|
||||
mutations[types.SET_ACTIVE_INBOX](state, '1');
|
||||
expect(state.currentInbox).toBe(1);
|
||||
});
|
||||
|
||||
it('should set null if no inbox ID provided', () => {
|
||||
const state = {
|
||||
currentInbox: 1,
|
||||
};
|
||||
|
||||
mutations[types.SET_ACTIVE_INBOX](state, null);
|
||||
expect(state.currentInbox).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CLEAR_CONTACT_CONVERSATIONS', () => {
|
||||
it('should remove all conversations with matching contact ID', () => {
|
||||
const state = {
|
||||
allConversations: [
|
||||
{ id: 1, meta: { sender: { id: 1 } } },
|
||||
{ id: 2, meta: { sender: { id: 2 } } },
|
||||
{ id: 3, meta: { sender: { id: 1 } } },
|
||||
],
|
||||
};
|
||||
|
||||
mutations[types.CLEAR_CONTACT_CONVERSATIONS](state, 1);
|
||||
expect(state.allConversations).toHaveLength(1);
|
||||
expect(state.allConversations[0].id).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#ADD_CONVERSATION', () => {
|
||||
it('should add a new conversation', () => {
|
||||
const state = {
|
||||
allConversations: [],
|
||||
};
|
||||
|
||||
const conversation = { id: 1, messages: [] };
|
||||
mutations[types.ADD_CONVERSATION](state, conversation);
|
||||
expect(state.allConversations).toEqual([conversation]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#DELETE_CONVERSATION', () => {
|
||||
it('should delete a conversation', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, messages: [] }],
|
||||
};
|
||||
|
||||
mutations[types.DELETE_CONVERSATION](state, 1);
|
||||
expect(state.allConversations).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_LIST_LOADING_STATUS', () => {
|
||||
it('should set listLoadingStatus to true', () => {
|
||||
const state = {
|
||||
listLoadingStatus: false,
|
||||
};
|
||||
|
||||
mutations[types.SET_LIST_LOADING_STATUS](state);
|
||||
expect(state.listLoadingStatus).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CLEAR_LIST_LOADING_STATUS', () => {
|
||||
it('should set listLoadingStatus to false', () => {
|
||||
const state = {
|
||||
listLoadingStatus: true,
|
||||
};
|
||||
|
||||
mutations[types.CLEAR_LIST_LOADING_STATUS](state);
|
||||
expect(state.listLoadingStatus).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CHANGE_CHAT_STATUS_FILTER', () => {
|
||||
it('should update chat status filter', () => {
|
||||
const state = {
|
||||
chatStatusFilter: 'open',
|
||||
};
|
||||
|
||||
mutations[types.CHANGE_CHAT_STATUS_FILTER](state, 'resolved');
|
||||
expect(state.chatStatusFilter).toBe('resolved');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#UPDATE_ASSIGNEE', () => {
|
||||
it('should update assignee on conversation', () => {
|
||||
const state = {
|
||||
allConversations: [{ id: 1, meta: { assignee: null } }],
|
||||
};
|
||||
|
||||
const payload = {
|
||||
id: 1,
|
||||
assignee: { id: 1, name: 'Agent' },
|
||||
};
|
||||
|
||||
mutations[types.UPDATE_ASSIGNEE](state, payload);
|
||||
expect(state.allConversations[0].meta.assignee).toEqual(payload.assignee);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_LAST_MESSAGE_ID_IN_SYNC_CONVERSATION', () => {
|
||||
it('should update the sync conversation message ID', () => {
|
||||
const state = {
|
||||
syncConversationsMessages: {},
|
||||
};
|
||||
|
||||
mutations[types.SET_LAST_MESSAGE_ID_IN_SYNC_CONVERSATION](state, {
|
||||
conversationId: 1,
|
||||
messageId: 100,
|
||||
});
|
||||
|
||||
expect(state.syncConversationsMessages[1]).toBe(100);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -62,6 +62,28 @@ describe('#actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createVoiceChannel', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.post.mockResolvedValue({ data: inboxList[0] });
|
||||
await actions.createVoiceChannel({ commit }, inboxList[0]);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INBOXES_UI_FLAG, { isCreating: true }],
|
||||
[types.default.ADD_INBOXES, inboxList[0]],
|
||||
[types.default.SET_INBOXES_UI_FLAG, { isCreating: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(actions.createVoiceChannel({ commit })).rejects.toThrow(
|
||||
Error
|
||||
);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_INBOXES_UI_FLAG, { isCreating: true }],
|
||||
[types.default.SET_INBOXES_UI_FLAG, { isCreating: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createFBChannel', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.post.mockResolvedValue({ data: inboxList[0] });
|
||||
@@ -209,4 +231,28 @@ describe('#actions', () => {
|
||||
).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#syncTemplates', () => {
|
||||
it('sends correct API call when sync is successful', async () => {
|
||||
axios.post.mockResolvedValue({
|
||||
data: { message: 'Template sync initiated successfully' },
|
||||
});
|
||||
|
||||
await actions.syncTemplates({ commit }, 123);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/inboxes/123/sync_templates'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error when API call fails', async () => {
|
||||
const errorMessage =
|
||||
'Template sync is only available for WhatsApp channels';
|
||||
axios.post.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
await expect(actions.syncTemplates({ commit }, 123)).rejects.toThrow(
|
||||
errorMessage
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export default [
|
||||
widget_color: null,
|
||||
website_token: null,
|
||||
enable_auto_assignment: true,
|
||||
instagram_id: 123456789,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -62,4 +63,12 @@ export default [
|
||||
channel_type: 'Channel::Sms',
|
||||
provider: 'default',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
channel_id: 7,
|
||||
name: 'Test Instagram 1',
|
||||
channel_type: 'Channel::Instagram',
|
||||
instagram_id: 123456789,
|
||||
provider: 'default',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getters } from '../../inboxes';
|
||||
import inboxList from './fixtures';
|
||||
import { templates } from './templateFixtures';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getInboxes', () => {
|
||||
@@ -26,7 +27,7 @@ describe('#getters', () => {
|
||||
|
||||
it('dialogFlowEnabledInboxes', () => {
|
||||
const state = { records: inboxList };
|
||||
expect(getters.dialogFlowEnabledInboxes(state).length).toEqual(6);
|
||||
expect(getters.dialogFlowEnabledInboxes(state).length).toEqual(7);
|
||||
});
|
||||
|
||||
it('getInbox', () => {
|
||||
@@ -43,6 +44,7 @@ describe('#getters', () => {
|
||||
widget_color: null,
|
||||
website_token: null,
|
||||
enable_auto_assignment: true,
|
||||
instagram_id: 123456789,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,4 +66,297 @@ describe('#getters', () => {
|
||||
isDeleting: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('getFacebookInboxByInstagramId', () => {
|
||||
const state = { records: inboxList };
|
||||
expect(getters.getFacebookInboxByInstagramId(state)(123456789)).toEqual({
|
||||
id: 1,
|
||||
channel_id: 1,
|
||||
name: 'Test FacebookPage 1',
|
||||
channel_type: 'Channel::FacebookPage',
|
||||
avatar_url: 'random_image.png',
|
||||
page_id: '12345',
|
||||
widget_color: null,
|
||||
website_token: null,
|
||||
enable_auto_assignment: true,
|
||||
instagram_id: 123456789,
|
||||
});
|
||||
});
|
||||
|
||||
it('getInstagramInboxByInstagramId', () => {
|
||||
const state = { records: inboxList };
|
||||
expect(getters.getInstagramInboxByInstagramId(state)(123456789)).toEqual({
|
||||
id: 7,
|
||||
channel_id: 7,
|
||||
name: 'Test Instagram 1',
|
||||
channel_type: 'Channel::Instagram',
|
||||
instagram_id: 123456789,
|
||||
provider: 'default',
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFilteredWhatsAppTemplates', () => {
|
||||
it('returns empty array when inbox not found', () => {
|
||||
const state = { records: [] };
|
||||
expect(getters.getFilteredWhatsAppTemplates(state)(999)).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array when templates is null or undefined', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
channel_type: 'Channel::Whatsapp',
|
||||
message_templates: null,
|
||||
additional_attributes: { message_templates: undefined },
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(getters.getFilteredWhatsAppTemplates(state)(1)).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns empty array when templates is not an array', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
channel_type: 'Channel::Whatsapp',
|
||||
message_templates: 'invalid',
|
||||
additional_attributes: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(getters.getFilteredWhatsAppTemplates(state)(1)).toEqual([]);
|
||||
});
|
||||
|
||||
it('filters out templates without required properties', () => {
|
||||
const invalidTemplates = [
|
||||
{ name: 'incomplete_template' }, // missing status and components
|
||||
{ status: 'approved' }, // missing name and components
|
||||
{ name: 'another_incomplete', status: 'approved' }, // missing components
|
||||
];
|
||||
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
channel_type: 'Channel::Whatsapp',
|
||||
message_templates: invalidTemplates,
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(getters.getFilteredWhatsAppTemplates(state)(1)).toEqual([]);
|
||||
});
|
||||
|
||||
it('filters out non-approved templates', () => {
|
||||
const mixedStatusTemplates = [
|
||||
{
|
||||
name: 'pending_template',
|
||||
status: 'pending',
|
||||
components: [{ type: 'BODY', text: 'Test' }],
|
||||
},
|
||||
{
|
||||
name: 'rejected_template',
|
||||
status: 'rejected',
|
||||
components: [{ type: 'BODY', text: 'Test' }],
|
||||
},
|
||||
{
|
||||
name: 'approved_template',
|
||||
status: 'approved',
|
||||
components: [{ type: 'BODY', text: 'Test' }],
|
||||
},
|
||||
];
|
||||
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
channel_type: 'Channel::Whatsapp',
|
||||
message_templates: mixedStatusTemplates,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = getters.getFilteredWhatsAppTemplates(state)(1);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe('approved_template');
|
||||
});
|
||||
|
||||
it('filters out interactive templates (LIST, PRODUCT, CATALOG)', () => {
|
||||
const interactiveTemplates = [
|
||||
{
|
||||
name: 'list_template',
|
||||
status: 'approved',
|
||||
components: [
|
||||
{ type: 'BODY', text: 'Choose an option' },
|
||||
{ type: 'LIST', sections: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'product_template',
|
||||
status: 'approved',
|
||||
components: [
|
||||
{ type: 'BODY', text: 'Product info' },
|
||||
{ type: 'PRODUCT', catalog_id: '123' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'catalog_template',
|
||||
status: 'approved',
|
||||
components: [
|
||||
{ type: 'BODY', text: 'Catalog' },
|
||||
{ type: 'CATALOG', thumbnail_product_retailer_id: '123' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'regular_template',
|
||||
status: 'approved',
|
||||
components: [{ type: 'BODY', text: 'Regular message' }],
|
||||
},
|
||||
];
|
||||
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
channel_type: 'Channel::Whatsapp',
|
||||
message_templates: interactiveTemplates,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = getters.getFilteredWhatsAppTemplates(state)(1);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe('regular_template');
|
||||
});
|
||||
|
||||
it('filters out location templates', () => {
|
||||
const locationTemplates = [
|
||||
{
|
||||
name: 'location_template',
|
||||
status: 'approved',
|
||||
components: [
|
||||
{ type: 'HEADER', format: 'LOCATION' },
|
||||
{ type: 'BODY', text: 'Location message' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'regular_template',
|
||||
status: 'approved',
|
||||
components: [
|
||||
{ type: 'HEADER', format: 'TEXT', text: 'Header' },
|
||||
{ type: 'BODY', text: 'Regular message' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
channel_type: 'Channel::Whatsapp',
|
||||
message_templates: locationTemplates,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = getters.getFilteredWhatsAppTemplates(state)(1);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe('regular_template');
|
||||
});
|
||||
|
||||
it('returns valid templates from fixture data', () => {
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
channel_type: 'Channel::Whatsapp',
|
||||
message_templates: templates,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = getters.getFilteredWhatsAppTemplates(state)(1);
|
||||
|
||||
// All templates in fixtures should be approved and valid
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify all returned templates are approved
|
||||
result.forEach(template => {
|
||||
expect(template.status).toBe('approved');
|
||||
expect(template.components).toBeDefined();
|
||||
expect(Array.isArray(template.components)).toBe(true);
|
||||
});
|
||||
|
||||
// Verify specific templates from fixtures are included
|
||||
const templateNames = result.map(t => t.name);
|
||||
expect(templateNames).toContain('sample_flight_confirmation');
|
||||
expect(templateNames).toContain('sample_issue_resolution');
|
||||
expect(templateNames).toContain('sample_shipping_confirmation');
|
||||
expect(templateNames).toContain('no_variable_template');
|
||||
expect(templateNames).toContain('order_confirmation');
|
||||
});
|
||||
|
||||
it('prioritizes message_templates over additional_attributes.message_templates', () => {
|
||||
const primaryTemplates = [
|
||||
{
|
||||
name: 'primary_template',
|
||||
status: 'approved',
|
||||
components: [{ type: 'BODY', text: 'Primary' }],
|
||||
},
|
||||
];
|
||||
|
||||
const fallbackTemplates = [
|
||||
{
|
||||
name: 'fallback_template',
|
||||
status: 'approved',
|
||||
components: [{ type: 'BODY', text: 'Fallback' }],
|
||||
},
|
||||
];
|
||||
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
channel_type: 'Channel::Whatsapp',
|
||||
message_templates: primaryTemplates,
|
||||
additional_attributes: {
|
||||
message_templates: fallbackTemplates,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = getters.getFilteredWhatsAppTemplates(state)(1);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe('primary_template');
|
||||
});
|
||||
|
||||
it('falls back to additional_attributes.message_templates when message_templates is null', () => {
|
||||
const fallbackTemplates = [
|
||||
{
|
||||
name: 'fallback_template',
|
||||
status: 'approved',
|
||||
components: [{ type: 'BODY', text: 'Fallback' }],
|
||||
},
|
||||
];
|
||||
|
||||
const state = {
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
channel_type: 'Channel::Whatsapp',
|
||||
message_templates: null,
|
||||
additional_attributes: {
|
||||
message_templates: fallbackTemplates,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = getters.getFilteredWhatsAppTemplates(state)(1);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe('fallback_template');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
export const templates = [
|
||||
{
|
||||
name: 'sample_flight_confirmation',
|
||||
status: 'approved',
|
||||
category: 'TICKET_UPDATE',
|
||||
language: 'pt_BR',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{ type: 'HEADER', format: 'DOCUMENT' },
|
||||
{
|
||||
text: 'Esta é a sua confirmação de voo para {{1}}-{{2}} em {{3}}.',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
text: 'Esta mensagem é de uma empresa não verificada.',
|
||||
type: 'FOOTER',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'sample_issue_resolution',
|
||||
status: 'approved',
|
||||
category: 'ISSUE_RESOLUTION',
|
||||
language: 'pt_BR',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: 'Oi, {{1}}. Nós conseguimos resolver o problema que você estava enfrentando?',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
text: 'Esta mensagem é de uma empresa não verificada.',
|
||||
type: 'FOOTER',
|
||||
},
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{ text: 'Sim', type: 'QUICK_REPLY' },
|
||||
{ text: 'Não', type: 'QUICK_REPLY' },
|
||||
],
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'sample_issue_resolution',
|
||||
status: 'approved',
|
||||
category: 'ISSUE_RESOLUTION',
|
||||
language: 'es',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: 'Hola, {{1}}. ¿Pudiste solucionar el problema que tenías?',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
text: 'Este mensaje proviene de un negocio no verificado.',
|
||||
type: 'FOOTER',
|
||||
},
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{ text: 'Sí', type: 'QUICK_REPLY' },
|
||||
{ text: 'No', type: 'QUICK_REPLY' },
|
||||
],
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'sample_issue_resolution',
|
||||
status: 'approved',
|
||||
category: 'ISSUE_RESOLUTION',
|
||||
language: 'id',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: 'Halo {{1}}, apakah kami bisa mengatasi masalah yang sedang Anda hadapi?',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
text: 'Pesan ini berasal dari bisnis yang tidak terverifikasi.',
|
||||
type: 'FOOTER',
|
||||
},
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{ text: 'Ya', type: 'QUICK_REPLY' },
|
||||
{ text: 'Tidak', type: 'QUICK_REPLY' },
|
||||
],
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'sample_shipping_confirmation',
|
||||
status: 'approved',
|
||||
category: 'SHIPPING_UPDATE',
|
||||
language: 'pt_BR',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: 'Seu pacote foi enviado. Ele será entregue em {{1}} dias úteis.',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
text: 'Esta mensagem é de uma empresa não verificada.',
|
||||
type: 'FOOTER',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'sample_shipping_confirmation',
|
||||
status: 'approved',
|
||||
category: 'SHIPPING_UPDATE',
|
||||
language: 'id',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: 'Paket Anda sudah dikirim. Paket akan sampai dalam {{1}} hari kerja.',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
text: 'Pesan ini berasal dari bisnis yang tidak terverifikasi.',
|
||||
type: 'FOOTER',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'sample_shipping_confirmation',
|
||||
status: 'approved',
|
||||
category: 'SHIPPING_UPDATE',
|
||||
language: 'es',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: 'ó tu paquete. La entrega se realizará en {{1}} dí.',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
text: 'Este mensaje proviene de un negocio no verificado.',
|
||||
type: 'FOOTER',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'sample_flight_confirmation',
|
||||
status: 'approved',
|
||||
category: 'TICKET_UPDATE',
|
||||
language: 'id',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{ type: 'HEADER', format: 'DOCUMENT' },
|
||||
{
|
||||
text: 'Ini merupakan konfirmasi penerbangan Anda untuk {{1}}-{{2}} di {{3}}.',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
text: 'Pesan ini berasal dari bisnis yang tidak terverifikasi.',
|
||||
type: 'FOOTER',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'sample_issue_resolution',
|
||||
status: 'approved',
|
||||
category: 'ISSUE_RESOLUTION',
|
||||
language: 'en_US',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: 'Hi {{1}}, were we able to solve the issue that you were facing?',
|
||||
type: 'BODY',
|
||||
},
|
||||
{ text: 'This message is from an unverified business.', type: 'FOOTER' },
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{ text: 'Yes', type: 'QUICK_REPLY' },
|
||||
{ text: 'No', type: 'QUICK_REPLY' },
|
||||
],
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'sample_flight_confirmation',
|
||||
status: 'approved',
|
||||
category: 'TICKET_UPDATE',
|
||||
language: 'es',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{ type: 'HEADER', format: 'DOCUMENT' },
|
||||
{
|
||||
text: 'Confirmamos tu vuelo a {{1}}-{{2}} para el {{3}}.',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
text: 'Este mensaje proviene de un negocio no verificado.',
|
||||
type: 'FOOTER',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'sample_flight_confirmation',
|
||||
status: 'approved',
|
||||
category: 'TICKET_UPDATE',
|
||||
language: 'en_US',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{ type: 'HEADER', format: 'DOCUMENT' },
|
||||
{
|
||||
text: 'This is your flight confirmation for {{1}}-{{2}} on {{3}}.',
|
||||
type: 'BODY',
|
||||
},
|
||||
{ text: 'This message is from an unverified business.', type: 'FOOTER' },
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'sample_shipping_confirmation',
|
||||
status: 'approved',
|
||||
category: 'SHIPPING_UPDATE',
|
||||
language: 'en_US',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: 'Your package has been shipped. It will be delivered in {{1}} business days.',
|
||||
type: 'BODY',
|
||||
},
|
||||
{ text: 'This message is from an unverified business.', type: 'FOOTER' },
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'no_variable_template',
|
||||
status: 'approved',
|
||||
category: 'TICKET_UPDATE',
|
||||
language: 'pt_BR',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
type: 'HEADER',
|
||||
format: 'DOCUMENT',
|
||||
},
|
||||
{
|
||||
text: 'This is a test whatsapp template',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
text: 'Esta mensagem é de uma empresa não verificada.',
|
||||
type: 'FOOTER',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'order_confirmation',
|
||||
status: 'approved',
|
||||
category: 'TICKET_UPDATE',
|
||||
language: 'en_US',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
type: 'HEADER',
|
||||
format: 'IMAGE',
|
||||
example: {
|
||||
header_handle: ['https://example.com/shoes.jpg'],
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Hi your order {{1}} is confirmed. Please wait for further updates',
|
||||
type: 'BODY',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'technician_visit',
|
||||
status: 'approved',
|
||||
category: 'UTILITY',
|
||||
language: 'en_US',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: 'Technician visit',
|
||||
type: 'HEADER',
|
||||
format: 'TEXT',
|
||||
},
|
||||
{
|
||||
text: "Hi {{1}}, we're scheduling a technician visit to {{2}} on {{3}} between {{4}} and {{5}}. Please confirm if this time slot works for you.",
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Confirm',
|
||||
type: 'QUICK_REPLY',
|
||||
},
|
||||
{
|
||||
text: 'Reschedule',
|
||||
type: 'QUICK_REPLY',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'event_invitation_static',
|
||||
status: 'approved',
|
||||
category: 'MARKETING',
|
||||
language: 'en',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: "You're invited to {{event_name}} at {{location}}, Join us for an amazing experience!",
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{
|
||||
url: 'https://events.example.com/register',
|
||||
text: 'Visit website',
|
||||
type: 'URL',
|
||||
},
|
||||
{
|
||||
url: 'https://maps.app.goo.gl/YoWAzRj1GDuxs6qz8',
|
||||
text: 'Get Directions',
|
||||
type: 'URL',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'purchase_receipt',
|
||||
status: 'approved',
|
||||
category: 'UTILITY',
|
||||
language: 'en_US',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
type: 'HEADER',
|
||||
format: 'DOCUMENT',
|
||||
},
|
||||
{
|
||||
text: 'Thank you for using your {{1}} card at {{2}}. Your {{3}} is attached as a PDF.',
|
||||
type: 'BODY',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'discount_coupon',
|
||||
status: 'approved',
|
||||
category: 'MARKETING',
|
||||
language: 'en',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: '🎉 Special offer for you! Get {{discount_percentage}}% off your next purchase. Use the code below at checkout',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Copy offer code',
|
||||
type: 'COPY_CODE',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'support_callback',
|
||||
status: 'approved',
|
||||
category: 'UTILITY',
|
||||
language: 'en',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: 'Hello {{name}}, our support team will call you regarding ticket # {{ticket_id}}.',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{
|
||||
text: 'Call Support',
|
||||
type: 'PHONE_NUMBER',
|
||||
phone_number: '+16506677566',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'training_video',
|
||||
status: 'approved',
|
||||
category: 'MARKETING',
|
||||
language: 'en',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
type: 'HEADER',
|
||||
format: 'VIDEO',
|
||||
},
|
||||
{
|
||||
text: "Hi {{name}}, here's your training video. Please watch by{{date}}.",
|
||||
type: 'BODY',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'product_launch',
|
||||
status: 'approved',
|
||||
category: 'MARKETING',
|
||||
language: 'en',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
type: 'HEADER',
|
||||
format: 'IMAGE',
|
||||
},
|
||||
{
|
||||
text: 'New arrival! Our stunning coat now available in {{color}} color.',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
text: 'Free shipping on orders over $100. Limited time offer.',
|
||||
type: 'FOOTER',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'greet',
|
||||
status: 'approved',
|
||||
category: 'MARKETING',
|
||||
language: 'en',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: 'Hey {{customer_name}} how may I help you?',
|
||||
type: 'BODY',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'hello_world',
|
||||
status: 'approved',
|
||||
category: 'UTILITY',
|
||||
language: 'en_US',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: 'Hello World',
|
||||
type: 'HEADER',
|
||||
format: 'TEXT',
|
||||
},
|
||||
{
|
||||
text: 'Welcome and congratulations!! This message demonstrates your ability to send a WhatsApp message notification from the Cloud API, hosted by Meta. Thank you for taking the time to test with us.',
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
text: 'WhatsApp Business Platform sample message',
|
||||
type: 'FOOTER',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'feedback_request',
|
||||
status: 'approved',
|
||||
category: 'MARKETING',
|
||||
language: 'en',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: "Hey {{name}}, how was your experience with Puma? We'd love your feedback!",
|
||||
type: 'BODY',
|
||||
},
|
||||
{
|
||||
type: 'BUTTONS',
|
||||
buttons: [
|
||||
{
|
||||
url: 'https://feedback.example.com/survey',
|
||||
text: 'Leave Feedback',
|
||||
type: 'URL',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'address_update',
|
||||
status: 'approved',
|
||||
category: 'UTILITY',
|
||||
language: 'en_US',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: 'Address update',
|
||||
type: 'HEADER',
|
||||
format: 'TEXT',
|
||||
},
|
||||
{
|
||||
text: 'Hi {{1}}, your delivery address has been successfully updated to {{2}}. Contact {{3}} for any inquiries.',
|
||||
type: 'BODY',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
{
|
||||
name: 'delivery_confirmation',
|
||||
status: 'approved',
|
||||
category: 'UTILITY',
|
||||
language: 'en_US',
|
||||
namespace: 'ed41a221_133a_4558_a1d6_192960e3aee9',
|
||||
components: [
|
||||
{
|
||||
text: '{{1}}, your order was successfully delivered on {{2}}.\n\nThank you for your purchase.\n',
|
||||
type: 'BODY',
|
||||
},
|
||||
],
|
||||
rejected_reason: 'NONE',
|
||||
},
|
||||
];
|
||||
@@ -1,14 +1,122 @@
|
||||
import axios from 'axios';
|
||||
import { actions } from '../../reports';
|
||||
import * as types from '../../../mutation-types';
|
||||
import { STATUS } from '../../../constants';
|
||||
import * as DownloadHelper from 'dashboard/helper/downloadHelper';
|
||||
import { flushPromises } from '@vue/test-utils';
|
||||
|
||||
global.open = vi.fn();
|
||||
global.axios = axios;
|
||||
global.URL.createObjectURL = vi.fn();
|
||||
|
||||
vi.mock('axios');
|
||||
vi.spyOn(DownloadHelper, 'downloadCsvFile');
|
||||
|
||||
describe('#actions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('#fetchAccountSummary', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const commit = vi.fn();
|
||||
const reportObj = {
|
||||
from: 1630504922510,
|
||||
to: 1630504922510,
|
||||
type: 'account',
|
||||
id: 1,
|
||||
groupBy: 'day',
|
||||
businessHours: true,
|
||||
};
|
||||
const summaryData = {
|
||||
conversations_count: 10,
|
||||
incoming_messages_count: 20,
|
||||
outgoing_messages_count: 15,
|
||||
avg_first_response_time: 30,
|
||||
avg_resolution_time: 60,
|
||||
resolutions_count: 5,
|
||||
bot_resolutions_count: 2,
|
||||
bot_handoffs_count: 1,
|
||||
reply_time: 25,
|
||||
};
|
||||
axios.get.mockResolvedValue({ data: summaryData });
|
||||
|
||||
actions.fetchAccountSummary({ commit }, reportObj);
|
||||
await flushPromises();
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FETCHING],
|
||||
[types.default.SET_ACCOUNT_SUMMARY, summaryData],
|
||||
[types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FINISHED],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sends correct actions if API fails', async () => {
|
||||
const commit = vi.fn();
|
||||
const reportObj = {
|
||||
from: 1630504922510,
|
||||
to: 1630504922510,
|
||||
};
|
||||
axios.get.mockRejectedValue(new Error('API Error'));
|
||||
|
||||
actions.fetchAccountSummary({ commit }, reportObj);
|
||||
await flushPromises();
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FETCHING],
|
||||
[types.default.SET_ACCOUNT_SUMMARY_STATUS, STATUS.FAILED],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#fetchBotSummary', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
const commit = vi.fn();
|
||||
const reportObj = {
|
||||
from: 1630504922510,
|
||||
to: 1630504922510,
|
||||
groupBy: 'day',
|
||||
businessHours: true,
|
||||
};
|
||||
const summaryData = {
|
||||
bot_resolutions_count: 10,
|
||||
bot_handoffs_count: 5,
|
||||
previous: {
|
||||
bot_resolutions_count: 8,
|
||||
bot_handoffs_count: 4,
|
||||
},
|
||||
};
|
||||
axios.get.mockResolvedValue({ data: summaryData });
|
||||
|
||||
actions.fetchBotSummary({ commit }, reportObj);
|
||||
await flushPromises();
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_BOT_SUMMARY_STATUS, STATUS.FETCHING],
|
||||
[types.default.SET_BOT_SUMMARY, summaryData],
|
||||
[types.default.SET_BOT_SUMMARY_STATUS, STATUS.FINISHED],
|
||||
]);
|
||||
});
|
||||
|
||||
it('sends correct actions if API fails', async () => {
|
||||
const commit = vi.fn();
|
||||
const reportObj = {
|
||||
from: 1630504922510,
|
||||
to: 1630504922510,
|
||||
};
|
||||
const error = new Error('API error');
|
||||
axios.get.mockRejectedValueOnce(error);
|
||||
|
||||
actions.fetchBotSummary({ commit }, reportObj);
|
||||
await flushPromises();
|
||||
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.default.SET_BOT_SUMMARY_STATUS, STATUS.FETCHING],
|
||||
[types.default.SET_BOT_SUMMARY_STATUS, STATUS.FAILED],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#downloadAgentReports', () => {
|
||||
it('open CSV download prompt if API is success', async () => {
|
||||
const data = `Agent name,Conversations count,Avg first response time (Minutes),Avg resolution time (Minutes)
|
||||
@@ -20,7 +128,9 @@ describe('#actions', () => {
|
||||
to: 1630504922510,
|
||||
fileName: 'agent-report-01-09-2021.csv',
|
||||
};
|
||||
await actions.downloadAgentReports(1, param);
|
||||
actions.downloadAgentReports(1, param);
|
||||
await flushPromises();
|
||||
|
||||
expect(DownloadHelper.downloadCsvFile).toBeCalledWith(
|
||||
param.fileName,
|
||||
data
|
||||
@@ -39,7 +149,9 @@ describe('#actions', () => {
|
||||
type: 'label',
|
||||
fileName: 'label-report-01-09-2021.csv',
|
||||
};
|
||||
await actions.downloadLabelReports(1, param);
|
||||
actions.downloadLabelReports(1, param);
|
||||
await flushPromises();
|
||||
|
||||
expect(DownloadHelper.downloadCsvFile).toBeCalledWith(
|
||||
param.fileName,
|
||||
data
|
||||
@@ -59,7 +171,9 @@ describe('#actions', () => {
|
||||
to: 1635013800,
|
||||
fileName: 'inbox-report-24-10-2021.csv',
|
||||
};
|
||||
await actions.downloadInboxReports(1, param);
|
||||
actions.downloadInboxReports(1, param);
|
||||
await flushPromises();
|
||||
|
||||
expect(DownloadHelper.downloadCsvFile).toBeCalledWith(
|
||||
param.fileName,
|
||||
data
|
||||
@@ -78,7 +192,9 @@ describe('#actions', () => {
|
||||
to: 1635013800,
|
||||
fileName: 'inbox-report-24-10-2021.csv',
|
||||
};
|
||||
await actions.downloadInboxReports(1, param);
|
||||
actions.downloadInboxReports(1, param);
|
||||
await flushPromises();
|
||||
|
||||
expect(DownloadHelper.downloadCsvFile).toBeCalledWith(
|
||||
param.fileName,
|
||||
data
|
||||
|
||||
@@ -7,6 +7,7 @@ vi.mock('dashboard/api/summaryReports', () => ({
|
||||
getInboxReports: vi.fn(),
|
||||
getAgentReports: vi.fn(),
|
||||
getTeamReports: vi.fn(),
|
||||
getLabelReports: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -25,10 +26,12 @@ describe('Summary Reports Store', () => {
|
||||
inboxSummaryReports: [],
|
||||
agentSummaryReports: [],
|
||||
teamSummaryReports: [],
|
||||
labelSummaryReports: [],
|
||||
uiFlags: {
|
||||
isFetchingInboxSummaryReports: false,
|
||||
isFetchingAgentSummaryReports: false,
|
||||
isFetchingTeamSummaryReports: false,
|
||||
isFetchingLabelSummaryReports: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -39,6 +42,7 @@ describe('Summary Reports Store', () => {
|
||||
inboxSummaryReports: [{ id: 1 }],
|
||||
agentSummaryReports: [{ id: 2 }],
|
||||
teamSummaryReports: [{ id: 3 }],
|
||||
labelSummaryReports: [{ id: 4 }],
|
||||
uiFlags: { isFetchingInboxSummaryReports: true },
|
||||
};
|
||||
|
||||
@@ -54,6 +58,10 @@ describe('Summary Reports Store', () => {
|
||||
expect(store.getters.getTeamSummaryReports(state)).toEqual([{ id: 3 }]);
|
||||
});
|
||||
|
||||
it('should return label summary reports', () => {
|
||||
expect(store.getters.getLabelSummaryReports(state)).toEqual([{ id: 4 }]);
|
||||
});
|
||||
|
||||
it('should return UI flags', () => {
|
||||
expect(store.getters.getUIFlags(state)).toEqual({
|
||||
isFetchingInboxSummaryReports: true,
|
||||
@@ -86,6 +94,14 @@ describe('Summary Reports Store', () => {
|
||||
expect(state.teamSummaryReports).toEqual(data);
|
||||
});
|
||||
|
||||
it('should set label summary report', () => {
|
||||
const state = { ...initialState };
|
||||
const data = [{ id: 4 }];
|
||||
|
||||
store.mutations.setLabelSummaryReport(state, data);
|
||||
expect(state.labelSummaryReports).toEqual(data);
|
||||
});
|
||||
|
||||
it('should merge UI flags with existing flags', () => {
|
||||
const state = {
|
||||
uiFlags: { flag1: true, flag2: false },
|
||||
@@ -185,5 +201,29 @@ describe('Summary Reports Store', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchLabelSummaryReports', () => {
|
||||
it('should fetch label reports successfully', async () => {
|
||||
const params = { labelId: 789 };
|
||||
const mockResponse = {
|
||||
data: [{ label_id: 789, label_name: 'Test Label' }],
|
||||
};
|
||||
|
||||
SummaryReportsAPI.getLabelReports.mockResolvedValue(mockResponse);
|
||||
|
||||
await store.actions.fetchLabelSummaryReports({ commit }, params);
|
||||
|
||||
expect(commit).toHaveBeenCalledWith('setUIFlags', {
|
||||
isFetchingLabelSummaryReports: true,
|
||||
});
|
||||
expect(SummaryReportsAPI.getLabelReports).toHaveBeenCalledWith(params);
|
||||
expect(commit).toHaveBeenCalledWith('setLabelSummaryReport', [
|
||||
{ labelId: 789, labelName: 'Test Label' },
|
||||
]);
|
||||
expect(commit).toHaveBeenCalledWith('setUIFlags', {
|
||||
isFetchingLabelSummaryReports: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,11 @@ const typeMap = {
|
||||
apiMethod: 'getTeamReports',
|
||||
mutationKey: 'setTeamSummaryReport',
|
||||
},
|
||||
label: {
|
||||
flagKey: 'isFetchingLabelSummaryReports',
|
||||
apiMethod: 'getLabelReports',
|
||||
mutationKey: 'setLabelSummaryReport',
|
||||
},
|
||||
};
|
||||
|
||||
async function fetchSummaryReports(type, params, { commit }) {
|
||||
@@ -38,10 +43,12 @@ export const initialState = {
|
||||
inboxSummaryReports: [],
|
||||
agentSummaryReports: [],
|
||||
teamSummaryReports: [],
|
||||
labelSummaryReports: [],
|
||||
uiFlags: {
|
||||
isFetchingInboxSummaryReports: false,
|
||||
isFetchingAgentSummaryReports: false,
|
||||
isFetchingTeamSummaryReports: false,
|
||||
isFetchingLabelSummaryReports: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -55,6 +62,9 @@ export const getters = {
|
||||
getTeamSummaryReports(state) {
|
||||
return state.teamSummaryReports;
|
||||
},
|
||||
getLabelSummaryReports(state) {
|
||||
return state.labelSummaryReports;
|
||||
},
|
||||
getUIFlags(state) {
|
||||
return state.uiFlags;
|
||||
},
|
||||
@@ -72,6 +82,10 @@ export const actions = {
|
||||
fetchTeamSummaryReports({ commit }, params) {
|
||||
return fetchSummaryReports('team', params, { commit });
|
||||
},
|
||||
|
||||
fetchLabelSummaryReports({ commit }, params) {
|
||||
return fetchSummaryReports('label', params, { commit });
|
||||
},
|
||||
};
|
||||
|
||||
export const mutations = {
|
||||
@@ -84,6 +98,9 @@ export const mutations = {
|
||||
setTeamSummaryReport(state, data) {
|
||||
state.teamSummaryReports = data;
|
||||
},
|
||||
setLabelSummaryReport(state, data) {
|
||||
state.labelSummaryReports = data;
|
||||
},
|
||||
setUIFlags(state, uiFlag) {
|
||||
state.uiFlags = { ...state.uiFlags, ...uiFlag };
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user