Merge branch 'develop' into feat/captain-inbox-selector
This commit is contained in:
@@ -5,6 +5,7 @@ export const CONVERSATION_EVENTS = Object.freeze({
|
||||
INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response',
|
||||
TRANSLATE_A_MESSAGE: 'Translated a message',
|
||||
INSERTED_A_VARIABLE: 'Inserted a variable',
|
||||
INSERTED_AN_EMOJI: 'Inserted an emoji',
|
||||
USED_MENTIONS: 'Used mentions',
|
||||
SEARCH_CONVERSATION: 'Searched conversations',
|
||||
APPLY_FILTER: 'Applied filters in the conversation list',
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import analyticsHelper from '.';
|
||||
|
||||
export default {
|
||||
// This function is called when the Vue plugin is installed
|
||||
install(Vue) {
|
||||
analyticsHelper.init();
|
||||
Vue.prototype.$analytics = analyticsHelper;
|
||||
// Add a shorthand function for the track method on the helper module
|
||||
Vue.prototype.$track = analyticsHelper.track.bind(analyticsHelper);
|
||||
},
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import Vue from 'vue';
|
||||
import plugin from '../plugin';
|
||||
import analyticsHelper from '../index';
|
||||
|
||||
vi.spyOn(analyticsHelper, 'init');
|
||||
vi.spyOn(analyticsHelper, 'track');
|
||||
|
||||
describe('Vue Analytics Plugin', () => {
|
||||
beforeEach(() => {
|
||||
Vue.use(plugin);
|
||||
});
|
||||
|
||||
it('should call the init method on analyticsHelper once during plugin installation', () => {
|
||||
expect(analyticsHelper.init).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should add the analyticsHelper to the Vue prototype as $analytics', () => {
|
||||
expect(Vue.prototype.$analytics).toBe(analyticsHelper);
|
||||
});
|
||||
|
||||
it('should add a track method to the Vue prototype as $track', () => {
|
||||
expect(typeof Vue.prototype.$track).toBe('function');
|
||||
Vue.prototype.$track('eventName');
|
||||
expect(analyticsHelper.track)
|
||||
.toHaveBeenCalledTimes(1)
|
||||
.toHaveBeenCalledWith('eventName');
|
||||
});
|
||||
|
||||
it('should call the track method on analyticsHelper with the correct event name when $track is called', () => {
|
||||
const eventName = 'testEvent';
|
||||
Vue.prototype.$track(eventName);
|
||||
expect(analyticsHelper.track)
|
||||
.toHaveBeenCalledTimes(1)
|
||||
.toHaveBeenCalledWith(eventName);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export const getAssignee = message => message?.conversation?.assignee_id;
|
||||
export const isConversationUnassigned = message => !getAssignee(message);
|
||||
export const isConversationAssignedToMe = (message, currentUserId) =>
|
||||
getAssignee(message) === currentUserId;
|
||||
export const isMessageFromCurrentUser = (message, currentUserId) =>
|
||||
message?.sender?.id === currentUserId;
|
||||
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions';
|
||||
import { getUserPermissions } from 'dashboard/helper/permissionsHelper';
|
||||
|
||||
class AudioNotificationStore {
|
||||
constructor(store) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
hasUnreadConversation = () => {
|
||||
const mineConversation = this.store.getters.getMineChats({
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
});
|
||||
|
||||
return mineConversation.some(conv => conv.unread_count > 0);
|
||||
};
|
||||
|
||||
isMessageFromCurrentConversation = message => {
|
||||
return this.store.getters.getSelectedChat?.id === message.conversation_id;
|
||||
};
|
||||
|
||||
hasConversationPermission = user => {
|
||||
const currentAccountId = this.store.getters.getCurrentAccountId;
|
||||
// Get the user permissions for the current account
|
||||
const userPermissions = getUserPermissions(user, currentAccountId);
|
||||
// Check if the user has the required permissions
|
||||
const hasRequiredPermission = [...ROLES, ...CONVERSATION_PERMISSIONS].some(
|
||||
permission => userPermissions.includes(permission)
|
||||
);
|
||||
return hasRequiredPermission;
|
||||
};
|
||||
}
|
||||
|
||||
export default AudioNotificationStore;
|
||||
@@ -1,82 +1,125 @@
|
||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
||||
import { showBadgeOnFavicon } from './faviconHelper';
|
||||
import { initFaviconSwitcher } from './faviconHelper';
|
||||
|
||||
import { EVENT_TYPES } from 'dashboard/routes/dashboard/settings/profile/constants.js';
|
||||
import GlobalStore from 'dashboard/store';
|
||||
import AudioNotificationStore from './AudioNotificationStore';
|
||||
import {
|
||||
getAlertAudio,
|
||||
initOnEvents,
|
||||
} from 'shared/helpers/AudioNotificationHelper';
|
||||
isConversationAssignedToMe,
|
||||
isConversationUnassigned,
|
||||
isMessageFromCurrentUser,
|
||||
} from './AudioMessageHelper';
|
||||
import WindowVisibilityHelper from './WindowVisibilityHelper';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
|
||||
const NOTIFICATION_TIME = 30000;
|
||||
const ALERT_DURATION = 10000;
|
||||
const ALERT_PATH_PREFIX = '/audio/dashboard/';
|
||||
const DEFAULT_TONE = 'ding';
|
||||
const DEFAULT_ALERT_TYPE = ['none'];
|
||||
|
||||
export class DashboardAudioNotificationHelper {
|
||||
constructor(store) {
|
||||
if (!store) {
|
||||
throw new Error('store is required');
|
||||
}
|
||||
this.store = new AudioNotificationStore(store);
|
||||
|
||||
this.notificationConfig = {
|
||||
audioAlertType: DEFAULT_ALERT_TYPE,
|
||||
playAlertOnlyWhenHidden: true,
|
||||
alertIfUnreadConversationExist: false,
|
||||
};
|
||||
|
||||
class DashboardAudioNotificationHelper {
|
||||
constructor() {
|
||||
this.recurringNotificationTimer = null;
|
||||
this.audioAlertType = 'none';
|
||||
this.playAlertOnlyWhenHidden = true;
|
||||
this.alertIfUnreadConversationExist = false;
|
||||
this.currentUserId = null;
|
||||
this.audioAlertTone = 'ding';
|
||||
|
||||
this.audioConfig = {
|
||||
audio: null,
|
||||
tone: DEFAULT_TONE,
|
||||
hasSentSoundPermissionsRequest: false,
|
||||
};
|
||||
|
||||
this.currentUser = null;
|
||||
}
|
||||
|
||||
setInstanceValues = ({
|
||||
currentUserId,
|
||||
alwaysPlayAudioAlert,
|
||||
alertIfUnreadConversationExist,
|
||||
audioAlertType,
|
||||
audioAlertTone,
|
||||
}) => {
|
||||
this.audioAlertType = audioAlertType;
|
||||
this.playAlertOnlyWhenHidden = !alwaysPlayAudioAlert;
|
||||
this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
|
||||
this.currentUserId = currentUserId;
|
||||
this.audioAlertTone = audioAlertTone;
|
||||
initOnEvents.forEach(e => {
|
||||
document.addEventListener(e, this.onAudioListenEvent, false);
|
||||
});
|
||||
initFaviconSwitcher();
|
||||
intializeAudio = () => {
|
||||
const resourceUrl = `${ALERT_PATH_PREFIX}${this.audioConfig.tone}.mp3`;
|
||||
this.audioConfig.audio = new Audio(resourceUrl);
|
||||
return this.audioConfig.audio.load();
|
||||
};
|
||||
|
||||
onAudioListenEvent = async () => {
|
||||
playAudioAlert = async () => {
|
||||
try {
|
||||
await getAlertAudio('', {
|
||||
type: 'dashboard',
|
||||
alertTone: this.audioAlertTone,
|
||||
});
|
||||
initOnEvents.forEach(event => {
|
||||
document.removeEventListener(event, this.onAudioListenEvent, false);
|
||||
});
|
||||
this.playAudioEvery30Seconds();
|
||||
await this.audioConfig.audio.play();
|
||||
} catch (error) {
|
||||
// Ignore audio fetch errors
|
||||
if (
|
||||
error.name === 'NotAllowedError' &&
|
||||
!this.hasSentSoundPermissionsRequest
|
||||
) {
|
||||
this.hasSentSoundPermissionsRequest = true;
|
||||
useAlert(
|
||||
'PROFILE_SETTINGS.FORM.AUDIO_NOTIFICATIONS_SECTION.SOUND_PERMISSION_ERROR',
|
||||
{ usei18n: true, duration: ALERT_DURATION }
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
set = ({
|
||||
currentUser,
|
||||
alwaysPlayAudioAlert,
|
||||
alertIfUnreadConversationExist,
|
||||
audioAlertType = DEFAULT_ALERT_TYPE,
|
||||
audioAlertTone = DEFAULT_TONE,
|
||||
}) => {
|
||||
this.notificationConfig = {
|
||||
...this.notificationConfig,
|
||||
audioAlertType: audioAlertType.split('+').filter(Boolean),
|
||||
playAlertOnlyWhenHidden: !alwaysPlayAudioAlert,
|
||||
alertIfUnreadConversationExist: alertIfUnreadConversationExist,
|
||||
};
|
||||
|
||||
this.currentUser = currentUser;
|
||||
|
||||
const previousAudioTone = this.audioConfig.tone;
|
||||
this.audioConfig = {
|
||||
...this.audioConfig,
|
||||
tone: audioAlertTone,
|
||||
};
|
||||
|
||||
if (previousAudioTone !== audioAlertTone) {
|
||||
this.intializeAudio();
|
||||
}
|
||||
|
||||
initFaviconSwitcher();
|
||||
this.clearRecurringTimer();
|
||||
this.playAudioEvery30Seconds();
|
||||
};
|
||||
|
||||
shouldPlayAlert = () => {
|
||||
if (this.notificationConfig.playAlertOnlyWhenHidden) {
|
||||
return !WindowVisibilityHelper.isWindowVisible();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
executeRecurringNotification = () => {
|
||||
if (!window.WOOT || !window.WOOT.$store) {
|
||||
this.clearSetTimeout();
|
||||
return;
|
||||
}
|
||||
const mineConversation = window.WOOT.$store.getters.getMineChats({
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
});
|
||||
const hasUnreadConversation = mineConversation.some(conv => {
|
||||
return conv.unread_count > 0;
|
||||
});
|
||||
|
||||
const shouldPlayAlert = !this.playAlertOnlyWhenHidden || document.hidden;
|
||||
|
||||
if (hasUnreadConversation && shouldPlayAlert) {
|
||||
window.playAudioAlert();
|
||||
if (this.store.hasUnreadConversation() && this.shouldPlayAlert()) {
|
||||
this.playAudioAlert();
|
||||
showBadgeOnFavicon();
|
||||
}
|
||||
this.clearSetTimeout();
|
||||
this.resetRecurringTimer();
|
||||
};
|
||||
|
||||
clearSetTimeout = () => {
|
||||
clearRecurringTimer = () => {
|
||||
if (this.recurringNotificationTimer) {
|
||||
clearTimeout(this.recurringNotificationTimer);
|
||||
}
|
||||
};
|
||||
|
||||
resetRecurringTimer = () => {
|
||||
this.clearRecurringTimer();
|
||||
this.recurringNotificationTimer = setTimeout(
|
||||
this.executeRecurringNotification,
|
||||
NOTIFICATION_TIME
|
||||
@@ -84,48 +127,57 @@ class DashboardAudioNotificationHelper {
|
||||
};
|
||||
|
||||
playAudioEvery30Seconds = () => {
|
||||
const { audioAlertType, alertIfUnreadConversationExist } =
|
||||
this.notificationConfig;
|
||||
|
||||
// Audio alert is disabled dismiss the timer
|
||||
if (this.audioAlertType === 'none') {
|
||||
return;
|
||||
}
|
||||
// If assigned conversation flag is disabled dismiss the timer
|
||||
if (!this.alertIfUnreadConversationExist) {
|
||||
return;
|
||||
}
|
||||
if (audioAlertType.includes('none')) return;
|
||||
|
||||
this.clearSetTimeout();
|
||||
};
|
||||
// If unread conversation flag is disabled, dismiss the timer
|
||||
if (!alertIfUnreadConversationExist) return;
|
||||
|
||||
isConversationAssignedToCurrentUser = message => {
|
||||
const conversationAssigneeId = message?.conversation?.assignee_id;
|
||||
return conversationAssigneeId === this.currentUserId;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
isMessageFromCurrentConversation = message => {
|
||||
return (
|
||||
window.WOOT.$store.getters.getSelectedChat?.id === message.conversation_id
|
||||
);
|
||||
};
|
||||
|
||||
isMessageFromCurrentUser = message => {
|
||||
return message?.sender_id === this.currentUserId;
|
||||
this.resetRecurringTimer();
|
||||
};
|
||||
|
||||
shouldNotifyOnMessage = message => {
|
||||
if (this.audioAlertType === 'mine') {
|
||||
return this.isConversationAssignedToCurrentUser(message);
|
||||
const { audioAlertType } = this.notificationConfig;
|
||||
if (audioAlertType.includes('none')) return false;
|
||||
if (audioAlertType.includes('all')) return true;
|
||||
|
||||
const assignedToMe = isConversationAssignedToMe(
|
||||
message,
|
||||
this.currentUser.id
|
||||
);
|
||||
const isUnassigned = isConversationUnassigned(message);
|
||||
|
||||
const shouldPlayAudio = [];
|
||||
|
||||
if (audioAlertType.includes(EVENT_TYPES.ASSIGNED)) {
|
||||
shouldPlayAudio.push(assignedToMe);
|
||||
}
|
||||
return this.audioAlertType === 'all';
|
||||
if (audioAlertType.includes(EVENT_TYPES.UNASSIGNED)) {
|
||||
shouldPlayAudio.push(isUnassigned);
|
||||
}
|
||||
if (audioAlertType.includes(EVENT_TYPES.NOTME)) {
|
||||
shouldPlayAudio.push(!isUnassigned && !assignedToMe);
|
||||
}
|
||||
|
||||
return shouldPlayAudio.some(Boolean);
|
||||
};
|
||||
|
||||
onNewMessage = message => {
|
||||
// If the message is sent by the current user or the
|
||||
// correct notification is not enabled, then dismiss the alert
|
||||
if (
|
||||
this.isMessageFromCurrentUser(message) ||
|
||||
!this.shouldNotifyOnMessage(message)
|
||||
) {
|
||||
// If the user does not have the permission to view the conversation, then dismiss the alert
|
||||
// FIX ME: There shouldn't be a new message if the user has no access to the conversation.
|
||||
if (!this.store.hasConversationPermission(this.currentUser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the message is sent by the current user then dismiss the alert
|
||||
if (isMessageFromCurrentUser(message, this.currentUser.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.shouldNotifyOnMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -135,19 +187,22 @@ class DashboardAudioNotificationHelper {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user looking at the conversation, then dismiss the alert
|
||||
if (this.isMessageFromCurrentConversation(message) && !document.hidden) {
|
||||
return;
|
||||
}
|
||||
// If the user has disabled alerts when active on the dashboard, the dismiss the alert
|
||||
if (this.playAlertOnlyWhenHidden && !document.hidden) {
|
||||
return;
|
||||
if (WindowVisibilityHelper.isWindowVisible()) {
|
||||
// If the user looking at the conversation, then dismiss the alert
|
||||
if (this.store.isMessageFromCurrentConversation(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user has disabled alerts when active on the dashboard, the dismiss the alert
|
||||
if (this.notificationConfig.playAlertOnlyWhenHidden) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
window.playAudioAlert();
|
||||
this.playAudioAlert();
|
||||
showBadgeOnFavicon();
|
||||
this.playAudioEvery30Seconds();
|
||||
};
|
||||
}
|
||||
|
||||
export default new DashboardAudioNotificationHelper();
|
||||
export default new DashboardAudioNotificationHelper(GlobalStore);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
export class WindowVisibilityHelper {
|
||||
constructor() {
|
||||
this.isVisible = true;
|
||||
this.initializeEvent();
|
||||
}
|
||||
|
||||
initializeEvent = () => {
|
||||
window.addEventListener('blur', () => {
|
||||
this.isVisible = false;
|
||||
});
|
||||
window.addEventListener('focus', () => {
|
||||
this.isVisible = true;
|
||||
});
|
||||
};
|
||||
|
||||
isWindowVisible() {
|
||||
return !document.hidden && this.isVisible;
|
||||
}
|
||||
}
|
||||
|
||||
export default new WindowVisibilityHelper();
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
getAssignee,
|
||||
isConversationUnassigned,
|
||||
isConversationAssignedToMe,
|
||||
isMessageFromCurrentUser,
|
||||
} from '../AudioMessageHelper';
|
||||
|
||||
describe('getAssignee', () => {
|
||||
it('should return assignee_id when present', () => {
|
||||
const message = { conversation: { assignee_id: 1 } };
|
||||
expect(getAssignee(message)).toBe(1);
|
||||
});
|
||||
|
||||
it('should return undefined when no assignee_id', () => {
|
||||
const message = { conversation: null };
|
||||
expect(getAssignee(message)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle null message', () => {
|
||||
expect(getAssignee(null)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConversationUnassigned', () => {
|
||||
it('should return true when no assignee', () => {
|
||||
const message = { conversation: { assignee_id: null } };
|
||||
expect(isConversationUnassigned(message)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when has assignee', () => {
|
||||
const message = { conversation: { assignee_id: 1 } };
|
||||
expect(isConversationUnassigned(message)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle null message', () => {
|
||||
expect(isConversationUnassigned(null)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isConversationAssignedToMe', () => {
|
||||
const currentUserId = 1;
|
||||
|
||||
it('should return true when assigned to current user', () => {
|
||||
const message = { conversation: { assignee_id: 1 } };
|
||||
expect(isConversationAssignedToMe(message, currentUserId)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when assigned to different user', () => {
|
||||
const message = { conversation: { assignee_id: 2 } };
|
||||
expect(isConversationAssignedToMe(message, currentUserId)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when unassigned', () => {
|
||||
const message = { conversation: { assignee_id: null } };
|
||||
expect(isConversationAssignedToMe(message, currentUserId)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle null message', () => {
|
||||
expect(isConversationAssignedToMe(null, currentUserId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMessageFromCurrentUser', () => {
|
||||
const currentUserId = 1;
|
||||
|
||||
it('should return true when message is from current user', () => {
|
||||
const message = { sender: { id: 1 } };
|
||||
expect(isMessageFromCurrentUser(message, currentUserId)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when message is from different user', () => {
|
||||
const message = { sender: { id: 2 } };
|
||||
expect(isMessageFromCurrentUser(message, currentUserId)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle null message', () => {
|
||||
expect(isMessageFromCurrentUser(null, currentUserId)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import AudioNotificationStore from '../AudioNotificationStore';
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions';
|
||||
import { getUserPermissions } from 'dashboard/helper/permissionsHelper';
|
||||
vi.mock('dashboard/helper/permissionsHelper', () => ({
|
||||
getUserPermissions: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('AudioNotificationStore', () => {
|
||||
let store;
|
||||
let audioNotificationStore;
|
||||
|
||||
beforeEach(() => {
|
||||
store = {
|
||||
getters: {
|
||||
getMineChats: vi.fn(),
|
||||
getSelectedChat: null,
|
||||
getCurrentAccountId: 1,
|
||||
},
|
||||
};
|
||||
audioNotificationStore = new AudioNotificationStore(store);
|
||||
});
|
||||
|
||||
describe('hasUnreadConversation', () => {
|
||||
it('should return true when there are unread conversations', () => {
|
||||
store.getters.getMineChats.mockReturnValue([
|
||||
{ id: 1, unread_count: 2 },
|
||||
{ id: 2, unread_count: 0 },
|
||||
]);
|
||||
|
||||
expect(audioNotificationStore.hasUnreadConversation()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when there are no unread conversations', () => {
|
||||
store.getters.getMineChats.mockReturnValue([
|
||||
{ id: 1, unread_count: 0 },
|
||||
{ id: 2, unread_count: 0 },
|
||||
]);
|
||||
|
||||
expect(audioNotificationStore.hasUnreadConversation()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when there are no conversations', () => {
|
||||
store.getters.getMineChats.mockReturnValue([]);
|
||||
|
||||
expect(audioNotificationStore.hasUnreadConversation()).toBe(false);
|
||||
});
|
||||
|
||||
it('should call getMineChats with correct parameters', () => {
|
||||
store.getters.getMineChats.mockReturnValue([]);
|
||||
audioNotificationStore.hasUnreadConversation();
|
||||
|
||||
expect(store.getters.getMineChats).toHaveBeenCalledWith({
|
||||
assigneeType: 'me',
|
||||
status: 'open',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMessageFromCurrentConversation', () => {
|
||||
it('should return true when message is from selected chat', () => {
|
||||
store.getters.getSelectedChat = { id: 6179 };
|
||||
const message = { conversation_id: 6179 };
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromCurrentConversation(message)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when message is from different chat', () => {
|
||||
store.getters.getSelectedChat = { id: 6179 };
|
||||
const message = { conversation_id: 1337 };
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromCurrentConversation(message)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no chat is selected', () => {
|
||||
store.getters.getSelectedChat = null;
|
||||
const message = { conversation_id: 6179 };
|
||||
|
||||
expect(
|
||||
audioNotificationStore.isMessageFromCurrentConversation(message)
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasConversationPermission', () => {
|
||||
const mockUser = { id: 'user123' };
|
||||
|
||||
beforeEach(() => {
|
||||
getUserPermissions.mockReset();
|
||||
});
|
||||
|
||||
it('should return true when user has a required role', () => {
|
||||
getUserPermissions.mockReturnValue([ROLES[0]]);
|
||||
|
||||
expect(audioNotificationStore.hasConversationPermission(mockUser)).toBe(
|
||||
true
|
||||
);
|
||||
expect(getUserPermissions).toHaveBeenCalledWith(mockUser, 1);
|
||||
});
|
||||
|
||||
it('should return true when user has a conversation permission', () => {
|
||||
getUserPermissions.mockReturnValue([CONVERSATION_PERMISSIONS[0]]);
|
||||
|
||||
expect(audioNotificationStore.hasConversationPermission(mockUser)).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when user has no required permissions', () => {
|
||||
getUserPermissions.mockReturnValue(['some-other-permission']);
|
||||
|
||||
expect(audioNotificationStore.hasConversationPermission(mockUser)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false when user has no permissions', () => {
|
||||
getUserPermissions.mockReturnValue([]);
|
||||
|
||||
expect(audioNotificationStore.hasConversationPermission(mockUser)).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { WindowVisibilityHelper } from '../WindowVisibilityHelper';
|
||||
|
||||
describe('WindowVisibilityHelper', () => {
|
||||
let blurCallback;
|
||||
let focusCallback;
|
||||
let windowEventListeners;
|
||||
let documentHiddenValue = false;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.resetAllMocks();
|
||||
|
||||
// Reset event listeners before each test
|
||||
windowEventListeners = {};
|
||||
|
||||
// Mock window.addEventListener
|
||||
window.addEventListener = vi.fn((event, callback) => {
|
||||
windowEventListeners[event] = callback;
|
||||
if (event === 'blur') blurCallback = callback;
|
||||
if (event === 'focus') focusCallback = callback;
|
||||
});
|
||||
|
||||
// Mock document.hidden with a getter that returns our controlled value
|
||||
Object.defineProperty(document, 'hidden', {
|
||||
configurable: true,
|
||||
get: () => documentHiddenValue,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
documentHiddenValue = false;
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should add blur and focus event listeners', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
expect(helper.isVisible).toBe(true);
|
||||
|
||||
expect(window.addEventListener).toHaveBeenCalledTimes(2);
|
||||
expect(window.addEventListener).toHaveBeenCalledWith(
|
||||
'blur',
|
||||
expect.any(Function)
|
||||
);
|
||||
expect(window.addEventListener).toHaveBeenCalledWith(
|
||||
'focus',
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('window events', () => {
|
||||
it('should set isVisible to false on blur', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
blurCallback();
|
||||
expect(helper.isVisible).toBe(false);
|
||||
});
|
||||
|
||||
it('should set isVisible to true on focus', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
blurCallback(); // First blur the window
|
||||
focusCallback(); // Then focus it
|
||||
expect(helper.isVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle multiple blur/focus events', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
|
||||
blurCallback();
|
||||
expect(helper.isVisible).toBe(false);
|
||||
|
||||
focusCallback();
|
||||
expect(helper.isVisible).toBe(true);
|
||||
|
||||
blurCallback();
|
||||
expect(helper.isVisible).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isWindowVisible', () => {
|
||||
it('should return true when document is visible and window is focused', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
documentHiddenValue = false;
|
||||
helper.isVisible = true;
|
||||
|
||||
expect(helper.isWindowVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when document is hidden', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
documentHiddenValue = true;
|
||||
helper.isVisible = true;
|
||||
|
||||
expect(helper.isWindowVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when window is not focused', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
documentHiddenValue = false;
|
||||
helper.isVisible = false;
|
||||
|
||||
expect(helper.isWindowVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when both document is hidden and window is not focused', () => {
|
||||
const helper = new WindowVisibilityHelper();
|
||||
documentHiddenValue = true;
|
||||
helper.isVisible = false;
|
||||
|
||||
expect(helper.isWindowVisible()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -85,7 +85,8 @@ class ReconnectService {
|
||||
};
|
||||
|
||||
fetchConversationMessagesOnReconnect = async () => {
|
||||
const { conversation_id: conversationId } = this.router.currentRoute.params;
|
||||
const { conversation_id: conversationId } =
|
||||
this.router.currentRoute.value.params;
|
||||
if (conversationId) {
|
||||
await this.store.dispatch('syncActiveConversationMessages', {
|
||||
conversationId: Number(conversationId),
|
||||
@@ -109,7 +110,7 @@ class ReconnectService {
|
||||
};
|
||||
|
||||
handleRouteSpecificFetch = async () => {
|
||||
const currentRoute = this.router.currentRoute.name;
|
||||
const currentRoute = this.router.currentRoute.value.name;
|
||||
if (isAConversationRoute(currentRoute, true)) {
|
||||
await this.fetchConversationsOnReconnect();
|
||||
await this.fetchConversationMessagesOnReconnect();
|
||||
@@ -123,7 +124,8 @@ class ReconnectService {
|
||||
};
|
||||
|
||||
setConversationLastMessageId = async () => {
|
||||
const { conversation_id: conversationId } = this.router.currentRoute.params;
|
||||
const { conversation_id: conversationId } =
|
||||
this.router.currentRoute.value.params;
|
||||
if (conversationId) {
|
||||
await this.store.dispatch('setConversationLastMessageId', {
|
||||
conversationId: Number(conversationId),
|
||||
|
||||
@@ -108,3 +108,20 @@ export const hasValidAvatarUrl = avatarUrl => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const timeStampAppendedURL = dataUrl => {
|
||||
const url = new URL(dataUrl);
|
||||
if (!url.searchParams.has('t')) {
|
||||
url.searchParams.append('t', Date.now());
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
export const getHostNameFromURL = url => {
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -201,7 +201,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
}
|
||||
|
||||
export default {
|
||||
init(pubsubToken) {
|
||||
return new ActionCableConnector(window.WOOT, pubsubToken);
|
||||
init(store, pubsubToken) {
|
||||
return new ActionCableConnector({ $store: store }, pubsubToken);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Default agent object representing 'None'
|
||||
* @type {Object}
|
||||
*/
|
||||
export const createNoneAgent = {
|
||||
confirmed: true,
|
||||
name: 'None',
|
||||
id: 0,
|
||||
role: 'agent',
|
||||
account_id: 0,
|
||||
email: 'None',
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters and sorts agents by availability status
|
||||
* @param {Array} agents - List of agents
|
||||
* @param {string} availability - Availability status to filter by
|
||||
* @returns {Array} Filtered and sorted list of agents
|
||||
*/
|
||||
export const getAgentsByAvailability = (agents, availability) => {
|
||||
return agents
|
||||
.filter(agent => agent.availability_status === availability)
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts agents by availability status: online, busy, then offline
|
||||
* @param {Array} agents - List of agents
|
||||
* @returns {Array} Sorted list of agents
|
||||
*/
|
||||
export const getSortedAgentsByAvailability = agents => {
|
||||
const onlineAgents = getAgentsByAvailability(agents, 'online');
|
||||
const busyAgents = getAgentsByAvailability(agents, 'busy');
|
||||
const offlineAgents = getAgentsByAvailability(agents, 'offline');
|
||||
const filteredAgents = [...onlineAgents, ...busyAgents, ...offlineAgents];
|
||||
return filteredAgents;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the availability status of the current user based on the current account
|
||||
* @param {Array} agents - List of agents
|
||||
* @param {Object} currentUser - Current user object
|
||||
* @param {number} currentAccountId - ID of the current account
|
||||
* @returns {Array} Updated list of agents with dynamic presence
|
||||
*/
|
||||
// Here we are updating the availability status of the current user dynamically
|
||||
// based on the current account availability status
|
||||
export const getAgentsByUpdatedPresence = (
|
||||
agents,
|
||||
currentUser,
|
||||
currentAccountId
|
||||
) => {
|
||||
const agentsWithDynamicPresenceUpdate = agents.map(item =>
|
||||
item.id === currentUser.id
|
||||
? {
|
||||
...item,
|
||||
availability_status: currentUser.accounts.find(
|
||||
account => account.id === currentAccountId
|
||||
).availability_status,
|
||||
}
|
||||
: item
|
||||
);
|
||||
return agentsWithDynamicPresenceUpdate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Combines the filtered agents with the 'None' agent option if applicable.
|
||||
*
|
||||
* @param {Array} filteredAgentsByAvailability - The list of agents sorted by availability.
|
||||
* @param {boolean} includeNoneAgent - Whether to include the 'None' agent option.
|
||||
* @param {boolean} isAgentSelected - Whether an agent is currently selected.
|
||||
* @returns {Array} The combined list of agents, potentially including the 'None' agent.
|
||||
*/
|
||||
export const getCombinedAgents = (
|
||||
filteredAgentsByAvailability,
|
||||
includeNoneAgent,
|
||||
isAgentSelected
|
||||
) => {
|
||||
return [
|
||||
...(includeNoneAgent && isAgentSelected ? [createNoneAgent] : []),
|
||||
...filteredAgentsByAvailability,
|
||||
];
|
||||
};
|
||||
@@ -3,41 +3,16 @@ import {
|
||||
OPERATOR_TYPES_3,
|
||||
OPERATOR_TYPES_4,
|
||||
} from 'dashboard/routes/dashboard/settings/automation/operators';
|
||||
import {
|
||||
DEFAULT_MESSAGE_CREATED_CONDITION,
|
||||
DEFAULT_CONVERSATION_OPENED_CONDITION,
|
||||
DEFAULT_OTHER_CONDITION,
|
||||
DEFAULT_ACTIONS,
|
||||
MESSAGE_CONDITION_VALUES,
|
||||
PRIORITY_CONDITION_VALUES,
|
||||
} from 'dashboard/constants/automation';
|
||||
import filterQueryGenerator from './filterQueryGenerator';
|
||||
import actionQueryGenerator from './actionQueryGenerator';
|
||||
const MESSAGE_CONDITION_VALUES = [
|
||||
{
|
||||
id: 'incoming',
|
||||
name: 'Incoming Message',
|
||||
},
|
||||
{
|
||||
id: 'outgoing',
|
||||
name: 'Outgoing Message',
|
||||
},
|
||||
];
|
||||
|
||||
export const PRIORITY_CONDITION_VALUES = [
|
||||
{
|
||||
id: 'nil',
|
||||
name: 'None',
|
||||
},
|
||||
{
|
||||
id: 'low',
|
||||
name: 'Low',
|
||||
},
|
||||
{
|
||||
id: 'medium',
|
||||
name: 'Medium',
|
||||
},
|
||||
{
|
||||
id: 'high',
|
||||
name: 'High',
|
||||
},
|
||||
{
|
||||
id: 'urgent',
|
||||
name: 'Urgent',
|
||||
},
|
||||
];
|
||||
|
||||
export const getCustomAttributeInputType = key => {
|
||||
const customAttributeMap = {
|
||||
@@ -198,45 +173,16 @@ export const getFileName = (action, files = []) => {
|
||||
|
||||
export const getDefaultConditions = eventName => {
|
||||
if (eventName === 'message_created') {
|
||||
return [
|
||||
{
|
||||
attribute_key: 'message_type',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
return DEFAULT_MESSAGE_CREATED_CONDITION;
|
||||
}
|
||||
if (eventName === 'conversation_opened') {
|
||||
return [
|
||||
{
|
||||
attribute_key: 'browser_language',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
return DEFAULT_CONVERSATION_OPENED_CONDITION;
|
||||
}
|
||||
return [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
return DEFAULT_OTHER_CONDITION;
|
||||
};
|
||||
|
||||
export const getDefaultActions = () => {
|
||||
return [
|
||||
{
|
||||
action_name: 'assign_agent',
|
||||
action_params: [],
|
||||
},
|
||||
];
|
||||
return DEFAULT_ACTIONS;
|
||||
};
|
||||
|
||||
export const filterCustomAttributes = customAttributes => {
|
||||
@@ -297,3 +243,100 @@ export const generateCustomAttributes = (
|
||||
}
|
||||
return customAttributes;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get attributes for a given key from automation types.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {string} key - The key to get attributes for.
|
||||
* @returns {Array} Array of condition objects for the given key.
|
||||
*/
|
||||
export const getAttributes = (automationTypes, key) => {
|
||||
return automationTypes[key].conditions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the automation type for a given key.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {Object} automation - The automation object.
|
||||
* @param {string} key - The key to get the automation type for.
|
||||
* @returns {Object} The automation type object.
|
||||
*/
|
||||
export const getAutomationType = (automationTypes, automation, key) => {
|
||||
return automationTypes[automation.event_name].conditions.find(
|
||||
condition => condition.key === key
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the input type for a given key.
|
||||
* @param {Array} allCustomAttributes - Array of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {Object} automation - The automation object.
|
||||
* @param {string} key - The key to get the input type for.
|
||||
* @returns {string} The input type.
|
||||
*/
|
||||
export const getInputType = (
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
key
|
||||
) => {
|
||||
const customAttribute = isACustomAttribute(allCustomAttributes, key);
|
||||
if (customAttribute) {
|
||||
return getCustomAttributeInputType(customAttribute.attribute_display_type);
|
||||
}
|
||||
const type = getAutomationType(automationTypes, automation, key);
|
||||
return type.inputType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get operators for a given key.
|
||||
* @param {Array} allCustomAttributes - Array of all custom attributes.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {Object} automation - The automation object.
|
||||
* @param {string} mode - The mode ('edit' or other).
|
||||
* @param {string} key - The key to get operators for.
|
||||
* @returns {Array} Array of operators.
|
||||
*/
|
||||
export const getOperators = (
|
||||
allCustomAttributes,
|
||||
automationTypes,
|
||||
automation,
|
||||
mode,
|
||||
key
|
||||
) => {
|
||||
if (mode === 'edit') {
|
||||
const customAttribute = isACustomAttribute(allCustomAttributes, key);
|
||||
if (customAttribute) {
|
||||
return getOperatorTypes(customAttribute.attribute_display_type);
|
||||
}
|
||||
}
|
||||
const type = getAutomationType(automationTypes, automation, key);
|
||||
return type.filterOperators;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the custom attribute type for a given key.
|
||||
* @param {Object} automationTypes - Object containing automation types.
|
||||
* @param {Object} automation - The automation object.
|
||||
* @param {string} key - The key to get the custom attribute type for.
|
||||
* @returns {string} The custom attribute type.
|
||||
*/
|
||||
export const getCustomAttributeType = (automationTypes, automation, key) => {
|
||||
return automationTypes[automation.event_name].conditions.find(
|
||||
i => i.key === key
|
||||
).customAttributeType;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determine if an action input should be shown.
|
||||
* @param {Array} automationActionTypes - Array of automation action type objects.
|
||||
* @param {string} action - The action to check.
|
||||
* @returns {boolean} True if the action input should be shown, false otherwise.
|
||||
*/
|
||||
export const showActionInput = (automationActionTypes, action) => {
|
||||
if (action === 'send_email_to_team' || action === 'send_message')
|
||||
return false;
|
||||
const type = automationActionTypes.find(i => i.key === action).inputType;
|
||||
return !!type;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import wootConstants from 'dashboard/constants/globals';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
import {
|
||||
CMD_MUTE_CONVERSATION,
|
||||
CMD_REOPEN_CONVERSATION,
|
||||
CMD_RESOLVE_CONVERSATION,
|
||||
CMD_SEND_TRANSCRIPT,
|
||||
CMD_SNOOZE_CONVERSATION,
|
||||
CMD_UNMUTE_CONVERSATION,
|
||||
} from 'dashboard/helper/commandbar/events';
|
||||
|
||||
import {
|
||||
ICON_MUTE_CONVERSATION,
|
||||
ICON_REOPEN_CONVERSATION,
|
||||
ICON_RESOLVE_CONVERSATION,
|
||||
ICON_SEND_TRANSCRIPT,
|
||||
ICON_SNOOZE_CONVERSATION,
|
||||
ICON_UNMUTE_CONVERSATION,
|
||||
} from 'dashboard/helper/commandbar/icons';
|
||||
|
||||
const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS;
|
||||
|
||||
export const OPEN_CONVERSATION_ACTIONS = [
|
||||
{
|
||||
id: 'resolve_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.RESOLVE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
|
||||
icon: ICON_RESOLVE_CONVERSATION,
|
||||
handler: () => emitter.emit(CMD_RESOLVE_CONVERSATION),
|
||||
},
|
||||
];
|
||||
|
||||
export const createSnoozeHandlers = (busEventName, parentId, section) => {
|
||||
return Object.values(SNOOZE_OPTIONS).map(option => ({
|
||||
id: option,
|
||||
title: `COMMAND_BAR.COMMANDS.${option.toUpperCase()}`,
|
||||
parent: parentId,
|
||||
section: section,
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
handler: () => emitter.emit(busEventName, option),
|
||||
}));
|
||||
};
|
||||
|
||||
export const SNOOZE_CONVERSATION_ACTIONS = [
|
||||
{
|
||||
id: 'snooze_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.SNOOZE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
|
||||
icon: ICON_SNOOZE_CONVERSATION,
|
||||
children: Object.values(SNOOZE_OPTIONS),
|
||||
},
|
||||
...createSnoozeHandlers(
|
||||
CMD_SNOOZE_CONVERSATION,
|
||||
'snooze_conversation',
|
||||
'COMMAND_BAR.SECTIONS.SNOOZE_CONVERSATION'
|
||||
),
|
||||
];
|
||||
|
||||
export const RESOLVED_CONVERSATION_ACTIONS = [
|
||||
{
|
||||
id: 'reopen_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.REOPEN_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
|
||||
icon: ICON_REOPEN_CONVERSATION,
|
||||
handler: () => emitter.emit(CMD_REOPEN_CONVERSATION),
|
||||
},
|
||||
];
|
||||
|
||||
export const SEND_TRANSCRIPT_ACTION = {
|
||||
id: 'send_transcript',
|
||||
title: 'COMMAND_BAR.COMMANDS.SEND_TRANSCRIPT',
|
||||
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
|
||||
icon: ICON_SEND_TRANSCRIPT,
|
||||
handler: () => emitter.emit(CMD_SEND_TRANSCRIPT),
|
||||
};
|
||||
|
||||
export const UNMUTE_ACTION = {
|
||||
id: 'unmute_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.UNMUTE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
|
||||
icon: ICON_UNMUTE_CONVERSATION,
|
||||
handler: () => emitter.emit(CMD_UNMUTE_CONVERSATION),
|
||||
};
|
||||
|
||||
export const MUTE_ACTION = {
|
||||
id: 'mute_conversation',
|
||||
title: 'COMMAND_BAR.COMMANDS.MUTE_CONVERSATION',
|
||||
section: 'COMMAND_BAR.SECTIONS.CONVERSATION',
|
||||
icon: ICON_MUTE_CONVERSATION,
|
||||
handler: () => emitter.emit(CMD_MUTE_CONVERSATION),
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
// General Actions - Switch conversation tabs
|
||||
export const CMD_SWITCH_TAB = 'CMD_SWITCH_TAB';
|
||||
|
||||
// General Actions - Switch conversation status
|
||||
export const CMD_SWITCH_STATUS = 'CMD_SWITCH_STATUS';
|
||||
|
||||
// Conversation Actions
|
||||
export const CMD_MUTE_CONVERSATION = 'CMD_MUTE_CONVERSATION';
|
||||
export const CMD_UNMUTE_CONVERSATION = 'CMD_UNMUTE_CONVERSATION';
|
||||
export const CMD_SEND_TRANSCRIPT = 'CMD_SEND_TRANSCRIPT';
|
||||
export const CMD_TOGGLE_CONTACT_SIDEBAR = 'CMD_TOGGLE_CONTACT_SIDEBAR';
|
||||
|
||||
// Status Commands
|
||||
export const CMD_REOPEN_CONVERSATION = 'CMD_REOPEN_CONVERSATION';
|
||||
export const CMD_RESOLVE_CONVERSATION = 'CMD_RESOLVE_CONVERSATION';
|
||||
export const CMD_SNOOZE_CONVERSATION = 'CMD_SNOOZE_CONVERSATION';
|
||||
export const CMD_AI_ASSIST = 'CMD_AI_ASSIST';
|
||||
|
||||
// Bulk Actions
|
||||
export const CMD_BULK_ACTION_SNOOZE_CONVERSATION =
|
||||
'CMD_BULK_ACTION_SNOOZE_CONVERSATION';
|
||||
export const CMD_BULK_ACTION_REOPEN_CONVERSATION =
|
||||
'CMD_BULK_ACTION_REOPEN_CONVERSATION';
|
||||
export const CMD_BULK_ACTION_RESOLVE_CONVERSATION =
|
||||
'CMD_BULK_ACTION_RESOLVE_CONVERSATION';
|
||||
|
||||
// Inbox Commands (Notifications)
|
||||
export const CMD_SNOOZE_NOTIFICATION = 'CMD_SNOOZE_NOTIFICATION';
|
||||
@@ -0,0 +1,75 @@
|
||||
export const ICON_ADD_LABEL = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19.75 2A2.25 2.25 0 0 1 22 4.25v5.462a3.25 3.25 0 0 1-.952 2.298l-8.5 8.503a3.255 3.255 0 0 1-4.597.001L3.489 16.06a3.25 3.25 0 0 1-.003-4.596l8.5-8.51A3.25 3.25 0 0 1 14.284 2h5.465Zm0 1.5h-5.465c-.465 0-.91.185-1.239.513l-8.512 8.523a1.75 1.75 0 0 0 .015 2.462l4.461 4.454a1.755 1.755 0 0 0 2.477 0l8.5-8.503a1.75 1.75 0 0 0 .513-1.237V4.25a.75.75 0 0 0-.75-.75ZM17 5.502a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_ASSIGN_AGENT = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17.5 12a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm-5.478 2a6.474 6.474 0 0 0-.708 1.5h-7.06a.75.75 0 0 0-.75.75v.907c0 .656.286 1.279.783 1.706C5.545 19.945 7.44 20.501 10 20.501c.599 0 1.162-.03 1.688-.091.25.5.563.964.93 1.38-.803.141-1.676.21-2.618.21-2.89 0-5.128-.656-6.691-2a3.75 3.75 0 0 1-1.305-2.843v-.907A2.25 2.25 0 0 1 4.254 14h7.768Zm4.697.588-.069.058-2.515 2.517-.041.05-.035.058-.032.078-.012.043-.01.086.003.088.019.085.032.078.025.042.05.066 2.516 2.516a.5.5 0 0 0 .765-.638l-.058-.069L15.711 18h4.79a.5.5 0 0 0 .491-.41L21 17.5a.5.5 0 0 0-.41-.492L20.5 17h-4.789l1.646-1.647a.5.5 0 0 0 .058-.637l-.058-.07a.5.5 0 0 0-.638-.058ZM10 2.004a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_MUTE_CONVERSATION = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12.92 3.316c.806-.717 2.08-.145 2.08.934v15.496c0 1.078-1.274 1.65-2.08.934l-4.492-3.994a.75.75 0 0 0-.498-.19H4.25A2.25 2.25 0 0 1 2 14.247V9.75a2.25 2.25 0 0 1 2.25-2.25h3.68a.75.75 0 0 0 .498-.19l4.491-3.993Zm.58 1.49L9.425 8.43A2.25 2.25 0 0 1 7.93 9H4.25a.75.75 0 0 0-.75.75v4.497c0 .415.336.75.75.75h3.68a2.25 2.25 0 0 1 1.495.57l4.075 3.623V4.807ZM16.22 9.22a.75.75 0 0 1 1.06 0L19 10.94l1.72-1.72a.75.75 0 1 1 1.06 1.06L20.06 12l1.72 1.72a.75.75 0 1 1-1.06 1.06L19 13.06l-1.72 1.72a.75.75 0 1 1-1.06-1.06L17.94 12l-1.72-1.72a.75.75 0 0 1 0-1.06Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_UNMUTE_CONVERSATION = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M15 4.25c0-1.079-1.274-1.65-2.08-.934L8.427 7.309a.75.75 0 0 1-.498.19H4.25A2.25 2.25 0 0 0 2 9.749v4.497a2.25 2.25 0 0 0 2.25 2.25h3.68a.75.75 0 0 1 .498.19l4.491 3.994c.806.716 2.081.144 2.081-.934V4.25ZM9.425 8.43 13.5 4.807v14.382l-4.075-3.624a2.25 2.25 0 0 0-1.495-.569H4.25a.75.75 0 0 1-.75-.75V9.75a.75.75 0 0 1 .75-.75h3.68a2.25 2.25 0 0 0 1.495-.569ZM18.992 5.897a.75.75 0 0 1 1.049.157A9.959 9.959 0 0 1 22 12a9.96 9.96 0 0 1-1.96 5.946.75.75 0 0 1-1.205-.892A8.459 8.459 0 0 0 20.5 12a8.459 8.459 0 0 0-1.665-5.054.75.75 0 0 1 .157-1.049Z" fill="#212121"/><path d="M17.143 8.37a.75.75 0 0 1 1.017.302c.536.99.84 2.125.84 3.328a6.973 6.973 0 0 1-.84 3.328.75.75 0 0 1-1.32-.714c.42-.777.66-1.666.66-2.614s-.24-1.837-.66-2.614a.75.75 0 0 1 .303-1.017Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_REMOVE_LABEL = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19.75 2A2.25 2.25 0 0 1 22 4.25v5.462a3.25 3.25 0 0 1-.952 2.298l-.026.026a6.473 6.473 0 0 0-1.43-.692l.395-.395a1.75 1.75 0 0 0 .513-1.237V4.25a.75.75 0 0 0-.75-.75h-5.466c-.464 0-.91.185-1.238.513l-8.512 8.523a1.75 1.75 0 0 0 .015 2.462l4.461 4.454a1.755 1.755 0 0 0 2.33.13c.165.487.386.947.654 1.374a3.256 3.256 0 0 1-4.043-.442L3.489 16.06a3.25 3.25 0 0 1-.004-4.596l8.5-8.51a3.25 3.25 0 0 1 2.3-.953h5.465ZM17 5.502a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM17.5 23a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11Zm-2.354-7.854a.5.5 0 0 1 .708 0l1.646 1.647 1.646-1.647a.5.5 0 0 1 .708.708L18.207 17.5l1.647 1.646a.5.5 0 0 1-.708.708L17.5 18.207l-1.646 1.647a.5.5 0 0 1-.708-.708l1.647-1.646-1.647-1.646a.5.5 0 0 1 0-.708Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_REOPEN_CONVERSATION = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19.25 2a.75.75 0 0 0-.743.648l-.007.102v5.69l-4.574-4.56a6.41 6.41 0 0 0-8.878-.179l-.186.18a6.41 6.41 0 0 0 0 9.063l8.845 8.84a.75.75 0 0 0 1.06-1.062l-8.845-8.838a4.91 4.91 0 0 1 6.766-7.112l.178.17L17.438 9.5H11.75a.75.75 0 0 0-.743.648L11 10.25c0 .38.282.694.648.743l.102.007h7.5a.75.75 0 0 0 .743-.648L20 10.25v-7.5a.75.75 0 0 0-.75-.75Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_RESOLVE_CONVERSATION = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm0 1.5a8.5 8.5 0 1 0 0 17 8.5 8.5 0 0 0 0-17Zm-1.25 9.94 4.47-4.47a.75.75 0 0 1 1.133.976l-.073.084-5 5a.75.75 0 0 1-.976.073l-.084-.073-2.5-2.5a.75.75 0 0 1 .976-1.133l.084.073 1.97 1.97 4.47-4.47-4.47 4.47Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_SEND_TRANSCRIPT = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19.75 11.5a.75.75 0 0 1 .743.648l.007.102v5a4.75 4.75 0 0 1-4.533 4.745L15.75 22h-7.5c-.98 0-1.813-.626-2.122-1.5h9.622l.184-.005a3.25 3.25 0 0 0 3.06-3.06L19 17.25v-5a.75.75 0 0 1 .75-.75Zm-2.5-2a.75.75 0 0 1 .743.648l.007.102v7a2.25 2.25 0 0 1-2.096 2.245l-.154.005h-10a2.25 2.25 0 0 1-2.245-2.096L3.5 17.25v-7a.75.75 0 0 1 1.493-.102L5 10.25v7c0 .38.282.694.648.743L5.75 18h10a.75.75 0 0 0 .743-.648l.007-.102v-7a.75.75 0 0 1 .75-.75ZM6.218 6.216l3.998-3.996a.75.75 0 0 1 .976-.073l.084.072 4.004 3.997a.75.75 0 0 1-.976 1.134l-.084-.073-2.72-2.714v9.692a.75.75 0 0 1-.648.743l-.102.007a.75.75 0 0 1-.743-.648L10 14.255V4.556L7.279 7.277a.75.75 0 0 1-.977.072l-.084-.072a.75.75 0 0 1-.072-.977l.072-.084 3.998-3.996-3.998 3.996Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_SNOOZE_CONVERSATION = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c5.523 0 10 4.478 10 10s-4.477 10-10 10S2 17.522 2 12S6.477 2 12 2Zm0 1.667c-4.595 0-8.333 3.738-8.333 8.333c0 4.595 3.738 8.333 8.333 8.333c4.595 0 8.333-3.738 8.333-8.333c0-4.595-3.738-8.333-8.333-8.333ZM11.25 6a.75.75 0 0 1 .743.648L12 6.75V12h3.25a.75.75 0 0 1 .102 1.493l-.102.007h-4a.75.75 0 0 1-.743-.648l-.007-.102v-6a.75.75 0 0 1 .75-.75Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_SNOOZE_UNTIL_NEXT_WEEK = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M7.75 7a.75.75 0 0 0-.75.75v4c0 .414.336.75.75.75h8.5a.75.75 0 0 0 .75-.75v-4a.75.75 0 0 0-.75-.75h-8.5Zm.75 4V8.5h7V11h-7Z" fill="currentColor"/><path d="M17.75 21A3.25 3.25 0 0 0 21 17.75V6.25A3.25 3.25 0 0 0 17.75 3H6.25A3.25 3.25 0 0 0 3 6.25v11.5A3.25 3.25 0 0 0 6.25 21h11.5ZM19.5 6.25v11.5a1.75 1.75 0 0 1-1.75 1.75H6.25a1.75 1.75 0 0 1-1.75-1.75V6.25c0-.966.784-1.75 1.75-1.75h11.5c.966 0 1.75.784 1.75 1.75Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_SNOOZE_UNTIL_TOMORRROW = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 8.744C7.847 8.362 8.415 8 9.25 8c1.152 0 1.894.792 2.155 1.661.253.847.1 1.895-.62 2.618a8.092 8.092 0 0 1-.793.67l-.04.031c-.28.216-.53.412-.75.63-.255.256-.464.535-.585.89h2.133a.75.75 0 0 1 0 1.5h-3a.75.75 0 0 1-.75-.75c0-1.247.524-2.083 1.144-2.701.296-.296.618-.545.89-.756l.003-.002c.286-.221.508-.393.685-.57.272-.274.367-.725.246-1.13-.115-.381-.37-.591-.718-.591-.353 0-.535.137-.64.253a.843.843 0 0 0-.148.229v.003a.75.75 0 0 1-1.428-.462l.035-.096a2.343 2.343 0 0 1 .43-.683ZM13.25 8a.75.75 0 0 1 .75.75v2.75h1.5V8.75a.75.75 0 0 1 1.5 0v6.47a.75.75 0 0 1-1.5 0V13h-2.25a.75.75 0 0 1-.75-.75v-3.5a.75.75 0 0 1 .75-.75Z" fill="currentColor"/><path d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12s4.477 10 10 10 10-4.477 10-10ZM3.5 12a8.5 8.5 0 1 1 17 0 8.5 8.5 0 0 1-17 0Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_CONVERSATION_DASHBOARD = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M10.55 2.532a2.25 2.25 0 0 1 2.9 0l6.75 5.692c.507.428.8 1.057.8 1.72v9.803a1.75 1.75 0 0 1-1.75 1.75h-3.5a1.75 1.75 0 0 1-1.75-1.75v-5.5a.25.25 0 0 0-.25-.25h-3.5a.25.25 0 0 0-.25.25v5.5a1.75 1.75 0 0 1-1.75 1.75h-3.5A1.75 1.75 0 0 1 3 19.747V9.944c0-.663.293-1.292.8-1.72l6.75-5.692zm1.933 1.147a.75.75 0 0 0-.966 0L4.767 9.37a.75.75 0 0 0-.267.573v9.803c0 .138.112.25.25.25h3.5a.25.25 0 0 0 .25-.25v-5.5c0-.967.784-1.75 1.75-1.75h3.5c.966 0 1.75.783 1.75 1.75v5.5c0 .138.112.25.25.25h3.5a.25.25 0 0 0 .25-.25V9.944a.75.75 0 0 0-.267-.573l-6.75-5.692z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_CONTACT_DASHBOARD = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M17.754 14a2.249 2.249 0 0 1 2.25 2.249v.575c0 .894-.32 1.76-.902 2.438c-1.57 1.834-3.957 2.739-7.102 2.739c-3.146 0-5.532-.905-7.098-2.74a3.75 3.75 0 0 1-.898-2.435v-.577a2.249 2.249 0 0 1 2.249-2.25h11.501zm0 1.5H6.253a.749.749 0 0 0-.75.749v.577c0 .536.192 1.054.54 1.461c1.253 1.468 3.219 2.214 5.957 2.214s4.706-.746 5.962-2.214a2.25 2.25 0 0 0 .541-1.463v-.575a.749.749 0 0 0-.749-.75zM12 2.004a5 5 0 1 1 0 10a5 5 0 0 1 0-10zm0 1.5a3.5 3.5 0 1 0 0 7a3.5 3.5 0 0 0 0-7z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_REPORTS_OVERVIEW = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M16.749 2h4.554l.1.014l.099.028l.06.026c.08.034.153.085.219.15l.04.044l.044.057l.054.09l.039.09l.019.064l.014.064l.009.095v4.532a.75.75 0 0 1-1.493.102l-.007-.102V4.559l-6.44 6.44a.75.75 0 0 1-.976.073L13 11L9.97 8.09l-5.69 5.689a.75.75 0 0 1-1.133-.977l.073-.084l6.22-6.22a.75.75 0 0 1 .976-.072l.084.072l3.03 2.91L19.438 3.5h-2.69a.75.75 0 0 1-.742-.648l-.007-.102a.75.75 0 0 1 .648-.743L16.75 2zM3.75 17a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5a.75.75 0 0 1 .75-.75zm5.75-3.25a.75.75 0 0 0-1.5 0v7.5a.75.75 0 0 0 1.5 0v-7.5zM13.75 15a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0v-5.5a.75.75 0 0 1 .75-.75zm5.75-4.25a.75.75 0 0 0-1.5 0v10.5a.75.75 0 0 0 1.5 0v-10.5z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_CONVERSATION_REPORTS = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.587-1.112l-3.826 1.067a1.25 1.25 0 0 1-1.54-1.54l1.068-3.823A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2Zm0 1.5A8.5 8.5 0 0 0 3.5 12c0 1.47.373 2.883 1.073 4.137l.15.27-1.112 3.984 3.987-1.112.27.15A8.5 8.5 0 1 0 12 3.5ZM8.75 13h4.498a.75.75 0 0 1 .102 1.493l-.102.007H8.75a.75.75 0 0 1-.102-1.493L8.75 13h4.498H8.75Zm0-3.5h6.505a.75.75 0 0 1 .101 1.493l-.101.007H8.75a.75.75 0 0 1-.102-1.493L8.75 9.5h6.505H8.75Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_AGENT_REPORTS = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M4 13.999L13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14zM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78zM4 15.499l-.1.01a.51.51 0 0 0-.254.136a.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242c.826.445 2.003.714 3.266.753l.317.005l.317-.005c1.263-.039 2.439-.308 3.266-.753c.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001zM8.5 3a4.5 4.5 0 1 1 0 9a4.5 4.5 0 0 1 0-9zm9 2a3.5 3.5 0 1 1 0 7a3.5 3.5 0 0 1 0-7zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3s3-1.346 3-3s-1.346-3-3-3zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2s2-.897 2-2s-.897-2-2-2z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_LABEL_REPORTS = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M19.75 2A2.25 2.25 0 0 1 22 4.25v5.462a3.25 3.25 0 0 1-.952 2.298l-8.5 8.503a3.255 3.255 0 0 1-4.597.001L3.489 16.06a3.25 3.25 0 0 1-.003-4.596l8.5-8.51A3.25 3.25 0 0 1 14.284 2h5.465zm0 1.5h-5.465c-.465 0-.91.185-1.239.513l-8.512 8.523a1.75 1.75 0 0 0 .015 2.462l4.461 4.454a1.755 1.755 0 0 0 2.477 0l8.5-8.503a1.75 1.75 0 0 0 .513-1.237V4.25a.75.75 0 0 0-.75-.75zM17 5.502a1.5 1.5 0 1 1 0 3a1.5 1.5 0 0 1 0-3z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_INBOX_REPORTS = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M6.25 3h11.5a3.25 3.25 0 0 1 3.245 3.066L21 6.25v11.5a3.25 3.25 0 0 1-3.066 3.245L17.75 21H6.25a3.25 3.25 0 0 1-3.245-3.066L3 17.75V6.25a3.25 3.25 0 0 1 3.066-3.245L6.25 3h11.5h-11.5zM4.5 14.5v3.25a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V14.5h-3.825a3.752 3.752 0 0 1-3.475 2.995l-.2.005a3.752 3.752 0 0 1-3.632-2.812l-.043-.188H4.5v3.25v-3.25zm13.25-10H6.25a1.75 1.75 0 0 0-1.744 1.606L4.5 6.25V13H9a.75.75 0 0 1 .743.648l.007.102a2.25 2.25 0 0 0 4.495.154l.005-.154a.75.75 0 0 1 .648-.743L15 13h4.5V6.25a1.75 1.75 0 0 0-1.607-1.744L17.75 4.5z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_TEAM_REPORTS = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none"><path d="M14.75 15c.966 0 1.75.784 1.75 1.75l-.001.962c.117 2.19-1.511 3.297-4.432 3.297c-2.91 0-4.567-1.09-4.567-3.259v-1c0-.966.784-1.75 1.75-1.75h5.5zm0 1.5h-5.5a.25.25 0 0 0-.25.25v1c0 1.176.887 1.759 3.067 1.759c2.168 0 2.995-.564 2.933-1.757V16.75a.25.25 0 0 0-.25-.25zm-11-6.5h4.376a4.007 4.007 0 0 0-.095 1.5H3.75a.25.25 0 0 0-.25.25v1c0 1.176.887 1.759 3.067 1.759c.462 0 .863-.026 1.207-.077a2.743 2.743 0 0 0-1.173 1.576l-.034.001C3.657 16.009 2 14.919 2 12.75v-1c0-.966.784-1.75 1.75-1.75zm16.5 0c.966 0 1.75.784 1.75 1.75l-.001.962c.117 2.19-1.511 3.297-4.432 3.297l-.169-.002a2.755 2.755 0 0 0-1.218-1.606c.387.072.847.108 1.387.108c2.168 0 2.995-.564 2.933-1.757V11.75a.25.25 0 0 0-.25-.25h-4.28a4.05 4.05 0 0 0-.096-1.5h4.376zM12 8a3 3 0 1 1 0 6a3 3 0 0 1 0-6zm0 1.5a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3zM6.5 3a3 3 0 1 1 0 6a3 3 0 0 1 0-6zm11 0a3 3 0 1 1 0 6a3 3 0 0 1 0-6zm-11 1.5a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3zm11 0a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3z" fill="currentColor"></path></g></svg>`;
|
||||
export const ICON_ASSIGN_TEAM = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M17.5 12a5.5 5.5 0 1 1 0 11 5.5 5.5 0 0 1 0-11Zm0 2-.09.007a.5.5 0 0 0-.402.402L17 14.5V17L14.498 17l-.09.008a.5.5 0 0 0-.402.402l-.008.09.008.09a.5.5 0 0 0 .402.402l.09.008H17v2.503l.008.09a.5.5 0 0 0 .402.402l.09.008.09-.008a.5.5 0 0 0 .402-.402l.008-.09V18l2.504.001.09-.008a.5.5 0 0 0 .402-.402l.008-.09-.008-.09a.5.5 0 0 0-.403-.402l-.09-.008H18v-2.5l-.008-.09a.5.5 0 0 0-.402-.403L17.5 14Zm-3.246-4c.835 0 1.563.454 1.951 1.13a6.44 6.44 0 0 0-1.518.509.736.736 0 0 0-.433-.139H9.752a.75.75 0 0 0-.75.75v4.249c0 1.41.974 2.594 2.286 2.915a6.42 6.42 0 0 0 .735 1.587l-.02-.001a4.501 4.501 0 0 1-4.501-4.501V12.25A2.25 2.25 0 0 1 9.752 10h4.502Zm-6.848 0a3.243 3.243 0 0 0-.817 1.5H4.25a.75.75 0 0 0-.75.75v2.749a2.501 2.501 0 0 0 3.082 2.433c.085.504.24.985.453 1.432A4.001 4.001 0 0 1 2 14.999V12.25a2.25 2.25 0 0 1 2.096-2.245L4.25 10h3.156Zm12.344 0A2.25 2.25 0 0 1 22 12.25v.56A6.478 6.478 0 0 0 17.5 11l-.245.005A3.21 3.21 0 0 0 16.6 10h3.15ZM18.5 4a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5ZM12 3a3 3 0 1 1 0 6 3 3 0 0 1 0-6ZM5.5 4a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm13 1.5a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-6.5-1a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm-6.5 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_NOTIFICATION = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 1.996a7.49 7.49 0 0 1 7.496 7.25l.004.25v4.097l1.38 3.156a1.25 1.25 0 0 1-1.145 1.75L15 18.502a3 3 0 0 1-5.995.177L9 18.499H4.275a1.251 1.251 0 0 1-1.147-1.747L4.5 13.594V9.496c0-4.155 3.352-7.5 7.5-7.5ZM13.5 18.5l-3 .002a1.5 1.5 0 0 0 2.993.145l.006-.147ZM12 3.496c-3.32 0-6 2.674-6 6v4.41L4.656 17h14.697L18 13.907V9.509l-.004-.225A5.988 5.988 0 0 0 12 3.496Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_USER_PROFILE = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M10.125 13.995a2.737 2.737 0 0 0-.617 1.5h-5.26a.749.749 0 0 0-.748.75v.577c0 .536.191 1.054.539 1.461 1.177 1.379 2.984 2.12 5.469 2.205.049.57.273 1.09.617 1.508h-.129c-3.145 0-5.531-.905-7.098-2.739A3.75 3.75 0 0 1 2 16.822v-.578c0-1.19.925-2.164 2.095-2.243l.154-.006h5.876Zm4.621-2.5h3c.648 0 1.18.492 1.244 1.123l.007.127-.001 1.25h1.25c.967 0 1.75.784 1.75 1.75v4.5a1.75 1.75 0 0 1-1.75 1.75h-8a1.75 1.75 0 0 1-1.75-1.75v-4.5c0-.966.784-1.75 1.75-1.75h1.25v-1.25c0-.647.492-1.18 1.123-1.243l.127-.007h3-3Zm5.5 4h-8a.25.25 0 0 0-.25.25v4.5c0 .138.112.25.25.25h8a.25.25 0 0 0 .25-.25v-4.5a.25.25 0 0 0-.25-.25Zm-2.75-2.5h-2.5v1h2.5v-1ZM9.997 2a5 5 0 1 1 0 10 5 5 0 0 1 0-10Zm0 1.5a3.5 3.5 0 1 0 0 7 3.5 3.5 0 0 0 0-7Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_CANNED_RESPONSE = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M21 7.511a3.247 3.247 0 0 1 1.5 2.739v6c0 2.9-2.35 5.25-5.25 5.25h-9A3.247 3.247 0 0 1 5.511 20H17.25A3.75 3.75 0 0 0 21 16.25V7.511ZM5.25 4h11.5a3.25 3.25 0 0 1 3.245 3.066L20 7.25v8.5a3.25 3.25 0 0 1-3.066 3.245L16.75 19H5.25a3.25 3.25 0 0 1-3.245-3.066L2 15.75v-8.5a3.25 3.25 0 0 1 3.066-3.245L5.25 4ZM18.5 8.899l-7.15 3.765a.75.75 0 0 1-.603.042l-.096-.042L3.5 8.9v6.85a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V8.899ZM16.75 5.5H5.25a1.75 1.75 0 0 0-1.744 1.606l-.004.1L11 11.152l7.5-3.947A1.75 1.75 0 0 0 16.75 5.5Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_LABELS = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19.75 2A2.25 2.25 0 0 1 22 4.25v5.462a3.25 3.25 0 0 1-.952 2.298l-8.5 8.503a3.255 3.255 0 0 1-4.597.001L3.489 16.06a3.25 3.25 0 0 1-.003-4.596l8.5-8.51A3.25 3.25 0 0 1 14.284 2h5.465Zm0 1.5h-5.465c-.465 0-.91.185-1.239.513l-8.512 8.523a1.75 1.75 0 0 0 .015 2.462l4.461 4.454a1.755 1.755 0 0 0 2.477 0l8.5-8.503a1.75 1.75 0 0 0 .513-1.237V4.25a.75.75 0 0 0-.75-.75ZM17 5.502a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_ACCOUNT_SETTINGS = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.75 3h6.5a.75.75 0 0 1 .743.648L16 3.75V7h1.75A3.25 3.25 0 0 1 21 10.25v6.5A3.25 3.25 0 0 1 17.75 20H6.25A3.25 3.25 0 0 1 3 16.75v-6.5A3.25 3.25 0 0 1 6.25 7H8V3.75a.75.75 0 0 1 .648-.743L8.75 3h6.5-6.5Zm9 5.5H6.25a1.75 1.75 0 0 0-1.75 1.75v6.5c0 .966.784 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-6.5a1.75 1.75 0 0 0-1.75-1.75Zm-3.25-4h-5V7h5V4.5Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_INBOXES = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.25 3h11.5a3.25 3.25 0 0 1 3.245 3.066L21 6.25v11.5a3.25 3.25 0 0 1-3.066 3.245L17.75 21H6.25a3.25 3.25 0 0 1-3.245-3.066L3 17.75V6.25a3.25 3.25 0 0 1 3.066-3.245L6.25 3h11.5-11.5ZM4.5 14.5v3.25a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V14.5h-3.825a3.752 3.752 0 0 1-3.475 2.995l-.2.005a3.752 3.752 0 0 1-3.632-2.812l-.043-.188H4.5v3.25-3.25Zm13.25-10H6.25a1.75 1.75 0 0 0-1.744 1.606L4.5 6.25V13H9a.75.75 0 0 1 .743.648l.007.102a2.25 2.25 0 0 0 4.495.154l.005-.154a.75.75 0 0 1 .648-.743L15 13h4.5V6.25a1.75 1.75 0 0 0-1.607-1.744L17.75 4.5Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_APPS = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18.492 2.33 3.179 3.179a2.25 2.25 0 0 1 0 3.182l-2.584 2.584A2.25 2.25 0 0 1 21 13.5v5.25A2.25 2.25 0 0 1 18.75 21H5.25A2.25 2.25 0 0 1 3 18.75V5.25A2.25 2.25 0 0 1 5.25 3h5.25a2.25 2.25 0 0 1 2.225 1.915L15.31 2.33a2.25 2.25 0 0 1 3.182 0ZM4.5 18.75c0 .414.336.75.75.75l5.999-.001.001-6.75H4.5v6Zm8.249.749h6.001a.75.75 0 0 0 .75-.75V13.5a.75.75 0 0 0-.75-.75h-6.001v6.75Zm-2.249-15H5.25a.75.75 0 0 0-.75.75v6h6.75v-6a.75.75 0 0 0-.75-.75Zm2.25 4.81v1.94h1.94l-1.94-1.94Zm3.62-5.918-3.178 3.178a.75.75 0 0 0 0 1.061l3.179 3.179a.75.75 0 0 0 1.06 0l3.18-3.179a.75.75 0 0 0 0-1.06l-3.18-3.18a.75.75 0 0 0-1.06 0Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_ASSIGN_PRIORITY = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20"><path fill="currentColor" d="M9.562 3.262a.5.5 0 0 1 .879 0l6.5 12a.5.5 0 0 1-.44.739H3.5a.5.5 0 0 1-.44-.739l6.503-12Zm1.758-.477c-.567-1.047-2.07-1.047-2.638 0L2.18 14.786a1.5 1.5 0 0 0 1.32 2.215h13.002a1.5 1.5 0 0 0 1.319-2.215l-6.5-12ZM10.5 7.5a.5.5 0 1 0-1 0v4a.5.5 0 0 0 1 0v-4Zm.25 6.25a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0Z"/></svg>`;
|
||||
export const ICON_PRIORITY_URGENT = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" fill="#FFEBEE"/>
|
||||
<path d="M8 8.5C8 7.94772 8.44772 7.5 9 7.5C9.55228 7.5 10 7.94772 10 8.5V13C10 13.5523 9.55228 14 9 14C8.44772 14 8 13.5523 8 13V8.5Z" fill="#FF382D"/>
|
||||
<path d="M8 15.5C8 14.9477 8.44772 14.5 9 14.5C9.55228 14.5 10 14.9477 10 15.5C10 16.0523 9.55228 16.5 9 16.5C8.44772 16.5 8 16.0523 8 15.5Z" fill="#FF382D"/>
|
||||
<path d="M11 8.5C11 7.94772 11.4477 7.5 12 7.5C12.5523 7.5 13 7.94772 13 8.5V13C13 13.5523 12.5523 14 12 14C11.4477 14 11 13.5523 11 13V8.5Z" fill="#FF382D"/>
|
||||
<path d="M11 15.5C11 14.9477 11.4477 14.5 12 14.5C12.5523 14.5 13 14.9477 13 15.5C13 16.0523 12.5523 16.5 12 16.5C11.4477 16.5 11 16.0523 11 15.5Z" fill="#FF382D"/>
|
||||
<path d="M14 8.5C14 7.94772 14.4477 7.5 15 7.5C15.5523 7.5 16 7.94772 16 8.5V13C16 13.5523 15.5523 14 15 14C14.4477 14 14 13.5523 14 13V8.5Z" fill="#FF382D"/>
|
||||
<path d="M14 15.5C14 14.9477 14.4477 14.5 15 14.5C15.5523 14.5 16 14.9477 16 15.5C16 16.0523 15.5523 16.5 15 16.5C14.4477 16.5 14 16.0523 14 15.5Z" fill="#FF382D"/>
|
||||
</svg>
|
||||
`;
|
||||
export const ICON_PRIORITY_HIGH = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" fill="#F1F5F8"/>
|
||||
<path d="M9.7642 8L9.62358 14.1619H8.25142L8.11506 8H9.7642ZM8.9375 16.821C8.67898 16.821 8.45739 16.7301 8.27273 16.5483C8.09091 16.3665 8 16.1449 8 15.8835C8 15.6278 8.09091 15.4091 8.27273 15.2273C8.45739 15.0455 8.67898 14.9545 8.9375 14.9545C9.19034 14.9545 9.40909 15.0455 9.59375 15.2273C9.78125 15.4091 9.875 15.6278 9.875 15.8835C9.875 16.0568 9.83097 16.2145 9.7429 16.3565C9.65767 16.4986 9.54403 16.6122 9.40199 16.6974C9.26278 16.7798 9.10795 16.821 8.9375 16.821Z" fill="#446888"/>
|
||||
<path d="M13.1073 8L12.9667 14.1619H11.5945L11.4582 8H13.1073ZM12.2806 16.821C12.0221 16.821 11.8005 16.7301 11.6159 16.5483C11.434 16.3665 11.3431 16.1449 11.3431 15.8835C11.3431 15.6278 11.434 15.4091 11.6159 15.2273C11.8005 15.0455 12.0221 14.9545 12.2806 14.9545C12.5335 14.9545 12.7522 15.0455 12.9369 15.2273C13.1244 15.4091 13.2181 15.6278 13.2181 15.8835C13.2181 16.0568 13.1741 16.2145 13.086 16.3565C13.0008 16.4986 12.8872 16.6122 12.7451 16.6974C12.6059 16.7798 12.4511 16.821 12.2806 16.821Z" fill="#446888"/>
|
||||
<path d="M16.4505 8L16.3098 14.1619H14.9377L14.8013 8H16.4505ZM15.6237 16.821C15.3652 16.821 15.1436 16.7301 14.959 16.5483C14.7772 16.3665 14.6862 16.1449 14.6862 15.8835C14.6862 15.6278 14.7772 15.4091 14.959 15.2273C15.1436 15.0455 15.3652 14.9545 15.6237 14.9545C15.8766 14.9545 16.0953 15.0455 16.28 15.2273C16.4675 15.4091 16.5612 15.6278 16.5612 15.8835C16.5612 16.0568 16.5172 16.2145 16.4291 16.3565C16.3439 16.4986 16.2303 16.6122 16.0882 16.6974C15.949 16.7798 15.7942 16.821 15.6237 16.821Z" fill="#446888"/>
|
||||
</svg>`;
|
||||
|
||||
export const ICON_PRIORITY_MEDIUM = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" fill="#F1F5F8"/>
|
||||
<path d="M10.7642 8L10.6236 14.1619H9.25142L9.11506 8H10.7642ZM9.9375 16.821C9.67898 16.821 9.45739 16.7301 9.27273 16.5483C9.09091 16.3665 9 16.1449 9 15.8835C9 15.6278 9.09091 15.4091 9.27273 15.2273C9.45739 15.0455 9.67898 14.9545 9.9375 14.9545C10.1903 14.9545 10.4091 15.0455 10.5938 15.2273C10.7812 15.4091 10.875 15.6278 10.875 15.8835C10.875 16.0568 10.831 16.2145 10.7429 16.3565C10.6577 16.4986 10.544 16.6122 10.402 16.6974C10.2628 16.7798 10.108 16.821 9.9375 16.821Z" fill="#446888"/>
|
||||
<path d="M14.1073 8L13.9667 14.1619H12.5945L12.4582 8H14.1073ZM13.2806 16.821C13.0221 16.821 12.8005 16.7301 12.6159 16.5483C12.434 16.3665 12.3431 16.1449 12.3431 15.8835C12.3431 15.6278 12.434 15.4091 12.6159 15.2273C12.8005 15.0455 13.0221 14.9545 13.2806 14.9545C13.5335 14.9545 13.7522 15.0455 13.9369 15.2273C14.1244 15.4091 14.2181 15.6278 14.2181 15.8835C14.2181 16.0568 14.1741 16.2145 14.086 16.3565C14.0008 16.4986 13.8872 16.6122 13.7451 16.6974C13.6059 16.7798 13.4511 16.821 13.2806 16.821Z" fill="#446888"/>
|
||||
</svg>`;
|
||||
|
||||
export const ICON_PRIORITY_LOW = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" fill="#F1F5F8"/>
|
||||
<path d="M12.7642 8L12.6236 14.1619H11.2514L11.1151 8H12.7642ZM11.9375 16.821C11.679 16.821 11.4574 16.7301 11.2727 16.5483C11.0909 16.3665 11 16.1449 11 15.8835C11 15.6278 11.0909 15.4091 11.2727 15.2273C11.4574 15.0455 11.679 14.9545 11.9375 14.9545C12.1903 14.9545 12.4091 15.0455 12.5938 15.2273C12.7812 15.4091 12.875 15.6278 12.875 15.8835C12.875 16.0568 12.831 16.2145 12.7429 16.3565C12.6577 16.4986 12.544 16.6122 12.402 16.6974C12.2628 16.7798 12.108 16.821 11.9375 16.821Z" fill="#446888"/>
|
||||
</svg>`;
|
||||
|
||||
export const ICON_PRIORITY_NONE = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="24" height="24" fill="#F1F5F8"/>
|
||||
<path d="M13.5686 8L11.1579 16.9562H10L12.4107 8H13.5686Z" fill="#446888"/>
|
||||
</svg>`;
|
||||
|
||||
export const ICON_AI_ASSIST = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m13.314 7.565l-.136.126l-10.48 10.488a2.27 2.27 0 0 0 3.211 3.208L16.388 10.9a2.251 2.251 0 0 0-.001-3.182l-.157-.146a2.25 2.25 0 0 0-2.916-.007Zm-.848 2.961l1.088 1.088l-8.706 8.713a.77.77 0 1 1-1.089-1.088l8.707-8.713Zm4.386 4.48L16.75 15a.75.75 0 0 0-.743.648L16 15.75v.75h-.75a.75.75 0 0 0-.743.648l-.007.102c0 .38.282.694.648.743l.102.007H16v.75c0 .38.282.694.648.743l.102.007a.75.75 0 0 0 .743-.648l.007-.102V18h.75a.75.75 0 0 0 .743-.648L19 17.25a.75.75 0 0 0-.648-.743l-.102-.007h-.75v-.75a.75.75 0 0 0-.648-.743L16.75 15l.102.007Zm-1.553-6.254l.027.027a.751.751 0 0 1 0 1.061l-.711.713l-1.089-1.089l.73-.73a.75.75 0 0 1 1.043.018ZM6.852 5.007L6.75 5a.75.75 0 0 0-.743.648L6 5.75v.75h-.75a.75.75 0 0 0-.743.648L4.5 7.25c0 .38.282.693.648.743L5.25 8H6v.75c0 .38.282.693.648.743l.102.007a.75.75 0 0 0 .743-.648L7.5 8.75V8h.75a.75.75 0 0 0 .743-.648L9 7.25a.75.75 0 0 0-.648-.743L8.25 6.5H7.5v-.75a.75.75 0 0 0-.648-.743L6.75 5l.102.007Zm12-2L18.75 3a.75.75 0 0 0-.743.648L18 3.75v.75h-.75a.75.75 0 0 0-.743.648l-.007.102c0 .38.282.693.648.743L17.25 6H18v.75c0 .38.282.693.648.743l.102.007a.75.75 0 0 0 .743-.648l.007-.102V6h.75a.75.75 0 0 0 .743-.648L21 5.25a.75.75 0 0 0-.648-.743L20.25 4.5h-.75v-.75a.75.75 0 0 0-.648-.743L18.75 3l.102.007Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_AI_SUMMARY = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M4 4.5A2.5 2.5 0 0 1 6.5 2H18a2.5 2.5 0 0 1 2.5 2.5v14.25a.75.75 0 0 1-.75.75H5.5a1 1 0 0 0 1 1h13.25a.75.75 0 0 1 0 1.5H6.5A2.5 2.5 0 0 1 4 19.5v-15ZM5.5 18H19V4.5a1 1 0 0 0-1-1H6.5a1 1 0 0 0-1 1V18Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_AI_SPELLING = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M4.53 12.97a.75.75 0 0 0-1.06 1.06l4.5 4.5a.75.75 0 0 0 1.06 0l11-11a.75.75 0 0 0-1.06-1.06L8.5 16.94l-3.97-3.97Z" fill="currentColor"/></svg>`;
|
||||
|
||||
export const ICON_AI_EXPAND = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.75 19.5h14.5a.75.75 0 0 0 .102-1.493L21.25 18H6.75a.75.75 0 0 0-.102 1.493l.102.007Zm0-15h14.5a.75.75 0 0 0 .102-1.493L21.25 3H6.75a.75.75 0 0 0-.102 1.493l.102.007Zm7 3.5a.75.75 0 0 0 0 1.5h7.5a.75.75 0 0 0 0-1.5h-7.5ZM13 13.75a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1-.75-.75Zm-2-2.25a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0Zm-4-2a.5.5 0 0 0-1 0V11H4.5a.5.5 0 0 0 0 1H6v1.5a.5.5 0 0 0 1 0V12h1.5a.5.5 0 0 0 0-1H7V9.5Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_AI_SHORTEN = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M6.75 4.5h14.5a.75.75 0 0 0 .102-1.493L21.25 3H6.75a.75.75 0 0 0-.102 1.493l.102.007Zm0 15h14.5a.75.75 0 0 0 .102-1.493L21.25 18H6.75a.75.75 0 0 0-.102 1.493l.102.007Zm7-11.5a.75.75 0 0 0 0 1.5h7.5a.75.75 0 0 0 0-1.5h-7.5ZM13 13.75a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1-.75-.75Zm-2-2.25a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0Zm-2 0a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 0 0 1h4a.5.5 0 0 0 .5-.5Z" fill="currentColor"/></svg>`;
|
||||
export const ICON_AI_GRAMMAR = `<svg role="img" class="ninja-icon ninja-icon--fluent" width="18" height="18" fill="none" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M3 17h7.522l-2 2H3a1 1 0 0 1-.117-1.993L3 17Zm0-2h7.848a1.75 1.75 0 0 1-.775-2H3l-.117.007A1 1 0 0 0 3 15Zm0-8h18l.117-.007A1 1 0 0 0 21 5H3l-.117.007A1 1 0 0 0 3 7Zm9.72 9.216a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 1 1-1.06-1.06l4.5-4.5ZM3 9h10a1 1 0 0 1 .117 1.993L13 11H3a1 1 0 0 1-.117-1.993L3 9Zm13.5-1a.75.75 0 0 1 .744.658l.14 1.13a3.25 3.25 0 0 0 2.828 2.829l1.13.139a.75.75 0 0 1 0 1.488l-1.13.14a3.25 3.25 0 0 0-2.829 2.828l-.139 1.13a.75.75 0 0 1-1.488 0l-.14-1.13a3.25 3.25 0 0 0-2.828-2.829l-1.13-.139a.75.75 0 0 1 0-1.488l1.13-.14a3.25 3.25 0 0 0 2.829-2.828l.139-1.13A.75.75 0 0 1 16.5 8Z" fill="currentColor"/></svg>`;
|
||||
|
||||
export const ICON_APPEARANCE = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18"viewBox="0 0 24 24"><path fill="currentColor" d="M3.839 5.858c2.94-3.916 9.03-5.055 13.364-2.36c4.28 2.66 5.854 7.777 4.1 12.577c-1.655 4.533-6.016 6.328-9.159 4.048c-1.177-.854-1.634-1.925-1.854-3.664l-.106-.987l-.045-.398c-.123-.934-.311-1.352-.705-1.572c-.535-.298-.892-.305-1.595-.033l-.351.146l-.179.078c-1.014.44-1.688.595-2.541.416l-.2-.047l-.164-.047c-2.789-.864-3.202-4.647-.565-8.157Zm.984 6.716l.123.037l.134.03c.439.087.814.015 1.437-.242l.602-.257c1.202-.493 1.985-.54 3.046.05c.917.512 1.275 1.298 1.457 2.66l.053.459l.055.532l.047.422c.172 1.361.485 2.09 1.248 2.644c2.275 1.65 5.534.309 6.87-3.349c1.516-4.152.174-8.514-3.484-10.789c-3.675-2.284-8.899-1.306-11.373 1.987c-2.075 2.763-1.82 5.28-.215 5.816Zm11.225-1.994a1.25 1.25 0 1 1 2.414-.647a1.25 1.25 0 0 1-2.414.647Zm.494 3.488a1.25 1.25 0 1 1 2.415-.647a1.25 1.25 0 0 1-2.415.647ZM14.07 7.577a1.25 1.25 0 1 1 2.415-.647a1.25 1.25 0 0 1-2.415.647Zm-.028 8.998a1.25 1.25 0 1 1 2.414-.647a1.25 1.25 0 0 1-2.414.647Zm-3.497-9.97a1.25 1.25 0 1 1 2.415-.646a1.25 1.25 0 0 1-2.415.646Z"/></svg>`;
|
||||
export const ICON_LIGHT_MODE = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M12 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 2Zm5 10a5 5 0 1 1-10 0a5 5 0 0 1 10 0Zm4.25.75a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5h1.5ZM12 19a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 12 19Zm-7.75-6.25a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5h1.5Zm-.03-8.53a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 0 1-1.06 1.06l-1.5-1.5a.75.75 0 0 1 0-1.06Zm1.06 15.56a.75.75 0 1 1-1.06-1.06l1.5-1.5a.75.75 0 1 1 1.06 1.06l-1.5 1.5Zm14.5-15.56a.75.75 0 0 0-1.06 0l-1.5 1.5a.75.75 0 0 0 1.06 1.06l1.5-1.5a.75.75 0 0 0 0-1.06Zm-1.06 15.56a.75.75 0 1 0 1.06-1.06l-1.5-1.5a.75.75 0 1 0-1.06 1.06l1.5 1.5Z"/></svg>`;
|
||||
export const ICON_DARK_MODE = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M20.026 17.001c-2.762 4.784-8.879 6.423-13.663 3.661A9.965 9.965 0 0 1 3.13 17.68a.75.75 0 0 1 .365-1.132c3.767-1.348 5.785-2.91 6.956-5.146c1.232-2.353 1.551-4.93.689-8.463a.75.75 0 0 1 .769-.927a9.961 9.961 0 0 1 4.457 1.327c4.784 2.762 6.423 8.879 3.66 13.662Zm-8.248-4.903c-1.25 2.389-3.31 4.1-6.817 5.499a8.49 8.49 0 0 0 2.152 1.766a8.502 8.502 0 0 0 8.502-14.725a8.484 8.484 0 0 0-2.792-1.015c.647 3.384.23 6.043-1.045 8.475Z"/></svg>`;
|
||||
export const ICON_SYSTEM_MODE = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M4.25 3A2.25 2.25 0 0 0 2 5.25v10.5A2.25 2.25 0 0 0 4.25 18H9.5v1.25c0 .69-.56 1.25-1.25 1.25h-.5a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-.5c-.69 0-1.25-.56-1.25-1.25V18h5.25A2.25 2.25 0 0 0 22 15.75V5.25A2.25 2.25 0 0 0 19.75 3H4.25ZM13 18v1.25c0 .45.108.875.3 1.25h-2.6c.192-.375.3-.8.3-1.25V18h2ZM3.5 5.25a.75.75 0 0 1 .75-.75h15.5a.75.75 0 0 1 .75.75V13h-17V5.25Zm0 9.25h17v1.25a.75.75 0 0 1-.75.75H4.25a.75.75 0 0 1-.75-.75V14.5Z"/></svg>`;
|
||||
|
||||
export const ICON_SNOOZE_NOTIFICATION = `<svg role="img" class="ninja-icon ninja-icon--fluent" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M12 3.5c-3.104 0-6 2.432-6 6.25v4.153L4.682 17h14.67l-1.354-3.093V11.75a.75.75 0 0 1 1.5 0v1.843l1.381 3.156a1.25 1.25 0 0 1-1.145 1.751H15a3.002 3.002 0 0 1-6.003 0H4.305a1.25 1.25 0 0 1-1.15-1.739l1.344-3.164V9.75C4.5 5.068 8.103 2 12 2c.86 0 1.705.15 2.5.432a.75.75 0 0 1-.502 1.413A5.964 5.964 0 0 0 12 3.5ZM12 20c.828 0 1.5-.671 1.501-1.5h-3.003c0 .829.673 1.5 1.502 1.5Zm3.25-13h-2.5l-.101.007A.75.75 0 0 0 12.75 8.5h1.043l-1.653 2.314l-.055.09A.75.75 0 0 0 12.75 12h2.5l.102-.007a.75.75 0 0 0-.102-1.493h-1.042l1.653-2.314l.055-.09A.75.75 0 0 0 15.25 7Zm6-5h-3.5l-.101.007A.75.75 0 0 0 17.75 3.5h2.134l-2.766 4.347l-.05.09A.75.75 0 0 0 17.75 9h3.5l.102-.007A.75.75 0 0 0 21.25 7.5h-2.133l2.766-4.347l.05-.09A.75.75 0 0 0 21.25 2Z"/></svg>`;
|
||||
@@ -27,19 +27,20 @@ export const isJSONValid = value => {
|
||||
|
||||
export const getTypingUsersText = (users = []) => {
|
||||
const count = users.length;
|
||||
const [firstUser, secondUser] = users;
|
||||
|
||||
if (count === 1) {
|
||||
const [user] = users;
|
||||
return `${user.name} is typing`;
|
||||
return ['TYPING.ONE', { user: firstUser.name }];
|
||||
}
|
||||
|
||||
if (count === 2) {
|
||||
const [first, second] = users;
|
||||
return `${first.name} and ${second.name} are typing`;
|
||||
return [
|
||||
'TYPING.TWO',
|
||||
{ user: firstUser.name, secondUser: secondUser.name },
|
||||
];
|
||||
}
|
||||
|
||||
const [user] = users;
|
||||
const rest = users.length - 1;
|
||||
return `${user.name} and ${rest} others are typing`;
|
||||
return ['TYPING.MULTIPLE', { user: firstUser.name, count: count - 1 }];
|
||||
};
|
||||
|
||||
export const createPendingMessage = data => {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
|
||||
const RESIZE_OBSERVER_DEBOUNCE_TIME = 100;
|
||||
|
||||
function createResizeObserver(el, binding) {
|
||||
const { value } = binding;
|
||||
const observer = new ResizeObserver(
|
||||
debounce(entries => {
|
||||
const entry = entries[0];
|
||||
if (entry && value && typeof value === 'function') {
|
||||
value(entry);
|
||||
}
|
||||
}, RESIZE_OBSERVER_DEBOUNCE_TIME)
|
||||
);
|
||||
|
||||
el.cwResizeObserver = observer;
|
||||
observer.observe(el);
|
||||
}
|
||||
|
||||
function destroyResizeObserver(el) {
|
||||
if (el.cwResizeObserver) {
|
||||
el.cwResizeObserver.unobserve(el);
|
||||
el.cwResizeObserver.disconnect();
|
||||
delete el.cwResizeObserver;
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
bind(el, binding) {
|
||||
createResizeObserver(el, binding);
|
||||
},
|
||||
update(el, binding) {
|
||||
if (binding.oldValue !== binding.value) {
|
||||
destroyResizeObserver(el);
|
||||
createResizeObserver(el, binding);
|
||||
}
|
||||
},
|
||||
unbind(el) {
|
||||
destroyResizeObserver(el);
|
||||
},
|
||||
};
|
||||
@@ -3,7 +3,8 @@ import {
|
||||
MessageMarkdownTransformer,
|
||||
MessageMarkdownSerializer,
|
||||
} from '@chatwoot/prosemirror-schema';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
||||
import * as Sentry from '@sentry/vue';
|
||||
|
||||
/**
|
||||
* The delimiter used to separate the signature from the rest of the body.
|
||||
@@ -281,3 +282,92 @@ export function setURLWithQueryAndSize(selectedImageNode, size, editorView) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Content Node Creation Helper Functions for
|
||||
* - mention
|
||||
* - canned response
|
||||
* - variable
|
||||
* - emoji
|
||||
*/
|
||||
|
||||
/**
|
||||
* Centralized node creation function that handles the creation of different types of nodes based on the specified type.
|
||||
* @param {Object} editorView - The editor view instance.
|
||||
* @param {string} nodeType - The type of node to create ('mention', 'cannedResponse', 'variable', 'emoji').
|
||||
* @param {Object|string} content - The content needed to create the node, which varies based on node type.
|
||||
* @returns {Object|null} - The created ProseMirror node or null if the type is not supported.
|
||||
*/
|
||||
const createNode = (editorView, nodeType, content) => {
|
||||
const { state } = editorView;
|
||||
switch (nodeType) {
|
||||
case 'mention':
|
||||
return state.schema.nodes.mention.create({
|
||||
userId: content.id,
|
||||
userFullName: content.name,
|
||||
});
|
||||
case 'cannedResponse':
|
||||
return new MessageMarkdownTransformer(messageSchema).parse(content);
|
||||
case 'variable':
|
||||
return state.schema.text(`{{${content}}}`);
|
||||
case 'emoji':
|
||||
return state.schema.text(content);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Object mapping types to their respective node creation functions.
|
||||
*/
|
||||
const nodeCreators = {
|
||||
mention: (editorView, content, from, to) => ({
|
||||
node: createNode(editorView, 'mention', content),
|
||||
from,
|
||||
to,
|
||||
}),
|
||||
cannedResponse: (editorView, content, from, to, variables) => {
|
||||
const updatedMessage = replaceVariablesInMessage({
|
||||
message: content,
|
||||
variables,
|
||||
});
|
||||
const node = createNode(editorView, 'cannedResponse', updatedMessage);
|
||||
return {
|
||||
node,
|
||||
from: node.textContent === updatedMessage ? from : from - 1,
|
||||
to,
|
||||
};
|
||||
},
|
||||
variable: (editorView, content, from, to) => ({
|
||||
node: createNode(editorView, 'variable', content),
|
||||
from,
|
||||
to,
|
||||
}),
|
||||
emoji: (editorView, content, from, to) => ({
|
||||
node: createNode(editorView, 'emoji', content),
|
||||
from,
|
||||
to,
|
||||
}),
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves a content node based on the specified type and content, using a functional approach to select the appropriate node creation function.
|
||||
* @param {Object} editorView - The editor view instance.
|
||||
* @param {string} type - The type of content node to create ('mention', 'cannedResponse', 'variable', 'emoji').
|
||||
* @param {string|Object} content - The content to be transformed into a node.
|
||||
* @param {Object} range - An object containing 'from' and 'to' properties indicating the range in the document where the node should be placed.
|
||||
* @param {Object} variables - Optional. Variables to replace in the content, used for 'cannedResponse' type.
|
||||
* @returns {Object} - An object containing the created node and the updated 'from' and 'to' positions.
|
||||
*/
|
||||
export const getContentNode = (
|
||||
editorView,
|
||||
type,
|
||||
content,
|
||||
{ from, to },
|
||||
variables
|
||||
) => {
|
||||
const creator = nodeCreators[type];
|
||||
return creator
|
||||
? creator(editorView, content, from, to, variables)
|
||||
: { node: null, from, to };
|
||||
};
|
||||
|
||||
@@ -9,13 +9,16 @@ const FEATURE_HELP_URLS = {
|
||||
custom_attributes: 'https://chwt.app/hc/custom-attributes',
|
||||
dashboard_apps: 'https://chwt.app/hc/dashboard-apps',
|
||||
help_center: 'https://chwt.app/hc/help-center',
|
||||
inboxes: 'https://chwt.app/hc/inboxes',
|
||||
integrations: 'https://chwt.app/hc/integrations',
|
||||
labels: 'https://chwt.app/hc/labels',
|
||||
macros: 'https://chwt.app/hc/macros',
|
||||
message_reply_to: 'https://chwt.app/hc/reply-to',
|
||||
reports: 'https://chwt.app/hc/reports',
|
||||
sla: 'https://chwt.app/hc/sla',
|
||||
captain: 'https://chwt.app/hc/captain',
|
||||
team_management: 'https://chwt.app/hc/teams',
|
||||
webhook: 'https://chwt.app/hc/webhooks',
|
||||
};
|
||||
|
||||
export function getHelpUrlForFeature(featureName) {
|
||||
|
||||
@@ -1,4 +1,41 @@
|
||||
import { INBOX_TYPES } from 'shared/mixins/inboxMixin';
|
||||
export const INBOX_TYPES = {
|
||||
WEB: 'Channel::WebWidget',
|
||||
FB: 'Channel::FacebookPage',
|
||||
TWITTER: 'Channel::TwitterProfile',
|
||||
TWILIO: 'Channel::TwilioSms',
|
||||
WHATSAPP: 'Channel::Whatsapp',
|
||||
API: 'Channel::Api',
|
||||
EMAIL: 'Channel::Email',
|
||||
TELEGRAM: 'Channel::Telegram',
|
||||
LINE: 'Channel::Line',
|
||||
SMS: 'Channel::Sms',
|
||||
};
|
||||
|
||||
const INBOX_ICON_MAP_FILL = {
|
||||
[INBOX_TYPES.WEB]: 'i-ri-global-fill',
|
||||
[INBOX_TYPES.FB]: 'i-ri-messenger-fill',
|
||||
[INBOX_TYPES.TWITTER]: 'i-ri-twitter-x-fill',
|
||||
[INBOX_TYPES.WHATSAPP]: 'i-ri-whatsapp-fill',
|
||||
[INBOX_TYPES.API]: 'i-ri-cloudy-fill',
|
||||
[INBOX_TYPES.EMAIL]: 'i-ri-mail-fill',
|
||||
[INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-fill',
|
||||
[INBOX_TYPES.LINE]: 'i-ri-line-fill',
|
||||
};
|
||||
|
||||
const DEFAULT_ICON_FILL = 'i-ri-chat-1-fill';
|
||||
|
||||
const INBOX_ICON_MAP_LINE = {
|
||||
[INBOX_TYPES.WEB]: 'i-ri-global-line',
|
||||
[INBOX_TYPES.FB]: 'i-ri-messenger-line',
|
||||
[INBOX_TYPES.TWITTER]: 'i-ri-twitter-x-line',
|
||||
[INBOX_TYPES.WHATSAPP]: 'i-ri-whatsapp-line',
|
||||
[INBOX_TYPES.API]: 'i-ri-cloudy-line',
|
||||
[INBOX_TYPES.EMAIL]: 'i-ri-mail-line',
|
||||
[INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-line',
|
||||
[INBOX_TYPES.LINE]: 'i-ri-line-line',
|
||||
};
|
||||
|
||||
const DEFAULT_ICON_LINE = 'i-ri-chat-1-line';
|
||||
|
||||
export const getInboxSource = (type, phoneNumber, inbox) => {
|
||||
switch (type) {
|
||||
@@ -86,6 +123,20 @@ export const getInboxClassByType = (type, phoneNumber) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getInboxIconByType = (type, phoneNumber, variant = 'fill') => {
|
||||
const iconMap =
|
||||
variant === 'fill' ? INBOX_ICON_MAP_FILL : INBOX_ICON_MAP_LINE;
|
||||
const defaultIcon =
|
||||
variant === 'fill' ? DEFAULT_ICON_FILL : DEFAULT_ICON_LINE;
|
||||
|
||||
// Special case for Twilio (whatsapp and sms)
|
||||
if (type === INBOX_TYPES.TWILIO && phoneNumber?.startsWith('whatsapp')) {
|
||||
return iconMap[INBOX_TYPES.WHATSAPP];
|
||||
}
|
||||
|
||||
return iconMap[type] ?? defaultIcon;
|
||||
};
|
||||
|
||||
export const getInboxWarningIconClass = (type, reauthorizationRequired) => {
|
||||
const allowedInboxTypes = [INBOX_TYPES.FB, INBOX_TYPES.EMAIL];
|
||||
if (allowedInboxTypes.includes(type) && reauthorizationRequired) {
|
||||
|
||||
@@ -7,6 +7,15 @@ export const hasPermissions = (
|
||||
);
|
||||
};
|
||||
|
||||
export const getCurrentAccount = ({ accounts } = {}, accountId = null) => {
|
||||
return accounts.find(account => Number(account.id) === Number(accountId));
|
||||
};
|
||||
|
||||
export const getUserPermissions = (user, accountId) => {
|
||||
const currentAccount = getCurrentAccount(user, accountId) || {};
|
||||
return currentAccount.permissions || [];
|
||||
};
|
||||
|
||||
const isPermissionsPresentInRoute = route =>
|
||||
route.meta && route.meta.permissions;
|
||||
|
||||
@@ -32,3 +41,32 @@ export const buildPermissionsFromRouter = (routes = []) =>
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* Filters and transforms items based on user permissions.
|
||||
*
|
||||
* @param {Object} items - An object containing items to be filtered.
|
||||
* @param {Array} userPermissions - Array of permissions the user has.
|
||||
* @param {Function} getPermissions - Function to extract required permissions from an item.
|
||||
* @param {Function} [transformItem] - Optional function to transform each item after filtering.
|
||||
* @returns {Array} Filtered and transformed items.
|
||||
*/
|
||||
export const filterItemsByPermission = (
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions,
|
||||
transformItem = (key, item) => ({ key, ...item })
|
||||
) => {
|
||||
// Helper function to check if an item has the required permissions
|
||||
const hasRequiredPermissions = item => {
|
||||
const requiredPermissions = getPermissions(item);
|
||||
return (
|
||||
requiredPermissions.length === 0 ||
|
||||
hasPermissions(requiredPermissions, userPermissions)
|
||||
);
|
||||
};
|
||||
|
||||
return Object.entries(items)
|
||||
.filter(([, item]) => hasRequiredPermissions(item)) // Keep only items with required permissions
|
||||
.map(([key, item]) => transformItem(key, item)); // Transform each remaining item
|
||||
};
|
||||
|
||||
@@ -13,3 +13,140 @@ export const buildPortalArticleURL = (
|
||||
const portalURL = buildPortalURL(portalSlug);
|
||||
return `${portalURL}/articles/${articleSlug}`;
|
||||
};
|
||||
|
||||
export const getArticleStatus = status => {
|
||||
switch (status) {
|
||||
case 'draft':
|
||||
return 0;
|
||||
case 'published':
|
||||
return 1;
|
||||
case 'archived':
|
||||
return 2;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// Constants
|
||||
export const HELP_CENTER_MENU_ITEMS = [
|
||||
{
|
||||
label: 'Articles',
|
||||
icon: 'i-lucide-book',
|
||||
action: 'portals_articles_index',
|
||||
value: [
|
||||
'portals_articles_index',
|
||||
'portals_articles_new',
|
||||
'portals_articles_edit',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Categories',
|
||||
icon: 'i-lucide-folder',
|
||||
action: 'portals_categories_index',
|
||||
value: [
|
||||
'portals_categories_index',
|
||||
'portals_categories_articles_index',
|
||||
'portals_categories_articles_edit',
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Locales',
|
||||
icon: 'i-lucide-languages',
|
||||
action: 'portals_locales_index',
|
||||
value: ['portals_locales_index'],
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
icon: 'i-lucide-settings',
|
||||
action: 'portals_settings_index',
|
||||
value: ['portals_settings_index'],
|
||||
},
|
||||
];
|
||||
|
||||
export const ARTICLE_STATUSES = {
|
||||
DRAFT: 'draft',
|
||||
PUBLISHED: 'published',
|
||||
ARCHIVED: 'archived',
|
||||
};
|
||||
|
||||
export const ARTICLE_MENU_ITEMS = {
|
||||
publish: {
|
||||
label: 'HELP_CENTER.ARTICLES_PAGE.ARTICLE_CARD.CARD.DROPDOWN_MENU.PUBLISH',
|
||||
value: ARTICLE_STATUSES.PUBLISHED,
|
||||
action: 'publish',
|
||||
icon: 'i-lucide-check',
|
||||
},
|
||||
draft: {
|
||||
label: 'HELP_CENTER.ARTICLES_PAGE.ARTICLE_CARD.CARD.DROPDOWN_MENU.DRAFT',
|
||||
value: ARTICLE_STATUSES.DRAFT,
|
||||
action: 'draft',
|
||||
icon: 'i-lucide-pencil-line',
|
||||
},
|
||||
archive: {
|
||||
label: 'HELP_CENTER.ARTICLES_PAGE.ARTICLE_CARD.CARD.DROPDOWN_MENU.ARCHIVE',
|
||||
value: ARTICLE_STATUSES.ARCHIVED,
|
||||
action: 'archive',
|
||||
icon: 'i-lucide-archive-restore',
|
||||
},
|
||||
delete: {
|
||||
label: 'HELP_CENTER.ARTICLES_PAGE.ARTICLE_CARD.CARD.DROPDOWN_MENU.DELETE',
|
||||
value: 'delete',
|
||||
action: 'delete',
|
||||
icon: 'i-lucide-trash',
|
||||
},
|
||||
};
|
||||
|
||||
export const ARTICLE_MENU_OPTIONS = {
|
||||
[ARTICLE_STATUSES.ARCHIVED]: ['publish', 'draft'],
|
||||
[ARTICLE_STATUSES.DRAFT]: ['publish', 'archive'],
|
||||
[ARTICLE_STATUSES.PUBLISHED]: ['draft', 'archive'],
|
||||
};
|
||||
|
||||
export const ARTICLE_TABS = {
|
||||
ALL: 'all',
|
||||
MINE: 'mine',
|
||||
DRAFT: 'draft',
|
||||
ARCHIVED: 'archived',
|
||||
};
|
||||
|
||||
export const CATEGORY_ALL = 'all';
|
||||
|
||||
export const ARTICLE_TABS_OPTIONS = [
|
||||
{
|
||||
key: 'ALL',
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
key: 'MINE',
|
||||
value: 'mine',
|
||||
},
|
||||
{
|
||||
key: 'DRAFT',
|
||||
value: 'draft',
|
||||
},
|
||||
{
|
||||
key: 'ARCHIVED',
|
||||
value: 'archived',
|
||||
},
|
||||
];
|
||||
|
||||
export const LOCALE_MENU_ITEMS = [
|
||||
{
|
||||
label: 'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.MAKE_DEFAULT',
|
||||
action: 'change-default',
|
||||
value: 'default',
|
||||
icon: 'i-lucide-star',
|
||||
},
|
||||
{
|
||||
label: 'HELP_CENTER.LOCALES_PAGE.LOCALE_CARD.DROPDOWN_MENU.DELETE',
|
||||
action: 'delete',
|
||||
value: 'delete',
|
||||
icon: 'i-lucide-trash',
|
||||
},
|
||||
];
|
||||
|
||||
export const ARTICLE_EDITOR_STATUS_OPTIONS = {
|
||||
published: ['archive', 'draft'],
|
||||
archived: ['draft'],
|
||||
draft: ['archive'],
|
||||
};
|
||||
|
||||
@@ -1,15 +1,42 @@
|
||||
import { hasPermissions } from './permissionsHelper';
|
||||
import {
|
||||
hasPermissions,
|
||||
getUserPermissions,
|
||||
getCurrentAccount,
|
||||
} from './permissionsHelper';
|
||||
|
||||
// eslint-disable-next-line default-param-last
|
||||
export const getCurrentAccount = ({ accounts } = {}, accountId) => {
|
||||
return accounts.find(account => account.id === accountId);
|
||||
};
|
||||
import {
|
||||
ROLES,
|
||||
CONVERSATION_PERMISSIONS,
|
||||
CONTACT_PERMISSIONS,
|
||||
REPORTS_PERMISSIONS,
|
||||
PORTAL_PERMISSIONS,
|
||||
} from 'dashboard/constants/permissions.js';
|
||||
|
||||
export const routeIsAccessibleFor = (route, userPermissions = []) => {
|
||||
const { meta: { permissions: routePermissions = [] } = {} } = route;
|
||||
return hasPermissions(routePermissions, userPermissions);
|
||||
};
|
||||
|
||||
export const defaultRedirectPage = (to, permissions) => {
|
||||
const { accountId } = to.params;
|
||||
|
||||
const permissionRoutes = [
|
||||
{
|
||||
permissions: [...ROLES, ...CONVERSATION_PERMISSIONS],
|
||||
path: 'dashboard',
|
||||
},
|
||||
{ permissions: [CONTACT_PERMISSIONS], path: 'contacts' },
|
||||
{ permissions: [REPORTS_PERMISSIONS], path: 'reports/overview' },
|
||||
{ permissions: [PORTAL_PERMISSIONS], path: 'portals' },
|
||||
];
|
||||
|
||||
const route = permissionRoutes.find(({ permissions: routePermissions }) =>
|
||||
hasPermissions(routePermissions, permissions)
|
||||
);
|
||||
|
||||
return `accounts/${accountId}/${route ? route.path : 'dashboard'}`;
|
||||
};
|
||||
|
||||
const validateActiveAccountRoutes = (to, user) => {
|
||||
// If the current account is active, then check for the route permissions
|
||||
const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`;
|
||||
@@ -19,9 +46,11 @@ const validateActiveAccountRoutes = (to, user) => {
|
||||
return accountDashboardURL;
|
||||
}
|
||||
|
||||
const isAccessible = routeIsAccessibleFor(to, user.permissions);
|
||||
const userPermissions = getUserPermissions(user, to.params.accountId);
|
||||
|
||||
const isAccessible = routeIsAccessibleFor(to, userPermissions);
|
||||
// If the route is not accessible for the user, return to dashboard screen
|
||||
return isAccessible ? null : accountDashboardURL;
|
||||
return isAccessible ? null : defaultRedirectPage(to, userPermissions);
|
||||
};
|
||||
|
||||
export const validateLoggedInRoutes = (to, user) => {
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import {
|
||||
ANALYTICS_IDENTITY,
|
||||
CHATWOOT_RESET,
|
||||
CHATWOOT_SET_USER,
|
||||
} from '../constants/appEvents';
|
||||
import AnalyticsHelper from './AnalyticsHelper';
|
||||
import DashboardAudioNotificationHelper from './AudioAlerts/DashboardAudioNotificationHelper';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
|
||||
export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER';
|
||||
export const CHATWOOT_RESET = 'CHATWOOT_RESET';
|
||||
|
||||
export const ANALYTICS_IDENTITY = 'ANALYTICS_IDENTITY';
|
||||
export const ANALYTICS_RESET = 'ANALYTICS_RESET';
|
||||
|
||||
export const initializeAnalyticsEvents = () => {
|
||||
emitter.on(ANALYTICS_IDENTITY, ({ user }) => {
|
||||
AnalyticsHelper.identify(user);
|
||||
});
|
||||
};
|
||||
|
||||
const initializeAudioAlerts = user => {
|
||||
export const initializeAudioAlerts = user => {
|
||||
const { ui_settings: uiSettings } = user || {};
|
||||
const {
|
||||
always_play_audio_alert: alwaysPlayAudioAlert,
|
||||
@@ -25,8 +24,8 @@ const initializeAudioAlerts = user => {
|
||||
// entire payload for the user during the signup process.
|
||||
} = uiSettings || {};
|
||||
|
||||
DashboardAudioNotificationHelper.setInstanceValues({
|
||||
currentUserId: user.id,
|
||||
DashboardAudioNotificationHelper.set({
|
||||
currentUser: user,
|
||||
audioAlertType: audioAlertType || 'none',
|
||||
audioAlertTone: audioAlertTone || 'ding',
|
||||
alwaysPlayAudioAlert: alwaysPlayAudioAlert || false,
|
||||
|
||||
@@ -37,8 +37,10 @@ const storeMock = {
|
||||
|
||||
const routerMock = {
|
||||
currentRoute: {
|
||||
name: '',
|
||||
params: { conversation_id: null },
|
||||
value: {
|
||||
name: '',
|
||||
params: { conversation_id: null },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -222,7 +224,7 @@ describe('ReconnectService', () => {
|
||||
|
||||
describe('fetchConversationMessagesOnReconnect', () => {
|
||||
it('should dispatch syncActiveConversationMessages if conversationId exists', async () => {
|
||||
routerMock.currentRoute.params.conversation_id = 1;
|
||||
routerMock.currentRoute.value.params.conversation_id = 1;
|
||||
await reconnectService.fetchConversationMessagesOnReconnect();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(
|
||||
'syncActiveConversationMessages',
|
||||
@@ -231,7 +233,7 @@ describe('ReconnectService', () => {
|
||||
});
|
||||
|
||||
it('should not dispatch syncActiveConversationMessages if conversationId does not exist', async () => {
|
||||
routerMock.currentRoute.params.conversation_id = null;
|
||||
routerMock.currentRoute.value.params.conversation_id = null;
|
||||
await reconnectService.fetchConversationMessagesOnReconnect();
|
||||
expect(storeMock.dispatch).not.toHaveBeenCalledWith(
|
||||
'syncActiveConversationMessages',
|
||||
@@ -305,7 +307,7 @@ describe('ReconnectService', () => {
|
||||
|
||||
describe('setConversationLastMessageId', () => {
|
||||
it('should dispatch setConversationLastMessageId if conversationId exists', async () => {
|
||||
routerMock.currentRoute.params.conversation_id = 1;
|
||||
routerMock.currentRoute.value.params.conversation_id = 1;
|
||||
await reconnectService.setConversationLastMessageId();
|
||||
expect(storeMock.dispatch).toHaveBeenCalledWith(
|
||||
'setConversationLastMessageId',
|
||||
@@ -314,7 +316,7 @@ describe('ReconnectService', () => {
|
||||
});
|
||||
|
||||
it('should not dispatch setConversationLastMessageId if conversationId does not exist', async () => {
|
||||
routerMock.currentRoute.params.conversation_id = null;
|
||||
routerMock.currentRoute.value.params.conversation_id = null;
|
||||
await reconnectService.setConversationLastMessageId();
|
||||
expect(storeMock.dispatch).not.toHaveBeenCalledWith(
|
||||
'setConversationLastMessageId',
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
conversationListPageURL,
|
||||
getArticleSearchURL,
|
||||
hasValidAvatarUrl,
|
||||
timeStampAppendedURL,
|
||||
getHostNameFromURL,
|
||||
} from '../URLHelper';
|
||||
|
||||
describe('#URL Helpers', () => {
|
||||
@@ -190,4 +192,75 @@ describe('#URL Helpers', () => {
|
||||
expect(hasValidAvatarUrl()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeStampAppendedURL', () => {
|
||||
const FIXED_TIMESTAMP = 1234567890000;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(Date, 'now').mockImplementation(() => FIXED_TIMESTAMP);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should append timestamp to a URL without query parameters', () => {
|
||||
const input = 'https://example.com/audio.mp3';
|
||||
const expected = `https://example.com/audio.mp3?t=${FIXED_TIMESTAMP}`;
|
||||
expect(timeStampAppendedURL(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should append timestamp to a URL with existing query parameters', () => {
|
||||
const input = 'https://example.com/audio.mp3?volume=50';
|
||||
const expected = `https://example.com/audio.mp3?volume=50&t=${FIXED_TIMESTAMP}`;
|
||||
expect(timeStampAppendedURL(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should not append timestamp if it already exists', () => {
|
||||
const input = 'https://example.com/audio.mp3?t=9876543210';
|
||||
expect(timeStampAppendedURL(input)).toBe(input);
|
||||
});
|
||||
|
||||
it('should handle URLs with hash fragments', () => {
|
||||
const input = 'https://example.com/audio.mp3#section1';
|
||||
const expected = `https://example.com/audio.mp3?t=${FIXED_TIMESTAMP}#section1`;
|
||||
expect(timeStampAppendedURL(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should handle complex URLs', () => {
|
||||
const input =
|
||||
'https://example.com/path/to/audio.mp3?key1=value1&key2=value2#fragment';
|
||||
const expected = `https://example.com/path/to/audio.mp3?key1=value1&key2=value2&t=${FIXED_TIMESTAMP}#fragment`;
|
||||
expect(timeStampAppendedURL(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it('should throw an error for invalid URLs', () => {
|
||||
const input = 'not a valid url';
|
||||
expect(() => timeStampAppendedURL(input)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getHostNameFromURL', () => {
|
||||
it('should return the hostname from a valid URL', () => {
|
||||
expect(getHostNameFromURL('https://example.com/path')).toBe(
|
||||
'example.com'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null for an invalid URL', () => {
|
||||
expect(getHostNameFromURL('not a valid url')).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null for an empty string', () => {
|
||||
expect(getHostNameFromURL('')).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null for undefined input', () => {
|
||||
expect(getHostNameFromURL(undefined)).toBe(null);
|
||||
});
|
||||
|
||||
it('should correctly handle URLs with non-standard TLDs', () => {
|
||||
expect(getHostNameFromURL('https://chatwoot.help')).toBe('chatwoot.help');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
getAgentsByAvailability,
|
||||
getSortedAgentsByAvailability,
|
||||
getAgentsByUpdatedPresence,
|
||||
getCombinedAgents,
|
||||
createNoneAgent,
|
||||
} from '../agentHelper';
|
||||
import {
|
||||
allAgentsData,
|
||||
onlineAgentsData,
|
||||
busyAgentsData,
|
||||
offlineAgentsData,
|
||||
sortedByAvailability,
|
||||
formattedAgentsByPresenceOnline,
|
||||
formattedAgentsByPresenceOffline,
|
||||
} from 'dashboard/helper/specs/fixtures/agentFixtures';
|
||||
|
||||
describe('agentHelper', () => {
|
||||
describe('getAgentsByAvailability', () => {
|
||||
it('returns agents by availability', () => {
|
||||
expect(getAgentsByAvailability(allAgentsData, 'online')).toEqual(
|
||||
onlineAgentsData
|
||||
);
|
||||
expect(getAgentsByAvailability(allAgentsData, 'busy')).toEqual(
|
||||
busyAgentsData
|
||||
);
|
||||
expect(getAgentsByAvailability(allAgentsData, 'offline')).toEqual(
|
||||
offlineAgentsData
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSortedAgentsByAvailability', () => {
|
||||
it('returns sorted agents by availability', () => {
|
||||
expect(getSortedAgentsByAvailability(allAgentsData)).toEqual(
|
||||
sortedByAvailability
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty array when given an empty input', () => {
|
||||
expect(getSortedAgentsByAvailability([])).toEqual([]);
|
||||
});
|
||||
|
||||
it('maintains the order of agents with the same availability status', () => {
|
||||
const result = getSortedAgentsByAvailability(allAgentsData);
|
||||
expect(result[2].name).toBe('Honey Bee');
|
||||
expect(result[3].name).toBe('Samuel Keta');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgentsByUpdatedPresence', () => {
|
||||
it('returns agents with updated presence', () => {
|
||||
const currentUser = {
|
||||
id: 1,
|
||||
accounts: [{ id: 1, availability_status: 'offline' }],
|
||||
};
|
||||
const currentAccountId = 1;
|
||||
|
||||
expect(
|
||||
getAgentsByUpdatedPresence(
|
||||
formattedAgentsByPresenceOnline,
|
||||
currentUser,
|
||||
currentAccountId
|
||||
)
|
||||
).toEqual(formattedAgentsByPresenceOffline);
|
||||
});
|
||||
|
||||
it('does not modify other agents presence', () => {
|
||||
const currentUser = {
|
||||
id: 2,
|
||||
accounts: [{ id: 1, availability_status: 'offline' }],
|
||||
};
|
||||
const currentAccountId = 1;
|
||||
|
||||
expect(
|
||||
getAgentsByUpdatedPresence(
|
||||
formattedAgentsByPresenceOnline,
|
||||
currentUser,
|
||||
currentAccountId
|
||||
)
|
||||
).toEqual(formattedAgentsByPresenceOnline);
|
||||
});
|
||||
|
||||
it('handles empty agent list', () => {
|
||||
const currentUser = {
|
||||
id: 1,
|
||||
accounts: [{ id: 1, availability_status: 'offline' }],
|
||||
};
|
||||
const currentAccountId = 1;
|
||||
|
||||
expect(
|
||||
getAgentsByUpdatedPresence([], currentUser, currentAccountId)
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCombinedAgents', () => {
|
||||
it('includes None agent when includeNoneAgent is true and isAgentSelected is true', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, true, true);
|
||||
expect(result).toEqual([createNoneAgent, ...sortedByAvailability]);
|
||||
expect(result.length).toBe(sortedByAvailability.length + 1);
|
||||
expect(result[0]).toEqual(createNoneAgent);
|
||||
});
|
||||
|
||||
it('excludes None agent when includeNoneAgent is false', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, false, true);
|
||||
expect(result).toEqual(sortedByAvailability);
|
||||
expect(result.length).toBe(sortedByAvailability.length);
|
||||
expect(result[0]).not.toEqual(createNoneAgent);
|
||||
});
|
||||
|
||||
it('excludes None agent when isAgentSelected is false', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, true, false);
|
||||
expect(result).toEqual(sortedByAvailability);
|
||||
expect(result.length).toBe(sortedByAvailability.length);
|
||||
expect(result[0]).not.toEqual(createNoneAgent);
|
||||
});
|
||||
|
||||
it('returns only filtered agents when both includeNoneAgent and isAgentSelected are false', () => {
|
||||
const result = getCombinedAgents(sortedByAvailability, false, false);
|
||||
expect(result).toEqual(sortedByAvailability);
|
||||
expect(result.length).toBe(sortedByAvailability.length);
|
||||
});
|
||||
|
||||
it('handles empty filteredAgentsByAvailability array', () => {
|
||||
const result = getCombinedAgents([], true, true);
|
||||
expect(result).toEqual([createNoneAgent]);
|
||||
expect(result.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,415 @@
|
||||
import * as helpers from 'dashboard/helper/automationHelper';
|
||||
import {
|
||||
OPERATOR_TYPES_1,
|
||||
OPERATOR_TYPES_3,
|
||||
OPERATOR_TYPES_4,
|
||||
} from 'dashboard/routes/dashboard/settings/automation/operators';
|
||||
import {
|
||||
customAttributes,
|
||||
labels,
|
||||
automation,
|
||||
contactAttrs,
|
||||
conversationAttrs,
|
||||
expectedOutputForCustomAttributeGenerator,
|
||||
} from './fixtures/automationFixtures';
|
||||
import { AUTOMATIONS } from 'dashboard/routes/dashboard/settings/automation/constants';
|
||||
|
||||
describe('getCustomAttributeInputType', () => {
|
||||
it('returns the attribute input type', () => {
|
||||
expect(helpers.getCustomAttributeInputType('date')).toEqual('date');
|
||||
expect(helpers.getCustomAttributeInputType('date')).not.toEqual(
|
||||
'some_random_value'
|
||||
);
|
||||
expect(helpers.getCustomAttributeInputType('text')).toEqual('plain_text');
|
||||
expect(helpers.getCustomAttributeInputType('list')).toEqual(
|
||||
'search_select'
|
||||
);
|
||||
expect(helpers.getCustomAttributeInputType('checkbox')).toEqual(
|
||||
'search_select'
|
||||
);
|
||||
expect(helpers.getCustomAttributeInputType('some_random_text')).toEqual(
|
||||
'plain_text'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isACustomAttribute', () => {
|
||||
it('returns the custom attribute value if true', () => {
|
||||
expect(
|
||||
helpers.isACustomAttribute(customAttributes, 'signed_up_at')
|
||||
).toBeTruthy();
|
||||
expect(helpers.isACustomAttribute(customAttributes, 'status')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCustomAttributeListDropdownValues', () => {
|
||||
it('returns the attribute dropdown values', () => {
|
||||
const myListValues = [
|
||||
{ id: 'item1', name: 'item1' },
|
||||
{ id: 'item2', name: 'item2' },
|
||||
{ id: 'item3', name: 'item3' },
|
||||
];
|
||||
expect(
|
||||
helpers.getCustomAttributeListDropdownValues(customAttributes, 'my_list')
|
||||
).toEqual(myListValues);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCustomAttributeCheckbox', () => {
|
||||
it('checks if attribute is a checkbox', () => {
|
||||
expect(
|
||||
helpers.isCustomAttributeCheckbox(customAttributes, 'prime_user')
|
||||
.attribute_display_type
|
||||
).toEqual('checkbox');
|
||||
expect(
|
||||
helpers.isCustomAttributeCheckbox(customAttributes, 'my_check')
|
||||
.attribute_display_type
|
||||
).toEqual('checkbox');
|
||||
expect(
|
||||
helpers.isCustomAttributeCheckbox(customAttributes, 'my_list')
|
||||
).not.toEqual('checkbox');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCustomAttributeList', () => {
|
||||
it('checks if attribute is a list', () => {
|
||||
expect(
|
||||
helpers.isCustomAttributeList(customAttributes, 'my_list')
|
||||
.attribute_display_type
|
||||
).toEqual('list');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOperatorTypes', () => {
|
||||
it('returns the correct custom attribute operators', () => {
|
||||
expect(helpers.getOperatorTypes('list')).toEqual(OPERATOR_TYPES_1);
|
||||
expect(helpers.getOperatorTypes('text')).toEqual(OPERATOR_TYPES_3);
|
||||
expect(helpers.getOperatorTypes('number')).toEqual(OPERATOR_TYPES_1);
|
||||
expect(helpers.getOperatorTypes('link')).toEqual(OPERATOR_TYPES_1);
|
||||
expect(helpers.getOperatorTypes('date')).toEqual(OPERATOR_TYPES_4);
|
||||
expect(helpers.getOperatorTypes('checkbox')).toEqual(OPERATOR_TYPES_1);
|
||||
expect(helpers.getOperatorTypes('some_random')).toEqual(OPERATOR_TYPES_1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateConditionOptions', () => {
|
||||
it('returns expected conditions options array', () => {
|
||||
const testConditions = [
|
||||
{ id: 123, title: 'Fayaz', email: 'test@test.com' },
|
||||
{ title: 'John', id: 324, email: 'test@john.com' },
|
||||
];
|
||||
const expectedConditions = [
|
||||
{ id: 123, name: 'Fayaz' },
|
||||
{ id: 324, name: 'John' },
|
||||
];
|
||||
expect(helpers.generateConditionOptions(testConditions)).toEqual(
|
||||
expectedConditions
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActionOptions', () => {
|
||||
it('returns expected actions options array', () => {
|
||||
const expectedOptions = [
|
||||
{ id: 'testlabel', name: 'testlabel' },
|
||||
{ id: 'snoozes', name: 'snoozes' },
|
||||
];
|
||||
expect(helpers.getActionOptions({ labels, type: 'add_label' })).toEqual(
|
||||
expectedOptions
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getConditionOptions', () => {
|
||||
it('returns expected conditions options', () => {
|
||||
const testOptions = [
|
||||
{ id: 'open', name: 'Open' },
|
||||
{ id: 'resolved', name: 'Resolved' },
|
||||
{ id: 'pending', name: 'Pending' },
|
||||
{ id: 'snoozed', name: 'Snoozed' },
|
||||
{ id: 'all', name: 'All' },
|
||||
];
|
||||
expect(
|
||||
helpers.getConditionOptions({
|
||||
customAttributes,
|
||||
campaigns: [],
|
||||
statusFilterOptions: testOptions,
|
||||
type: 'status',
|
||||
})
|
||||
).toEqual(testOptions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileName', () => {
|
||||
it('returns the correct file name', () => {
|
||||
expect(
|
||||
helpers.getFileName(automation.actions[0], automation.files)
|
||||
).toEqual('pfp.jpeg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultConditions', () => {
|
||||
it('returns the resp default condition model', () => {
|
||||
const messageCreatedModel = [
|
||||
{
|
||||
attribute_key: 'message_type',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
const genericConditionModel = [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
];
|
||||
expect(helpers.getDefaultConditions('message_created')).toEqual(
|
||||
messageCreatedModel
|
||||
);
|
||||
expect(helpers.getDefaultConditions()).toEqual(genericConditionModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDefaultActions', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
const genericActionModel = [
|
||||
{
|
||||
action_name: 'assign_agent',
|
||||
action_params: [],
|
||||
},
|
||||
];
|
||||
expect(helpers.getDefaultActions()).toEqual(genericActionModel);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterCustomAttributes', () => {
|
||||
it('filters the raw custom attributes', () => {
|
||||
const filteredAttributes = [
|
||||
{ key: 'signed_up_at', name: 'Signed Up At', type: 'date' },
|
||||
{ key: 'prime_user', name: 'Prime User', type: 'checkbox' },
|
||||
{ key: 'test', name: 'Test', type: 'text' },
|
||||
{ key: 'link', name: 'Link', type: 'link' },
|
||||
{ key: 'my_list', name: 'My List', type: 'list' },
|
||||
{ key: 'my_check', name: 'My Check', type: 'checkbox' },
|
||||
{ key: 'conlist', name: 'ConList', type: 'list' },
|
||||
{ key: 'asdf', name: 'asdf', type: 'link' },
|
||||
];
|
||||
expect(helpers.filterCustomAttributes(customAttributes)).toEqual(
|
||||
filteredAttributes
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStandardAttributeInputType', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
expect(
|
||||
helpers.getStandardAttributeInputType(
|
||||
AUTOMATIONS,
|
||||
'message_created',
|
||||
'message_type'
|
||||
)
|
||||
).toEqual('search_select');
|
||||
expect(
|
||||
helpers.getStandardAttributeInputType(
|
||||
AUTOMATIONS,
|
||||
'conversation_created',
|
||||
'status'
|
||||
)
|
||||
).toEqual('multi_select');
|
||||
expect(
|
||||
helpers.getStandardAttributeInputType(
|
||||
AUTOMATIONS,
|
||||
'conversation_updated',
|
||||
'referer'
|
||||
)
|
||||
).toEqual('plain_text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateAutomationPayload', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
const testPayload = {
|
||||
name: 'Test',
|
||||
description: 'This is a test',
|
||||
event_name: 'conversation_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: [{ id: 'open', name: 'Open' }],
|
||||
query_operator: 'and',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
action_name: 'add_label',
|
||||
action_params: [{ id: 2, name: 'testlabel' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
const expectedPayload = {
|
||||
name: 'Test',
|
||||
description: 'This is a test',
|
||||
event_name: 'conversation_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: ['open'],
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
action_name: 'add_label',
|
||||
action_params: [2],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(helpers.generateAutomationPayload(testPayload)).toEqual(
|
||||
expectedPayload
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCustomAttribute', () => {
|
||||
it('returns the resp default action model', () => {
|
||||
const attrs = helpers.filterCustomAttributes(customAttributes);
|
||||
expect(helpers.isCustomAttribute(attrs, 'my_list')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'my_check')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'signed_up_at')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'link')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'prime_user')).toBeTruthy();
|
||||
expect(helpers.isCustomAttribute(attrs, 'hello')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateCustomAttributes', () => {
|
||||
it('generates and returns correct condition attribute', () => {
|
||||
expect(
|
||||
helpers.generateCustomAttributes(
|
||||
conversationAttrs,
|
||||
contactAttrs,
|
||||
'Conversation Custom Attributes',
|
||||
'Contact Custom Attributes'
|
||||
)
|
||||
).toEqual(expectedOutputForCustomAttributeGenerator);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAttributes', () => {
|
||||
it('returns the conditions for the given automation type', () => {
|
||||
const result = helpers.getAttributes(AUTOMATIONS, 'message_created');
|
||||
expect(result).toEqual(AUTOMATIONS.message_created.conditions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAttributes', () => {
|
||||
it('returns the conditions for the given automation type', () => {
|
||||
const result = helpers.getAttributes(AUTOMATIONS, 'message_created');
|
||||
expect(result).toEqual(AUTOMATIONS.message_created.conditions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAutomationType', () => {
|
||||
it('returns the automation type for the given key', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getAutomationType(
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual(
|
||||
AUTOMATIONS.message_created.conditions.find(c => c.key === 'message_type')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInputType', () => {
|
||||
it('returns the input type for a custom attribute', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getInputType(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'signed_up_at'
|
||||
);
|
||||
expect(result).toEqual('date');
|
||||
});
|
||||
|
||||
it('returns the input type for a standard attribute', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getInputType(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual('search_select');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOperators', () => {
|
||||
it('returns operators for a custom attribute in edit mode', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getOperators(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'edit',
|
||||
'signed_up_at'
|
||||
);
|
||||
expect(result).toEqual(OPERATOR_TYPES_4);
|
||||
});
|
||||
|
||||
it('returns operators for a standard attribute', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getOperators(
|
||||
customAttributes,
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'create',
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual(
|
||||
AUTOMATIONS.message_created.conditions.find(c => c.key === 'message_type')
|
||||
.filterOperators
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCustomAttributeType', () => {
|
||||
it('returns the custom attribute type for the given key', () => {
|
||||
const mockAutomation = { event_name: 'message_created' };
|
||||
const result = helpers.getCustomAttributeType(
|
||||
AUTOMATIONS,
|
||||
mockAutomation,
|
||||
'message_type'
|
||||
);
|
||||
expect(result).toEqual(
|
||||
AUTOMATIONS.message_created.conditions.find(c => c.key === 'message_type')
|
||||
.customAttributeType
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('showActionInput', () => {
|
||||
it('returns false for send_email_to_team and send_message actions', () => {
|
||||
expect(helpers.showActionInput([], 'send_email_to_team')).toBe(false);
|
||||
expect(helpers.showActionInput([], 'send_message')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true if the action has an input type', () => {
|
||||
const mockActionTypes = [{ key: 'add_label', inputType: 'select' }];
|
||||
expect(helpers.showActionInput(mockActionTypes, 'add_label')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if the action does not have an input type', () => {
|
||||
const mockActionTypes = [{ key: 'some_action', inputType: null }];
|
||||
expect(helpers.showActionInput(mockActionTypes, 'some_action')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -8,15 +8,16 @@ import {
|
||||
|
||||
describe('#getTypingUsersText', () => {
|
||||
it('returns the correct text is there is only one typing user', () => {
|
||||
expect(getTypingUsersText([{ name: 'Pranav' }])).toEqual(
|
||||
'Pranav is typing'
|
||||
);
|
||||
expect(getTypingUsersText([{ name: 'Pranav' }])).toEqual([
|
||||
'TYPING.ONE',
|
||||
{ user: 'Pranav' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns the correct text is there are two typing users', () => {
|
||||
expect(
|
||||
getTypingUsersText([{ name: 'Pranav' }, { name: 'Nithin' }])
|
||||
).toEqual('Pranav and Nithin are typing');
|
||||
).toEqual(['TYPING.TWO', { user: 'Pranav', secondUser: 'Nithin' }]);
|
||||
});
|
||||
|
||||
it('returns the correct text is there are more than two users are typing', () => {
|
||||
@@ -27,7 +28,7 @@ describe('#getTypingUsersText', () => {
|
||||
{ name: 'Subin' },
|
||||
{ name: 'Sojan' },
|
||||
])
|
||||
).toEqual('Pranav and 3 others are typing');
|
||||
).toEqual(['TYPING.MULTIPLE', { user: 'Pranav', count: 3 }]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import resize from '../../directives/resize';
|
||||
|
||||
class ResizeObserverMock {
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
observe() {}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
unobserve() {}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
disconnect() {}
|
||||
}
|
||||
|
||||
describe('resize directive', () => {
|
||||
let el;
|
||||
let binding;
|
||||
let observer;
|
||||
|
||||
beforeEach(() => {
|
||||
el = document.createElement('div');
|
||||
binding = {
|
||||
value: vi.fn(),
|
||||
};
|
||||
observer = {
|
||||
observe: vi.fn(),
|
||||
unobserve: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
};
|
||||
window.ResizeObserver = ResizeObserverMock;
|
||||
vi.spyOn(window, 'ResizeObserver').mockImplementation(() => observer);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should create ResizeObserver on bind', () => {
|
||||
resize.bind(el, binding);
|
||||
|
||||
expect(ResizeObserver).toHaveBeenCalled();
|
||||
expect(observer.observe).toHaveBeenCalledWith(el);
|
||||
});
|
||||
|
||||
it('should call callback on observer callback', () => {
|
||||
el = document.createElement('div');
|
||||
binding = {
|
||||
value: vi.fn(),
|
||||
};
|
||||
|
||||
resize.bind(el, binding);
|
||||
|
||||
const entries = [{ contentRect: { width: 100, height: 100 } }];
|
||||
const callback = binding.value;
|
||||
callback(entries[0]);
|
||||
|
||||
expect(binding.value).toHaveBeenCalledWith(entries[0]);
|
||||
});
|
||||
|
||||
it('should destroy and recreate observer on update', () => {
|
||||
resize.bind(el, binding);
|
||||
|
||||
resize.update(el, { ...binding, oldValue: 'old' });
|
||||
|
||||
expect(observer.unobserve).toHaveBeenCalledWith(el);
|
||||
expect(observer.disconnect).toHaveBeenCalled();
|
||||
expect(ResizeObserver).toHaveBeenCalledTimes(2);
|
||||
expect(observer.observe).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should destroy observer on unbind', () => {
|
||||
resize.bind(el, binding);
|
||||
|
||||
resize.unbind(el);
|
||||
|
||||
expect(observer.unobserve).toHaveBeenCalledWith(el);
|
||||
expect(observer.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
// Moved from editorHelper.spec.js to editorContentHelper.spec.js
|
||||
// the mock of chatwoot/prosemirror-schema is getting conflicted with other specs
|
||||
import { getContentNode } from '../editorHelper';
|
||||
import {
|
||||
MessageMarkdownTransformer,
|
||||
messageSchema,
|
||||
} from '@chatwoot/prosemirror-schema';
|
||||
import { replaceVariablesInMessage } from '@chatwoot/utils';
|
||||
|
||||
vi.mock('@chatwoot/prosemirror-schema', () => ({
|
||||
MessageMarkdownTransformer: vi.fn(),
|
||||
messageSchema: {},
|
||||
}));
|
||||
|
||||
vi.mock('@chatwoot/utils', () => ({
|
||||
replaceVariablesInMessage: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('getContentNode', () => {
|
||||
let editorView;
|
||||
|
||||
beforeEach(() => {
|
||||
editorView = {
|
||||
state: {
|
||||
schema: {
|
||||
nodes: {
|
||||
mention: {
|
||||
create: vi.fn(),
|
||||
},
|
||||
},
|
||||
text: vi.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe('getMentionNode', () => {
|
||||
it('should create a mention node', () => {
|
||||
const content = { id: 1, name: 'John Doe' };
|
||||
const from = 0;
|
||||
const to = 10;
|
||||
getContentNode(editorView, 'mention', content, {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
expect(editorView.state.schema.nodes.mention.create).toHaveBeenCalledWith(
|
||||
{
|
||||
userId: content.id,
|
||||
userFullName: content.name,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCannedResponseNode', () => {
|
||||
it('should create a canned response node', () => {
|
||||
const content = 'Hello {{name}}';
|
||||
const variables = { name: 'John' };
|
||||
const from = 0;
|
||||
const to = 10;
|
||||
const updatedMessage = 'Hello John';
|
||||
|
||||
replaceVariablesInMessage.mockReturnValue(updatedMessage);
|
||||
MessageMarkdownTransformer.mockImplementation(() => ({
|
||||
parse: vi.fn().mockReturnValue({ textContent: updatedMessage }),
|
||||
}));
|
||||
|
||||
const { node } = getContentNode(
|
||||
editorView,
|
||||
'cannedResponse',
|
||||
content,
|
||||
{ from, to },
|
||||
variables
|
||||
);
|
||||
|
||||
expect(replaceVariablesInMessage).toHaveBeenCalledWith({
|
||||
message: content,
|
||||
variables,
|
||||
});
|
||||
expect(MessageMarkdownTransformer).toHaveBeenCalledWith(messageSchema);
|
||||
expect(node.textContent).toBe(updatedMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getVariableNode', () => {
|
||||
it('should create a variable node', () => {
|
||||
const content = 'name';
|
||||
const from = 0;
|
||||
const to = 10;
|
||||
getContentNode(editorView, 'variable', content, {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
expect(editorView.state.schema.text).toHaveBeenCalledWith('{{name}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEmojiNode', () => {
|
||||
it('should create an emoji node', () => {
|
||||
const content = '😊';
|
||||
const from = 0;
|
||||
const to = 2;
|
||||
getContentNode(editorView, 'emoji', content, {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
expect(editorView.state.schema.text).toHaveBeenCalledWith('😊');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getContentNode', () => {
|
||||
it('should return null for invalid type', () => {
|
||||
const content = 'invalid';
|
||||
const from = 0;
|
||||
const to = 10;
|
||||
const { node } = getContentNode(editorView, 'invalid', content, {
|
||||
from,
|
||||
to,
|
||||
});
|
||||
|
||||
expect(node).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
findNodeToInsertImage,
|
||||
setURLWithQueryAndSize,
|
||||
} from '../editorHelper';
|
||||
import { EditorState } from 'prosemirror-state';
|
||||
import { EditorView } from 'prosemirror-view';
|
||||
import { EditorState } from '@chatwoot/prosemirror-schema';
|
||||
import { EditorView } from '@chatwoot/prosemirror-schema';
|
||||
import { Schema } from 'prosemirror-model';
|
||||
|
||||
// Define a basic ProseMirror schema
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
export const allAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const onlineAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
];
|
||||
export const busyAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const offlineAgentsData = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const sortedByAvailability = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abraham@chatwoot.com',
|
||||
id: 5,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'John K',
|
||||
confirmed: true,
|
||||
email: 'john@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'John Kennady',
|
||||
role: 'administrator',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Honey',
|
||||
confirmed: true,
|
||||
email: 'bee@chatwoot.com',
|
||||
id: 4,
|
||||
name: 'Honey Bee',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'busy',
|
||||
available_name: 'Samuel K',
|
||||
confirmed: true,
|
||||
email: 'samuel@chatwoot.com',
|
||||
id: 2,
|
||||
name: 'Samuel Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'James K',
|
||||
confirmed: true,
|
||||
email: 'james@chatwoot.com',
|
||||
id: 3,
|
||||
name: 'James Koti',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const formattedAgentsByPresenceOnline = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abr@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
export const formattedAgentsByPresenceOffline = [
|
||||
{
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
available_name: 'Abraham',
|
||||
confirmed: true,
|
||||
email: 'abr@chatwoot.com',
|
||||
id: 1,
|
||||
name: 'Abraham Keta',
|
||||
role: 'agent',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,806 @@
|
||||
import allLanguages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
|
||||
|
||||
import allCountries from 'shared/constants/countries.js';
|
||||
|
||||
export const customAttributes = [
|
||||
{
|
||||
id: 1,
|
||||
attribute_display_name: 'Signed Up At',
|
||||
attribute_display_type: 'date',
|
||||
attribute_description: 'This is a test',
|
||||
attribute_key: 'signed_up_at',
|
||||
attribute_values: [],
|
||||
attribute_model: 'conversation_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-01-26T08:06:39.470Z',
|
||||
updated_at: '2022-01-26T08:06:39.470Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
attribute_display_name: 'Prime User',
|
||||
attribute_display_type: 'checkbox',
|
||||
attribute_description: 'Test',
|
||||
attribute_key: 'prime_user',
|
||||
attribute_values: [],
|
||||
attribute_model: 'contact_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-01-26T08:07:29.664Z',
|
||||
updated_at: '2022-01-26T08:07:29.664Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
attribute_display_name: 'Test',
|
||||
attribute_display_type: 'text',
|
||||
attribute_description: 'Test',
|
||||
attribute_key: 'test',
|
||||
attribute_values: [],
|
||||
attribute_model: 'conversation_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-01-26T08:07:58.325Z',
|
||||
updated_at: '2022-01-26T08:07:58.325Z',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
attribute_display_name: 'Link',
|
||||
attribute_display_type: 'link',
|
||||
attribute_description: 'Test',
|
||||
attribute_key: 'link',
|
||||
attribute_values: [],
|
||||
attribute_model: 'conversation_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-02-07T07:31:51.562Z',
|
||||
updated_at: '2022-02-07T07:31:51.562Z',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
attribute_display_name: 'My List',
|
||||
attribute_display_type: 'list',
|
||||
attribute_description: 'This is a sample list',
|
||||
attribute_key: 'my_list',
|
||||
attribute_values: ['item1', 'item2', 'item3'],
|
||||
attribute_model: 'conversation_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-02-21T20:31:34.175Z',
|
||||
updated_at: '2022-02-21T20:31:34.175Z',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
attribute_display_name: 'My Check',
|
||||
attribute_display_type: 'checkbox',
|
||||
attribute_description: 'Test Checkbox',
|
||||
attribute_key: 'my_check',
|
||||
attribute_values: [],
|
||||
attribute_model: 'conversation_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-02-21T20:31:53.385Z',
|
||||
updated_at: '2022-02-21T20:31:53.385Z',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
attribute_display_name: 'ConList',
|
||||
attribute_display_type: 'list',
|
||||
attribute_description: 'This is a test list\n',
|
||||
attribute_key: 'conlist',
|
||||
attribute_values: ['Hello', 'Test', 'Test2'],
|
||||
attribute_model: 'contact_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-02-28T12:58:05.005Z',
|
||||
updated_at: '2022-02-28T12:58:05.005Z',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
attribute_display_name: 'asdf',
|
||||
attribute_display_type: 'link',
|
||||
attribute_description: 'This is a some text',
|
||||
attribute_key: 'asdf',
|
||||
attribute_values: [],
|
||||
attribute_model: 'contact_attribute',
|
||||
default_value: null,
|
||||
created_at: '2022-04-21T05:48:16.168Z',
|
||||
updated_at: '2022-04-21T05:48:16.168Z',
|
||||
},
|
||||
];
|
||||
export const emptyAutomation = {
|
||||
name: null,
|
||||
description: null,
|
||||
event_name: 'conversation_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: '',
|
||||
query_operator: 'and',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
action_name: 'assign_team',
|
||||
action_params: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
export const filterAttributes = [
|
||||
{
|
||||
key: 'status',
|
||||
name: 'Status',
|
||||
attributeI18nKey: 'STATUS',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'browser_language',
|
||||
name: 'Browser Language',
|
||||
attributeI18nKey: 'BROWSER_LANGUAGE',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'country_code',
|
||||
name: 'Country',
|
||||
attributeI18nKey: 'COUNTRY_NAME',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'referer',
|
||||
name: 'Referrer Link',
|
||||
attributeI18nKey: 'REFERER_LINK',
|
||||
inputType: 'plain_text',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
{ value: 'contains', label: 'Contains' },
|
||||
{ value: 'does_not_contain', label: 'Does not contain' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'inbox_id',
|
||||
name: 'Inbox',
|
||||
attributeI18nKey: 'INBOX',
|
||||
inputType: 'multi_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'conversation_custom_attribute',
|
||||
name: 'Conversation Custom Attributes',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'signed_up_at',
|
||||
name: 'Signed Up At',
|
||||
inputType: 'date',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
{ value: 'is_present', label: 'Is present' },
|
||||
{ value: 'is_not_present', label: 'Is not present' },
|
||||
{ value: 'is_greater_than', label: 'Is greater than' },
|
||||
{ value: 'is_less_than', label: 'Is less than' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'test',
|
||||
name: 'Test',
|
||||
inputType: 'plain_text',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
{ value: 'is_present', label: 'Is present' },
|
||||
{ value: 'is_not_present', label: 'Is not present' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'link',
|
||||
name: 'Link',
|
||||
inputType: 'plain_text',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'my_list',
|
||||
name: 'My List',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'my_check',
|
||||
name: 'My Check',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'contact_custom_attribute',
|
||||
name: 'Contact Custom Attributes',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'prime_user',
|
||||
name: 'Prime User',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'conlist',
|
||||
name: 'ConList',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'asdf',
|
||||
name: 'asdf',
|
||||
inputType: 'plain_text',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
];
|
||||
export const automation = {
|
||||
id: 164,
|
||||
account_id: 1,
|
||||
name: 'Attachment',
|
||||
description: 'Yo',
|
||||
event_name: 'conversation_created',
|
||||
conditions: [
|
||||
{
|
||||
values: [{ id: 'open', name: 'Open' }],
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
query_operator: 'and',
|
||||
},
|
||||
],
|
||||
actions: [{ action_name: 'send_attachment', action_params: [59] }],
|
||||
created_on: 1652717181,
|
||||
active: true,
|
||||
files: [
|
||||
{
|
||||
id: 50,
|
||||
automation_rule_id: 164,
|
||||
file_type: 'image/jpeg',
|
||||
account_id: 1,
|
||||
file_url:
|
||||
'http://localhost:3000/rails/active_storage/blobs/redirect/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBRQT09IiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--965b4c27f4c5e47c526f0f38266b25417b72e5dd/pfp.jpeg',
|
||||
blob_id: 59,
|
||||
filename: 'pfp.jpeg',
|
||||
},
|
||||
],
|
||||
};
|
||||
export const agents = [
|
||||
{
|
||||
id: 1,
|
||||
account_id: 1,
|
||||
availability_status: 'online',
|
||||
auto_offline: true,
|
||||
confirmed: true,
|
||||
email: 'john@acme.inc',
|
||||
available_name: 'Fayaz',
|
||||
name: 'Fayaz',
|
||||
role: 'administrator',
|
||||
thumbnail:
|
||||
'https://www.gravatar.com/avatar/0d722ac7bc3b3c92c030d0da9690d981?d=404',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
account_id: 1,
|
||||
availability_status: 'offline',
|
||||
auto_offline: true,
|
||||
confirmed: true,
|
||||
email: 'john@doe.com',
|
||||
available_name: 'John',
|
||||
name: 'John',
|
||||
role: 'agent',
|
||||
thumbnail:
|
||||
'https://www.gravatar.com/avatar/6a6c19fea4a3676970167ce51f39e6ee?d=404',
|
||||
},
|
||||
];
|
||||
export const booleanFilterOptions = [
|
||||
{
|
||||
id: true,
|
||||
name: 'True',
|
||||
},
|
||||
{
|
||||
id: false,
|
||||
name: 'False',
|
||||
},
|
||||
];
|
||||
export const teams = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'sales team',
|
||||
description: 'This is our internal sales team',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'fayaz',
|
||||
description: 'Test',
|
||||
allow_auto_assign: true,
|
||||
account_id: 1,
|
||||
is_member: false,
|
||||
},
|
||||
];
|
||||
export const campaigns = [];
|
||||
export const contacts = [
|
||||
{
|
||||
additional_attributes: {},
|
||||
availability_status: 'offline',
|
||||
email: 'asd123123@asd.com',
|
||||
id: 32,
|
||||
name: 'asd123123',
|
||||
phone_number: null,
|
||||
identifier: null,
|
||||
thumbnail:
|
||||
'https://www.gravatar.com/avatar/46000d9a1eef3e24a02ca9d6c2a8f494?d=404',
|
||||
custom_attributes: {},
|
||||
conversations_count: 5,
|
||||
last_activity_at: 1650519706,
|
||||
},
|
||||
{
|
||||
additional_attributes: {},
|
||||
availability_status: 'offline',
|
||||
email: 'barry_allen@a.com',
|
||||
id: 29,
|
||||
name: 'barry_allen',
|
||||
phone_number: null,
|
||||
identifier: null,
|
||||
thumbnail:
|
||||
'https://www.gravatar.com/avatar/ab5ff99efa3bc1f74db1dc2885f9e2ce?d=404',
|
||||
custom_attributes: {},
|
||||
conversations_count: 1,
|
||||
last_activity_at: 1643728899,
|
||||
},
|
||||
];
|
||||
export const inboxes = [
|
||||
{
|
||||
id: 1,
|
||||
avatar_url: '',
|
||||
channel_id: 1,
|
||||
name: 'Acme Support',
|
||||
channel_type: 'Channel::WebWidget',
|
||||
greeting_enabled: false,
|
||||
greeting_message: '',
|
||||
working_hours_enabled: false,
|
||||
enable_email_collect: true,
|
||||
csat_survey_enabled: true,
|
||||
sender_name_type: 0,
|
||||
enable_auto_assignment: true,
|
||||
out_of_office_message:
|
||||
'We are unavailable at the moment. Leave a message we will respond once we are back.',
|
||||
working_hours: [
|
||||
{
|
||||
day_of_week: 0,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 1,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 2,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 3,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 4,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 5,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 6,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
open_all_day: false,
|
||||
},
|
||||
],
|
||||
timezone: 'America/Los_Angeles',
|
||||
callback_webhook_url: null,
|
||||
allow_messages_after_resolved: true,
|
||||
widget_color: '#1f93ff',
|
||||
website_url: 'https://acme.inc',
|
||||
hmac_mandatory: false,
|
||||
welcome_title: '',
|
||||
welcome_tagline: '',
|
||||
web_widget_script:
|
||||
'\n <script>\n (function(d,t) {\n var BASE_URL="http://localhost:3000";\n var g=d.createElement(t),s=d.getElementsByTagName(t)[0];\n g.src=BASE_URL+"/packs/js/sdk.js";\n g.defer = true;\n g.async = true;\n s.parentNode.insertBefore(g,s);\n g.onload=function(){\n window.chatwootSDK.run({\n websiteToken: \'yZ7USzaEs7hrwUAHLGwjbxJ1\',\n baseUrl: BASE_URL\n })\n }\n })(document,"script");\n </script>\n ',
|
||||
website_token: 'yZ7USzaEs7hrwUAHLGwjbxJ1',
|
||||
selected_feature_flags: ['attachments', 'emoji_picker', 'end_conversation'],
|
||||
reply_time: 'in_a_few_minutes',
|
||||
hmac_token: 'rRJW1BHu4aFMMey4SE7tWr8A',
|
||||
pre_chat_form_enabled: false,
|
||||
pre_chat_form_options: {
|
||||
pre_chat_fields: [
|
||||
{
|
||||
name: 'emailAddress',
|
||||
type: 'email',
|
||||
label: 'Email Id',
|
||||
enabled: false,
|
||||
required: true,
|
||||
field_type: 'standard',
|
||||
},
|
||||
{
|
||||
name: 'fullName',
|
||||
type: 'text',
|
||||
label: 'Full name',
|
||||
enabled: false,
|
||||
required: false,
|
||||
field_type: 'standard',
|
||||
},
|
||||
{
|
||||
name: 'phoneNumber',
|
||||
type: 'text',
|
||||
label: 'Phone number',
|
||||
enabled: false,
|
||||
required: false,
|
||||
field_type: 'standard',
|
||||
},
|
||||
],
|
||||
pre_chat_message: 'Share your queries or comments here.',
|
||||
},
|
||||
continuity_via_email: true,
|
||||
phone_number: null,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
avatar_url: '',
|
||||
channel_id: 1,
|
||||
name: 'Email',
|
||||
channel_type: 'Channel::Email',
|
||||
greeting_enabled: false,
|
||||
greeting_message: null,
|
||||
working_hours_enabled: false,
|
||||
enable_email_collect: true,
|
||||
csat_survey_enabled: false,
|
||||
enable_auto_assignment: true,
|
||||
out_of_office_message: null,
|
||||
working_hours: [
|
||||
{
|
||||
day_of_week: 0,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 1,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 2,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 3,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 4,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 5,
|
||||
closed_all_day: false,
|
||||
open_hour: 9,
|
||||
open_minutes: 0,
|
||||
close_hour: 17,
|
||||
close_minutes: 0,
|
||||
open_all_day: false,
|
||||
},
|
||||
{
|
||||
day_of_week: 6,
|
||||
closed_all_day: true,
|
||||
open_hour: null,
|
||||
open_minutes: null,
|
||||
close_hour: null,
|
||||
close_minutes: null,
|
||||
open_all_day: false,
|
||||
},
|
||||
],
|
||||
timezone: 'UTC',
|
||||
callback_webhook_url: null,
|
||||
allow_messages_after_resolved: true,
|
||||
widget_color: null,
|
||||
website_url: null,
|
||||
hmac_mandatory: null,
|
||||
welcome_title: null,
|
||||
welcome_tagline: null,
|
||||
web_widget_script: null,
|
||||
website_token: null,
|
||||
selected_feature_flags: null,
|
||||
reply_time: null,
|
||||
phone_number: null,
|
||||
forward_to_email: '9ae8ebb96c7f2d6705009f5add6d1a2d@false',
|
||||
email: 'fayaz@chatwoot.com',
|
||||
imap_login: '',
|
||||
imap_password: '',
|
||||
imap_address: '',
|
||||
imap_port: 0,
|
||||
imap_enabled: false,
|
||||
imap_enable_ssl: true,
|
||||
smtp_login: '',
|
||||
smtp_password: '',
|
||||
smtp_address: '',
|
||||
smtp_port: 0,
|
||||
smtp_enabled: false,
|
||||
smtp_domain: '',
|
||||
smtp_enable_ssl_tls: false,
|
||||
smtp_enable_starttls_auto: true,
|
||||
smtp_openssl_verify_mode: 'none',
|
||||
smtp_authentication: 'login',
|
||||
},
|
||||
];
|
||||
export const labels = [
|
||||
{
|
||||
id: 2,
|
||||
title: 'testlabel',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
title: 'snoozes',
|
||||
},
|
||||
];
|
||||
export const statusFilterOptions = [
|
||||
{ id: 'open', name: 'Open' },
|
||||
{ id: 'resolved', name: 'Resolved' },
|
||||
{ id: 'pending', name: 'Pending' },
|
||||
{ id: 'snoozed', name: 'Snoozed' },
|
||||
{ id: 'all', name: 'All' },
|
||||
];
|
||||
export const languages = allLanguages;
|
||||
export const countries = allCountries;
|
||||
export const MESSAGE_CONDITION_VALUES = [
|
||||
{
|
||||
id: 'incoming',
|
||||
name: 'Incoming Message',
|
||||
},
|
||||
{
|
||||
id: 'outgoing',
|
||||
name: 'Outgoing Message',
|
||||
},
|
||||
];
|
||||
|
||||
export const automationToSubmit = {
|
||||
name: 'Fayaz',
|
||||
description: 'Hello',
|
||||
event_name: 'conversation_created',
|
||||
conditions: [
|
||||
{
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
values: [{ id: 'open', name: 'Open' }],
|
||||
query_operator: 'and',
|
||||
custom_attribute_type: '',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{ action_name: 'add_label', action_params: [{ id: 2, name: 'testlabel' }] },
|
||||
],
|
||||
};
|
||||
|
||||
export const savedAutomation = {
|
||||
id: 165,
|
||||
account_id: 1,
|
||||
name: 'Fayaz',
|
||||
description: 'Hello',
|
||||
event_name: 'conversation_created',
|
||||
conditions: [
|
||||
{
|
||||
values: ['open'],
|
||||
attribute_key: 'status',
|
||||
filter_operator: 'equal_to',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
action_name: 'add_label',
|
||||
action_params: [2],
|
||||
},
|
||||
],
|
||||
created_on: 1652776043,
|
||||
active: true,
|
||||
};
|
||||
|
||||
export const contactAttrs = [
|
||||
{
|
||||
key: 'contact_list',
|
||||
name: 'Contact List',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
export const conversationAttrs = [
|
||||
{
|
||||
key: 'text_attr',
|
||||
name: 'Text Attr',
|
||||
inputType: 'plain_text',
|
||||
filterOperators: [
|
||||
{
|
||||
value: 'equal_to',
|
||||
label: 'Equal to',
|
||||
},
|
||||
{
|
||||
value: 'not_equal_to',
|
||||
label: 'Not equal to',
|
||||
},
|
||||
{
|
||||
value: 'is_present',
|
||||
label: 'Is present',
|
||||
},
|
||||
{
|
||||
value: 'is_not_present',
|
||||
label: 'Is not present',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
export const expectedOutputForCustomAttributeGenerator = [
|
||||
{
|
||||
key: 'conversation_custom_attribute',
|
||||
name: 'Conversation Custom Attributes',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'text_attr',
|
||||
name: 'Text Attr',
|
||||
inputType: 'plain_text',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
{ value: 'is_present', label: 'Is present' },
|
||||
{ value: 'is_not_present', label: 'Is not present' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'contact_custom_attribute',
|
||||
name: 'Contact Custom Attributes',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
key: 'contact_list',
|
||||
name: 'Contact List',
|
||||
inputType: 'search_select',
|
||||
filterOperators: [
|
||||
{ value: 'equal_to', label: 'Equal to' },
|
||||
{ value: 'not_equal_to', label: 'Not equal to' },
|
||||
],
|
||||
},
|
||||
];
|
||||
export const slaPolicies = [
|
||||
{
|
||||
id: 1,
|
||||
account_id: 1,
|
||||
name: 'Low',
|
||||
first_response_time_threshold: 60,
|
||||
next_response_time_threshold: 120,
|
||||
resolution_time_threshold: 240,
|
||||
created_at: '2022-01-26T08:06:39.470Z',
|
||||
updated_at: '2022-01-26T08:06:39.470Z',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
account_id: 1,
|
||||
name: 'Medium',
|
||||
first_response_time_threshold: 30,
|
||||
next_response_time_threshold: 60,
|
||||
resolution_time_threshold: 120,
|
||||
created_at: '2022-01-26T08:06:39.470Z',
|
||||
updated_at: '2022-01-26T08:06:39.470Z',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
account_id: 1,
|
||||
name: 'High',
|
||||
first_response_time_threshold: 15,
|
||||
next_response_time_threshold: 30,
|
||||
resolution_time_threshold: 60,
|
||||
created_at: '2022-01-26T08:06:39.470Z',
|
||||
updated_at: '2022-01-26T08:06:39.470Z',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
account_id: 1,
|
||||
name: 'Urgent',
|
||||
first_response_time_threshold: 5,
|
||||
next_response_time_threshold: 10,
|
||||
resolution_time_threshold: 20,
|
||||
created_at: '2022-01-26T08:06:39.470Z',
|
||||
updated_at: '2022-01-26T08:06:39.470Z',
|
||||
},
|
||||
];
|
||||
@@ -1,4 +1,9 @@
|
||||
import { getInboxClassByType, getInboxWarningIconClass } from '../inbox';
|
||||
import {
|
||||
INBOX_TYPES,
|
||||
getInboxClassByType,
|
||||
getInboxIconByType,
|
||||
getInboxWarningIconClass,
|
||||
} from '../inbox';
|
||||
|
||||
describe('#Inbox Helpers', () => {
|
||||
describe('getInboxClassByType', () => {
|
||||
@@ -35,6 +40,116 @@ describe('#Inbox Helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInboxIconByType', () => {
|
||||
describe('fill variant (default)', () => {
|
||||
it('returns correct icon for web widget', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.WEB)).toBe('i-ri-global-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for Facebook', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.FB)).toBe('i-ri-messenger-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for Twitter', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TWITTER)).toBe(
|
||||
'i-ri-twitter-x-fill'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct icon for WhatsApp', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.WHATSAPP)).toBe(
|
||||
'i-ri-whatsapp-fill'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct icon for API', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.API)).toBe('i-ri-cloudy-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for Email', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.EMAIL)).toBe('i-ri-mail-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for Telegram', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TELEGRAM)).toBe(
|
||||
'i-ri-telegram-fill'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct icon for Line', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.LINE)).toBe('i-ri-line-fill');
|
||||
});
|
||||
|
||||
it('returns default icon for unknown type', () => {
|
||||
expect(getInboxIconByType('UNKNOWN_TYPE')).toBe('i-ri-chat-1-fill');
|
||||
});
|
||||
|
||||
it('returns default icon for undefined type', () => {
|
||||
expect(getInboxIconByType(undefined)).toBe('i-ri-chat-1-fill');
|
||||
});
|
||||
});
|
||||
|
||||
describe('line variant', () => {
|
||||
it('returns correct line icon for web widget', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.WEB, null, 'line')).toBe(
|
||||
'i-ri-global-line'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct line icon for Facebook', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.FB, null, 'line')).toBe(
|
||||
'i-ri-messenger-line'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct line icon for unknown type', () => {
|
||||
expect(getInboxIconByType('UNKNOWN_TYPE', null, 'line')).toBe(
|
||||
'i-ri-chat-1-line'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Twilio cases', () => {
|
||||
describe('fill variant', () => {
|
||||
it('returns WhatsApp icon for Twilio WhatsApp number', () => {
|
||||
expect(
|
||||
getInboxIconByType(INBOX_TYPES.TWILIO, 'whatsapp:+1234567890')
|
||||
).toBe('i-ri-whatsapp-fill');
|
||||
});
|
||||
|
||||
it('returns SMS icon for regular Twilio number', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TWILIO, '+1234567890')).toBe(
|
||||
'i-ri-chat-1-fill'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns SMS icon when phone number is undefined', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TWILIO, undefined)).toBe(
|
||||
'i-ri-chat-1-fill'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('line variant', () => {
|
||||
it('returns WhatsApp line icon for Twilio WhatsApp number', () => {
|
||||
expect(
|
||||
getInboxIconByType(
|
||||
INBOX_TYPES.TWILIO,
|
||||
'whatsapp:+1234567890',
|
||||
'line'
|
||||
)
|
||||
).toBe('i-ri-whatsapp-line');
|
||||
});
|
||||
|
||||
it('returns SMS line icon for regular Twilio number', () => {
|
||||
expect(
|
||||
getInboxIconByType(INBOX_TYPES.TWILIO, '+1234567890', 'line')
|
||||
).toBe('i-ri-chat-1-line');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInboxWarningIconClass', () => {
|
||||
it('should return correct class for warning', () => {
|
||||
expect(getInboxWarningIconClass('Channel::FacebookPage', true)).toEqual(
|
||||
|
||||
@@ -1,8 +1,32 @@
|
||||
import {
|
||||
buildPermissionsFromRouter,
|
||||
getCurrentAccount,
|
||||
getUserPermissions,
|
||||
hasPermissions,
|
||||
filterItemsByPermission,
|
||||
} from '../permissionsHelper';
|
||||
|
||||
describe('#getCurrentAccount', () => {
|
||||
it('should return the current account', () => {
|
||||
expect(getCurrentAccount({ accounts: [{ id: 1 }] }, 1)).toEqual({ id: 1 });
|
||||
expect(getCurrentAccount({ accounts: [] }, 1)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getUserPermissions', () => {
|
||||
it('should return the correct permissions', () => {
|
||||
const user = {
|
||||
accounts: [
|
||||
{ id: 1, permissions: ['conversations_manage'] },
|
||||
{ id: 3, permissions: ['contacts_manage'] },
|
||||
],
|
||||
};
|
||||
expect(getUserPermissions(user, 1)).toEqual(['conversations_manage']);
|
||||
expect(getUserPermissions(user, '3')).toEqual(['contacts_manage']);
|
||||
expect(getUserPermissions(user, 2)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPermissions', () => {
|
||||
it('returns true if permission is present', () => {
|
||||
expect(
|
||||
@@ -82,3 +106,113 @@ describe('buildPermissionsFromRouter', () => {
|
||||
}).toThrow("The route doesn't have the required permissions defined");
|
||||
});
|
||||
});
|
||||
|
||||
describe('filterItemsByPermission', () => {
|
||||
const items = {
|
||||
item1: { name: 'Item 1', permissions: ['agent', 'administrator'] },
|
||||
item2: {
|
||||
name: 'Item 2',
|
||||
permissions: [
|
||||
'conversation_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
],
|
||||
},
|
||||
item3: { name: 'Item 3', permissions: ['contact_manage'] },
|
||||
item4: { name: 'Item 4', permissions: ['report_manage'] },
|
||||
item5: { name: 'Item 5', permissions: ['knowledge_base_manage'] },
|
||||
item6: {
|
||||
name: 'Item 6',
|
||||
permissions: [
|
||||
'agent',
|
||||
'administrator',
|
||||
'conversation_manage',
|
||||
'conversation_unassigned_manage',
|
||||
'conversation_participating_manage',
|
||||
'contact_manage',
|
||||
'report_manage',
|
||||
'knowledge_base_manage',
|
||||
],
|
||||
},
|
||||
item7: { name: 'Item 7', permissions: [] },
|
||||
};
|
||||
|
||||
const getPermissions = item => item.permissions;
|
||||
|
||||
it('filters items based on user permissions', () => {
|
||||
const userPermissions = ['agent', 'contact_manage', 'report_manage'];
|
||||
const result = filterItemsByPermission(
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(5);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item1', name: 'Item 1' })
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item3', name: 'Item 3' })
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item4', name: 'Item 4' })
|
||||
);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item6', name: 'Item 6' })
|
||||
);
|
||||
});
|
||||
|
||||
it('includes items with empty permissions', () => {
|
||||
const userPermissions = [];
|
||||
const result = filterItemsByPermission(
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item7', name: 'Item 7' })
|
||||
);
|
||||
});
|
||||
|
||||
it('uses custom transform function when provided', () => {
|
||||
const userPermissions = ['agent', 'contact_manage'];
|
||||
const customTransform = (key, item) => ({ id: key, title: item.name });
|
||||
const result = filterItemsByPermission(
|
||||
items,
|
||||
userPermissions,
|
||||
getPermissions,
|
||||
customTransform
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result).toContainEqual({ id: 'item1', title: 'Item 1' });
|
||||
expect(result).toContainEqual({ id: 'item3', title: 'Item 3' });
|
||||
expect(result).toContainEqual({ id: 'item6', title: 'Item 6' });
|
||||
});
|
||||
|
||||
it('handles empty items object', () => {
|
||||
const result = filterItemsByPermission({}, ['agent'], getPermissions);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles custom getPermissions function', () => {
|
||||
const customItems = {
|
||||
item1: { name: 'Item 1', requiredPerms: ['agent', 'administrator'] },
|
||||
item2: { name: 'Item 2', requiredPerms: ['contact_manage'] },
|
||||
};
|
||||
const customGetPermissions = item => item.requiredPerms;
|
||||
const result = filterItemsByPermission(
|
||||
customItems,
|
||||
['agent'],
|
||||
customGetPermissions
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result).toContainEqual(
|
||||
expect.objectContaining({ key: 'item1', name: 'Item 1' })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import {
|
||||
getConversationDashboardRoute,
|
||||
getCurrentAccount,
|
||||
isAConversationRoute,
|
||||
defaultRedirectPage,
|
||||
routeIsAccessibleFor,
|
||||
validateLoggedInRoutes,
|
||||
isAInboxViewRoute,
|
||||
} from '../routeHelpers';
|
||||
|
||||
describe('#getCurrentAccount', () => {
|
||||
it('should return the current account', () => {
|
||||
expect(getCurrentAccount({ accounts: [{ id: 1 }] }, 1)).toEqual({ id: 1 });
|
||||
expect(getCurrentAccount({ accounts: [] }, 1)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#routeIsAccessibleFor', () => {
|
||||
it('should return the correct access', () => {
|
||||
let route = { meta: { permissions: ['administrator'] } };
|
||||
@@ -22,6 +15,57 @@ describe('#routeIsAccessibleFor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#defaultRedirectPage', () => {
|
||||
const to = {
|
||||
params: { accountId: '2' },
|
||||
fullPath: '/app/accounts/2/dashboard',
|
||||
name: 'home',
|
||||
};
|
||||
|
||||
it('should return dashboard route for users with conversation permissions', () => {
|
||||
const permissions = ['conversation_manage', 'agent'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
|
||||
it('should return contacts route for users with contact permissions', () => {
|
||||
const permissions = ['contact_manage'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/contacts');
|
||||
});
|
||||
|
||||
it('should return reports route for users with report permissions', () => {
|
||||
const permissions = ['report_manage'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe(
|
||||
'accounts/2/reports/overview'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return portals route for users with portal permissions', () => {
|
||||
const permissions = ['knowledge_base_manage'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/portals');
|
||||
});
|
||||
|
||||
it('should return dashboard route as default for users with custom roles', () => {
|
||||
const permissions = ['custom_role'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
|
||||
it('should return dashboard route for users with administrator role', () => {
|
||||
const permissions = ['administrator'];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
|
||||
it('should return dashboard route for users with multiple permissions', () => {
|
||||
const permissions = [
|
||||
'contact_manage',
|
||||
'custom_role',
|
||||
'conversation_manage',
|
||||
'agent',
|
||||
'administrator',
|
||||
];
|
||||
expect(defaultRedirectPage(to, permissions)).toBe('accounts/2/dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#validateLoggedInRoutes', () => {
|
||||
describe('when account access is missing', () => {
|
||||
it('should return the login route', () => {
|
||||
|
||||
@@ -1,52 +1,93 @@
|
||||
import { uploadFile } from '../uploadHelper';
|
||||
import axios from 'axios';
|
||||
import { uploadExternalImage, uploadFile } from '../uploadHelper';
|
||||
|
||||
global.axios = axios;
|
||||
vi.mock('axios');
|
||||
|
||||
describe('#Upload Helpers', () => {
|
||||
describe('Upload Helpers', () => {
|
||||
afterEach(() => {
|
||||
// Cleaning up the mock after each test
|
||||
axios.post.mockReset();
|
||||
});
|
||||
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
describe('uploadFile', () => {
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await uploadFile(mockFile, '1602');
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
expect.any(FormData),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
});
|
||||
});
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
it('should handle errors', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
});
|
||||
const mockError = new Error('Failed to upload');
|
||||
|
||||
const result = await uploadFile(mockFile, '1602');
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
expect.any(FormData),
|
||||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle errors', async () => {
|
||||
const mockFile = new File(['dummy content'], 'example.png', {
|
||||
type: 'image/png',
|
||||
describe('uploadExternalImage', () => {
|
||||
it('should send a POST request with correct data', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockResponse = {
|
||||
data: {
|
||||
file_url: 'https://example.com/fileUrl',
|
||||
blob_key: 'blobKey123',
|
||||
blob_id: 'blobId456',
|
||||
},
|
||||
};
|
||||
|
||||
axios.post.mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const result = await uploadExternalImage(mockUrl, '1602');
|
||||
|
||||
expect(axios.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1602/upload',
|
||||
{ external_url: mockUrl },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
fileUrl: 'https://example.com/fileUrl',
|
||||
blobKey: 'blobKey123',
|
||||
blobId: 'blobId456',
|
||||
});
|
||||
});
|
||||
const mockError = new Error('Failed to upload');
|
||||
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
it('should handle errors', async () => {
|
||||
const mockUrl = 'https://example.com/image.jpg';
|
||||
const mockError = new Error('Failed to upload');
|
||||
|
||||
await expect(uploadFile(mockFile)).rejects.toThrow('Failed to upload');
|
||||
axios.post.mockRejectedValueOnce(mockError);
|
||||
|
||||
await expect(uploadExternalImage(mockUrl)).rejects.toThrow(
|
||||
'Failed to upload'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,26 +19,47 @@ const HEADERS = {
|
||||
* The function uses FormData to wrap the file and axios to send the request.
|
||||
*
|
||||
* @param {File} file - The file to be uploaded. It should be a File object (typically coming from a file input element).
|
||||
* @param {string} accountId - The account ID.
|
||||
* @returns {Promise} A promise that resolves with the server's response when the upload is successful, or rejects if there's an error.
|
||||
*/
|
||||
export async function uploadFile(file, accountId) {
|
||||
// Create a new FormData instance.
|
||||
let formData = new FormData();
|
||||
|
||||
if (!accountId) {
|
||||
accountId = window.location.pathname.split('/')[3];
|
||||
}
|
||||
|
||||
// Append the file to the FormData instance under the key 'attachment'.
|
||||
let formData = new FormData();
|
||||
formData.append('attachment', file);
|
||||
|
||||
// Use axios to send a POST request to the upload endpoint.
|
||||
const { data } = await axios.post(
|
||||
`/api/${API_VERSION}/accounts/${accountId}/upload`,
|
||||
formData,
|
||||
{
|
||||
headers: HEADERS,
|
||||
}
|
||||
{ headers: HEADERS }
|
||||
);
|
||||
|
||||
return {
|
||||
fileUrl: data.file_url,
|
||||
blobKey: data.blob_key,
|
||||
blobId: data.blob_id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads an image from an external URL.
|
||||
*
|
||||
* @param {string} url - The external URL of the image.
|
||||
* @param {string} accountId - The account ID.
|
||||
* @returns {Promise} A promise that resolves with the server's response.
|
||||
*/
|
||||
export async function uploadExternalImage(url, accountId) {
|
||||
if (!accountId) {
|
||||
accountId = window.location.pathname.split('/')[3];
|
||||
}
|
||||
|
||||
const { data } = await axios.post(
|
||||
`/api/${API_VERSION}/accounts/${accountId}/upload`,
|
||||
{ external_url: url },
|
||||
{ headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,24 @@ export const VALUE_MUST_BE_BETWEEN_1_AND_998 =
|
||||
export const ACTION_PARAMETERS_REQUIRED = 'ACTION_PARAMETERS_REQUIRED';
|
||||
export const ATLEAST_ONE_CONDITION_REQUIRED = 'ATLEAST_ONE_CONDITION_REQUIRED';
|
||||
export const ATLEAST_ONE_ACTION_REQUIRED = 'ATLEAST_ONE_ACTION_REQUIRED';
|
||||
|
||||
const isEmptyValue = value => {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return !value.length;
|
||||
}
|
||||
|
||||
// We can safely check the type here as both the null value
|
||||
// and the array is ruled out earlier.
|
||||
if (typeof value === 'object') {
|
||||
return !Object.keys(value).length;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
// ------------------------------------------------------------------
|
||||
// ------------------------ Filter Validation -----------------------
|
||||
// ------------------------------------------------------------------
|
||||
@@ -20,7 +38,7 @@ export const ATLEAST_ONE_ACTION_REQUIRED = 'ATLEAST_ONE_ACTION_REQUIRED';
|
||||
*
|
||||
* @returns {string|null} An error message if validation fails, or null if validation passes.
|
||||
*/
|
||||
const validateSingleFilter = filter => {
|
||||
export const validateSingleFilter = filter => {
|
||||
if (!filter.attribute_key) {
|
||||
return ATTRIBUTE_KEY_REQUIRED;
|
||||
}
|
||||
@@ -29,12 +47,11 @@ const validateSingleFilter = filter => {
|
||||
return FILTER_OPERATOR_REQUIRED;
|
||||
}
|
||||
|
||||
if (
|
||||
filter.filter_operator !== 'is_present' &&
|
||||
filter.filter_operator !== 'is_not_present' &&
|
||||
(!filter.values ||
|
||||
(Array.isArray(filter.values) && filter.values.length === 0))
|
||||
) {
|
||||
const operatorRequiresValue = !['is_present', 'is_not_present'].includes(
|
||||
filter.filter_operator
|
||||
);
|
||||
|
||||
if (operatorRequiresValue && isEmptyValue(filter.values)) {
|
||||
return VALUE_REQUIRED;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user