-// [VITE] TODO: Test this component across different screen sizes and usages
import { ref, provide, onMounted, computed } from 'vue';
import { useEventListener } from '@vueuse/core';
@@ -17,15 +16,10 @@ const props = defineProps({
const emit = defineEmits(['change']);
const hasScroll = ref(false);
-// TODO: We may not this internalActiveIndex, we can use activeIndex directly
-// But right I'll keep it and fix it when testing the rest of the codebase
-const internalActiveIndex = ref(props.index);
-// Create a proxy for activeIndex using computed
const activeIndex = computed({
- get: () => internalActiveIndex.value,
+ get: () => props.index,
set: newValue => {
- internalActiveIndex.value = newValue;
emit('change', newValue);
},
});
diff --git a/app/javascript/dashboard/components/widgets/TableHeaderCell.vue b/app/javascript/dashboard/components/widgets/TableHeaderCell.vue
index ce3cb21ac..e838b477d 100644
--- a/app/javascript/dashboard/components/widgets/TableHeaderCell.vue
+++ b/app/javascript/dashboard/components/widgets/TableHeaderCell.vue
@@ -29,7 +29,7 @@ const spanClass = computed(() => {
diff --git a/app/javascript/dashboard/components/widgets/UserAvatarWithName.vue b/app/javascript/dashboard/components/widgets/UserAvatarWithName.vue
index 23553f4c2..1e78c334d 100644
--- a/app/javascript/dashboard/components/widgets/UserAvatarWithName.vue
+++ b/app/javascript/dashboard/components/widgets/UserAvatarWithName.vue
@@ -4,7 +4,7 @@ import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
defineProps({
user: {
type: Object,
- default: () => {},
+ default: () => ({}),
},
size: {
type: String,
@@ -12,7 +12,7 @@ defineProps({
},
textClass: {
type: String,
- default: 'text-xs text-slate-600',
+ default: 'text-sm text-n-slate-12',
},
});
@@ -25,11 +25,11 @@ defineProps({
:username="user.name"
:status="user.availability_status"
/>
-
{{ user.name }}
-
+
diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue
index 17ab554c4..83f234f9f 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue
@@ -62,10 +62,13 @@ export default {
},
},
watch: {
- 'currentChat.inbox_id'(inboxId) {
- if (inboxId) {
- this.$store.dispatch('inboxAssignableAgents/fetch', [inboxId]);
- }
+ 'currentChat.inbox_id': {
+ immediate: true,
+ handler(inboxId) {
+ if (inboxId) {
+ this.$store.dispatch('inboxAssignableAgents/fetch', [inboxId]);
+ }
+ },
},
'currentChat.id'() {
this.fetchLabels();
diff --git a/app/javascript/dashboard/components/widgets/conversation/components/SLAEventItem.vue b/app/javascript/dashboard/components/widgets/conversation/components/SLAEventItem.vue
index 82774cbe0..1f513d78b 100644
--- a/app/javascript/dashboard/components/widgets/conversation/components/SLAEventItem.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/components/SLAEventItem.vue
@@ -18,7 +18,7 @@ const formatDate = timestamp =>
{{ label }}
@@ -26,7 +26,7 @@ const formatDate = timestamp =>
{{ formatDate(item.created_at) }}
diff --git a/app/javascript/dashboard/components/widgets/conversation/components/SLAPopoverCard.vue b/app/javascript/dashboard/components/widgets/conversation/components/SLAPopoverCard.vue
index 6b1c66d02..3355905c6 100644
--- a/app/javascript/dashboard/components/widgets/conversation/components/SLAPopoverCard.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/components/SLAPopoverCard.vue
@@ -40,9 +40,9 @@ const toggleShowAllNRT = () => {
-
+
{{ $t('SLA.EVENTS.TITLE') }}
{
it('returns an account-scoped route', () => {
const wrapper = mount(createComponent(), mountParams);
const { accountScopedRoute } = wrapper.vm;
- const result = accountScopedRoute('accountDetail', { userId: 456 });
+ const result = accountScopedRoute('accountDetail', { userId: 456 }, {});
expect(result).toEqual({
name: 'accountDetail',
params: { accountId: 123, userId: 456 },
+ query: {},
});
});
diff --git a/app/javascript/dashboard/composables/useAccount.js b/app/javascript/dashboard/composables/useAccount.js
index 08e291e39..c8c245adb 100644
--- a/app/javascript/dashboard/composables/useAccount.js
+++ b/app/javascript/dashboard/composables/useAccount.js
@@ -28,10 +28,11 @@ export function useAccount() {
return `/app/accounts/${accountId.value}/${url}`;
};
- const accountScopedRoute = (name, params) => {
+ const accountScopedRoute = (name, params, query) => {
return {
name,
params: { accountId: accountId.value, ...params },
+ query: { ...query },
};
};
diff --git a/app/javascript/dashboard/composables/useTransformKeys.js b/app/javascript/dashboard/composables/useTransformKeys.js
new file mode 100644
index 000000000..e1feaf1a7
--- /dev/null
+++ b/app/javascript/dashboard/composables/useTransformKeys.js
@@ -0,0 +1,25 @@
+// NOTE: In the future if performance becomes an issue, we can memoize the functions
+
+import { unref } from 'vue';
+import camelcaseKeys from 'camelcase-keys';
+import snakecaseKeys from 'snakecase-keys';
+
+/**
+ * Vue composable that converts object keys to camelCase
+ * @param {Object|Array|import('vue').Ref} payload - Object or array to convert
+ * @returns {Object|Array} Converted payload with camelCase keys
+ */
+export function useCamelCase(payload) {
+ const unrefPayload = unref(payload);
+ return camelcaseKeys(unrefPayload);
+}
+
+/**
+ * Vue composable that converts object keys to snake_case
+ * @param {Object|Array|import('vue').Ref} payload - Object or array to convert
+ * @returns {Object|Array} Converted payload with snake_case keys
+ */
+export function useSnakeCase(payload) {
+ const unrefPayload = unref(payload);
+ return snakecaseKeys(unrefPayload);
+}
diff --git a/app/javascript/dashboard/constants/appEvents.js b/app/javascript/dashboard/constants/appEvents.js
new file mode 100644
index 000000000..71f044232
--- /dev/null
+++ b/app/javascript/dashboard/constants/appEvents.js
@@ -0,0 +1,5 @@
+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';
diff --git a/app/javascript/dashboard/helper/AudioAlerts/AudioMessageHelper.js b/app/javascript/dashboard/helper/AudioAlerts/AudioMessageHelper.js
new file mode 100644
index 000000000..0590b1a58
--- /dev/null
+++ b/app/javascript/dashboard/helper/AudioAlerts/AudioMessageHelper.js
@@ -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;
diff --git a/app/javascript/dashboard/helper/AudioAlerts/AudioNotificationStore.js b/app/javascript/dashboard/helper/AudioAlerts/AudioNotificationStore.js
new file mode 100644
index 000000000..233516b99
--- /dev/null
+++ b/app/javascript/dashboard/helper/AudioAlerts/AudioNotificationStore.js
@@ -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;
diff --git a/app/javascript/dashboard/helper/AudioAlerts/DashboardAudioNotificationHelper.js b/app/javascript/dashboard/helper/AudioAlerts/DashboardAudioNotificationHelper.js
index 26417bcad..ca823fc31 100644
--- a/app/javascript/dashboard/helper/AudioAlerts/DashboardAudioNotificationHelper.js
+++ b/app/javascript/dashboard/helper/AudioAlerts/DashboardAudioNotificationHelper.js
@@ -1,92 +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';
-import {
- ROLES,
- CONVERSATION_PERMISSIONS,
-} from 'dashboard/constants/permissions.js';
-import { getUserPermissions } from 'dashboard/helper/permissionsHelper.js';
+ 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'];
-class DashboardAudioNotificationHelper {
- constructor() {
- this.recurringNotificationTimer = null;
- this.audioAlertType = 'none';
- this.playAlertOnlyWhenHidden = true;
- this.alertIfUnreadConversationExist = false;
- this.currentUser = null;
- this.currentUserId = null;
- this.audioAlertTone = 'ding';
+export class DashboardAudioNotificationHelper {
+ constructor(store) {
+ if (!store) {
+ throw new Error('store is required');
+ }
+ this.store = new AudioNotificationStore(store);
- this.onAudioListenEvent = async () => {
- try {
- await getAlertAudio('', {
- type: 'dashboard',
- alertTone: this.audioAlertTone,
- });
- initOnEvents.forEach(event => {
- document.removeEventListener(event, this.onAudioListenEvent, false);
- });
- this.playAudioEvery30Seconds();
- } catch (error) {
- // Ignore audio fetch errors
- }
+ this.notificationConfig = {
+ audioAlertType: DEFAULT_ALERT_TYPE,
+ playAlertOnlyWhenHidden: true,
+ alertIfUnreadConversationExist: false,
};
+
+ this.recurringNotificationTimer = null;
+
+ this.audioConfig = {
+ audio: null,
+ tone: DEFAULT_TONE,
+ hasSentSoundPermissionsRequest: false,
+ };
+
+ this.currentUser = null;
}
- setInstanceValues = ({
+ intializeAudio = () => {
+ const resourceUrl = `${ALERT_PATH_PREFIX}${this.audioConfig.tone}.mp3`;
+ this.audioConfig.audio = new Audio(resourceUrl);
+ return this.audioConfig.audio.load();
+ };
+
+ playAudioAlert = async () => {
+ try {
+ await this.audioConfig.audio.play();
+ } catch (error) {
+ 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,
- audioAlertTone,
+ audioAlertType = DEFAULT_ALERT_TYPE,
+ audioAlertTone = DEFAULT_TONE,
}) => {
- this.audioAlertType = audioAlertType;
- this.playAlertOnlyWhenHidden = !alwaysPlayAudioAlert;
- this.alertIfUnreadConversationExist = alertIfUnreadConversationExist;
+ this.notificationConfig = {
+ ...this.notificationConfig,
+ audioAlertType: audioAlertType.split('+').filter(Boolean),
+ playAlertOnlyWhenHidden: !alwaysPlayAudioAlert,
+ alertIfUnreadConversationExist: alertIfUnreadConversationExist,
+ };
+
this.currentUser = currentUser;
- this.currentUserId = currentUser.id;
- this.audioAlertTone = audioAlertTone;
- initOnEvents.forEach(e => {
- document.addEventListener(e, this.onAudioListenEvent, {
- once: true,
- });
- });
+
+ 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_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
@@ -94,67 +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;
- };
-
- isUserHasConversationPermission = () => {
- const currentAccountId = window.WOOT_STORE.getters.getCurrentAccountId;
- // Get the user permissions for the current account
- const userPermissions = getUserPermissions(
- this.currentUser,
- currentAccountId
- );
- // Check if the user has the required permissions
- const hasRequiredPermission = [...ROLES, ...CONVERSATION_PERMISSIONS].some(
- permission => userPermissions.includes(permission)
- );
- return hasRequiredPermission;
+ 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 user does not have the permission to view the conversation, then dismiss the alert
- if (!this.isUserHasConversationPermission()) {
+ // 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 or the
- // correct notification is not enabled, then dismiss the alert
- if (
- this.isMessageFromCurrentUser(message) ||
- !this.shouldNotifyOnMessage(message)
- ) {
+ // 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;
}
@@ -164,21 +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();
};
}
-const notifHelper = new DashboardAudioNotificationHelper();
-window.notifHelper = notifHelper;
-export default notifHelper;
+export default new DashboardAudioNotificationHelper(GlobalStore);
diff --git a/app/javascript/dashboard/helper/AudioAlerts/WindowVisibilityHelper.js b/app/javascript/dashboard/helper/AudioAlerts/WindowVisibilityHelper.js
new file mode 100644
index 000000000..23772c9bd
--- /dev/null
+++ b/app/javascript/dashboard/helper/AudioAlerts/WindowVisibilityHelper.js
@@ -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();
diff --git a/app/javascript/dashboard/helper/AudioAlerts/specs/AudioMessageHelper.spec.js b/app/javascript/dashboard/helper/AudioAlerts/specs/AudioMessageHelper.spec.js
new file mode 100644
index 000000000..9751cf729
--- /dev/null
+++ b/app/javascript/dashboard/helper/AudioAlerts/specs/AudioMessageHelper.spec.js
@@ -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);
+ });
+});
diff --git a/app/javascript/dashboard/helper/AudioAlerts/specs/AudioNotificationStore.spec.js b/app/javascript/dashboard/helper/AudioAlerts/specs/AudioNotificationStore.spec.js
new file mode 100644
index 000000000..5e8971c2d
--- /dev/null
+++ b/app/javascript/dashboard/helper/AudioAlerts/specs/AudioNotificationStore.spec.js
@@ -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
+ );
+ });
+ });
+});
diff --git a/app/javascript/dashboard/helper/AudioAlerts/specs/WindowVisibilityHelper.spec.js b/app/javascript/dashboard/helper/AudioAlerts/specs/WindowVisibilityHelper.spec.js
new file mode 100644
index 000000000..6440cc0e0
--- /dev/null
+++ b/app/javascript/dashboard/helper/AudioAlerts/specs/WindowVisibilityHelper.spec.js
@@ -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);
+ });
+ });
+});
diff --git a/app/javascript/dashboard/helper/scriptHelpers.js b/app/javascript/dashboard/helper/scriptHelpers.js
index dd007d0db..55ad6eb13 100644
--- a/app/javascript/dashboard/helper/scriptHelpers.js
+++ b/app/javascript/dashboard/helper/scriptHelpers.js
@@ -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,7 +24,7 @@ const initializeAudioAlerts = user => {
// entire payload for the user during the signup process.
} = uiSettings || {};
- DashboardAudioNotificationHelper.setInstanceValues({
+ DashboardAudioNotificationHelper.set({
currentUser: user,
audioAlertType: audioAlertType || 'none',
audioAlertTone: audioAlertTone || 'ding',
diff --git a/app/javascript/dashboard/helper/validations.js b/app/javascript/dashboard/helper/validations.js
index 352e5397e..4347f55aa 100644
--- a/app/javascript/dashboard/helper/validations.js
+++ b/app/javascript/dashboard/helper/validations.js
@@ -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;
}
diff --git a/app/javascript/dashboard/i18n/locale/en/advancedFilters.json b/app/javascript/dashboard/i18n/locale/en/advancedFilters.json
index a382aec2e..a991cb25b 100644
--- a/app/javascript/dashboard/i18n/locale/en/advancedFilters.json
+++ b/app/javascript/dashboard/i18n/locale/en/advancedFilters.json
@@ -22,14 +22,23 @@
"OPERATOR_LABELS": {
"equal_to": "Equal to",
"not_equal_to": "Not equal to",
- "contains": "Contains",
"does_not_contain": "Does not contain",
"is_present": "Is present",
"is_not_present": "Is not present",
"is_greater_than": "Is greater than",
"is_less_than": "Is lesser than",
"days_before": "Is x days before",
- "starts_with": "Starts with"
+ "starts_with": "Starts with",
+ "equalTo": "Equal to",
+ "notEqualTo": "Not equal to",
+ "contains": "Contains",
+ "doesNotContain": "Does not contain",
+ "isPresent": "Is present",
+ "isNotPresent": "Is not present",
+ "isGreaterThan": "Is greater than",
+ "isLessThan": "Is lesser than",
+ "daysBefore": "Is x days before",
+ "startsWith": "Starts with"
},
"ATTRIBUTE_LABELS": {
"TRUE": "True",
@@ -56,7 +65,10 @@
"LAST_ACTIVITY": "Last activity"
},
"ERRORS": {
- "VALUE_REQUIRED": "Value is required"
+ "VALUE_REQUIRED": "Value is required",
+ "ATTRIBUTE_KEY_REQUIRED": "Attribute key is required",
+ "FILTER_OPERATOR_REQUIRED": "Filter operator is required",
+ "VALUE_MUST_BE_BETWEEN_1_AND_998": "Value must be between 1 and 998"
},
"GROUPS": {
"STANDARD_FILTERS": "Standard filters",
diff --git a/app/javascript/dashboard/i18n/locale/en/components.json b/app/javascript/dashboard/i18n/locale/en/components.json
index 7a0533cef..cafedd5fa 100644
--- a/app/javascript/dashboard/i18n/locale/en/components.json
+++ b/app/javascript/dashboard/i18n/locale/en/components.json
@@ -5,8 +5,10 @@
},
"COMBOBOX": {
"PLACEHOLDER": "Select an option...",
+ "EMPTY_SEARCH_RESULTS": "No items found for the search term `{searchTerm}`",
"EMPTY_STATE": "No results found.",
- "SEARCH_PLACEHOLDER": "Search..."
+ "SEARCH_PLACEHOLDER": "Search...",
+ "MORE": "+{count} more"
},
"DROPDOWN_MENU": {
"SEARCH_PLACEHOLDER": "Search...",
@@ -30,5 +32,11 @@
},
"BREADCRUMB": {
"ARIA_LABEL": "Breadcrumb"
+ },
+ "SWITCH": {
+ "TOGGLE": "Toggle switch"
+ },
+ "LABEL": {
+ "TAG_BUTTON": "tag"
}
}
diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json
index 54a06bb53..e0d2efdfd 100644
--- a/app/javascript/dashboard/i18n/locale/en/contact.json
+++ b/app/javascript/dashboard/i18n/locale/en/contact.json
@@ -385,5 +385,273 @@
"DROPDOWN_ITEM": {
"ID": "(ID: {identifier})"
}
+ },
+
+ "CONTACTS_LAYOUT": {
+ "HEADER": {
+ "TITLE": "Contacts",
+ "SEARCH_TITLE": "Search contacts",
+ "SEARCH_PLACEHOLDER": "Search...",
+ "MESSAGE_BUTTON": "Message",
+ "BREADCRUMB": {
+ "CONTACTS": "Contacts"
+ },
+ "ACTIONS": {
+ "CONTACT_CREATION": {
+ "ADD_CONTACT": "Add contact",
+ "EXPORT_CONTACT": "Export contacts",
+ "IMPORT_CONTACT": "Import contacts",
+ "SAVE_CONTACT": "Save contact",
+ "EMAIL_ADDRESS_DUPLICATE": "This email address is in use for another contact.",
+ "PHONE_NUMBER_DUPLICATE": "This phone number is in use for another contact.",
+ "SUCCESS_MESSAGE": "Contact saved successfully",
+ "ERROR_MESSAGE": "Unable to save contact. Please try again later."
+ },
+ "IMPORT_CONTACT": {
+ "TITLE": "Import contacts",
+ "DESCRIPTION": "Import contacts through a CSV file.",
+ "DOWNLOAD_LABEL": "Download a sample csv.",
+ "LABEL": "CSV File:",
+ "CHOOSE_FILE": "Choose file",
+ "CHANGE": "Change",
+ "CANCEL": "Cancel",
+ "IMPORT": "Import",
+ "SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",
+ "ERROR_MESSAGE": "There was an error, please try again"
+ },
+ "EXPORT_CONTACT": {
+ "TITLE": "Export contacts",
+ "DESCRIPTION": "Quickly export a csv file with comprehensive details of your contacts",
+ "CONFIRM": "Export",
+ "SUCCESS_MESSAGE": "Export is in progress. You will be notified on email when the export file is ready to download.",
+ "ERROR_MESSAGE": "There was an error, please try again"
+ },
+ "SORT_BY": {
+ "LABEL": "Sort by",
+ "OPTIONS": {
+ "NAME": "Name",
+ "EMAIL": "Email",
+ "PHONE_NUMBER": "Phone number",
+ "COMPANY": "Company",
+ "COUNTRY": "Country",
+ "CITY": "City",
+ "LAST_ACTIVITY": "Last activity",
+ "CREATED_AT": "Created at"
+ }
+ },
+ "ORDER": {
+ "LABEL": "Ordering",
+ "OPTIONS": {
+ "ASCENDING": "Ascending",
+ "DESCENDING": "Descending"
+ }
+ },
+ "FILTERS": {
+ "CREATE_SEGMENT": {
+ "TITLE": "Do you want to save this filter?",
+ "CONFIRM": "Save filter",
+ "LABEL": "Name",
+ "PLACEHOLDER": "Enter the name of the filter",
+ "ERROR": "Enter a valid name",
+ "SUCCESS_MESSAGE": "Filter saved successfully",
+ "ERROR_MESSAGE": "Unable to save filter. Please try again later."
+ },
+ "DELETE_SEGMENT": {
+ "TITLE": "Confirm Deletion",
+ "DESCRIPTION": "Are you sure you want to delete this filter?",
+ "CONFIRM": "Yes, Delete",
+ "CANCEL": "No, Cancel",
+ "SUCCESS_MESSAGE": "Filter deleted successfully",
+ "ERROR_MESSAGE": "Unable to delete filter. Please try again later."
+ }
+ }
+ }
+ },
+ "PAGINATION_FOOTER": {
+ "SHOWING": "Showing {startItem} - {endItem} of {totalItems} contacts"
+ },
+ "FILTER": {
+ "NAME": "Name",
+ "EMAIL": "Email",
+ "PHONE_NUMBER": "Phone number",
+ "IDENTIFIER": "Identifier",
+ "COUNTRY": "Country",
+ "CITY": "City",
+ "CREATED_AT": "Created at",
+ "LAST_ACTIVITY": "Last activity",
+ "REFERER_LINK": "Referer link",
+ "BLOCKED": "Blocked",
+ "BLOCKED_TRUE": "True",
+ "BLOCKED_FALSE": "False",
+ "BUTTONS": {
+ "CLEAR_FILTERS": "Clear filters",
+ "UPDATE_SEGMENT": "Update segment",
+ "APPLY_FILTERS": "Apply filters",
+ "ADD_FILTER": "Add filter"
+ },
+ "TITLE": "Filter contacts",
+ "EDIT_SEGMENT": "Edit segment",
+ "SEGMENT": {
+ "LABEL": "Segment name",
+ "INPUT_PLACEHOLDER": "Enter the name of the segment"
+ },
+ "ACTIVE_FILTERS": {
+ "MORE_FILTERS": "+ {count} more filters",
+ "CLEAR_FILTERS": "Clear filters"
+ }
+ },
+ "CARD": {
+ "OF": "of",
+ "VIEW_DETAILS": "View details",
+ "EDIT_DETAILS_FORM": {
+ "TITLE": "Edit contact details",
+ "FORM": {
+ "FIRST_NAME": {
+ "PLACEHOLDER": "Enter the first name"
+ },
+ "LAST_NAME": {
+ "PLACEHOLDER": "Enter the last name"
+ },
+ "EMAIL_ADDRESS": {
+ "PLACEHOLDER": "Enter the email address",
+ "DUPLICATE": "This email address is in use for another contact."
+ },
+ "PHONE_NUMBER": {
+ "PLACEHOLDER": "Enter the phone number",
+ "DUPLICATE": "This phone number is in use for another contact."
+ },
+ "CITY": {
+ "PLACEHOLDER": "Enter the city name"
+ },
+ "COUNTRY": {
+ "PLACEHOLDER": "Select country"
+ },
+ "BIO": {
+ "PLACEHOLDER": "Enter the bio"
+ },
+ "COMPANY_NAME": {
+ "PLACEHOLDER": "Enter the company name"
+ }
+ },
+ "UPDATE_BUTTON": "Update contact",
+ "SUCCESS_MESSAGE": "Contact updated successfully",
+ "ERROR_MESSAGE": "Unable to update contact. Please try again later."
+ },
+ "SOCIAL_MEDIA": {
+ "TITLE": "Edit social links",
+ "FORM": {
+ "FACEBOOK": {
+ "PLACEHOLDER": "Add Facebook"
+ },
+ "GITHUB": {
+ "PLACEHOLDER": "Add Github"
+ },
+ "INSTAGRAM": {
+ "PLACEHOLDER": "Add Instagram"
+ },
+ "LINKEDIN": {
+ "PLACEHOLDER": "Add LinkedIn"
+ },
+ "TWITTER": {
+ "PLACEHOLDER": "Add Twitter"
+ }
+ }
+ }
+ },
+ "DETAILS": {
+ "CREATED_AT": "Created {date}",
+ "LAST_ACTIVITY": "Last active {date}",
+ "DELETE_CONTACT_DESCRIPTION": "Permanently delete this contact. This action is irreversible",
+ "DELETE_CONTACT": "Delete contact",
+ "DELETE_DIALOG": {
+ "TITLE": "Confirm Deletion",
+ "DESCRIPTION": "Are you sure you want to delete this {contactName} contact?",
+ "CONFIRM": "Yes, Delete",
+ "API": {
+ "SUCCESS_MESSAGE": "Contact deleted successfully",
+ "ERROR_MESSAGE": "Could not delete contact. Please try again later."
+ }
+ },
+ "AVATAR": {
+ "UPLOAD": {
+ "ERROR_MESSAGE": "Could not upload avatar. Please try again later.",
+ "SUCCESS_MESSAGE": "Avatar uploaded successfully"
+ },
+ "DELETE": {
+ "SUCCESS_MESSAGE": "Avatar deleted successfully",
+ "ERROR_MESSAGE": "Could not delete avatar. Please try again later."
+ }
+ }
+ },
+ "SIDEBAR": {
+ "TABS": {
+ "ATTRIBUTES": "Attributes",
+ "HISTORY": "History",
+ "NOTES": "Notes",
+ "MERGE": "Merge"
+ },
+ "HISTORY": {
+ "EMPTY_STATE": "There are no previous conversations associated to this contact"
+ },
+ "ATTRIBUTES": {
+ "SEARCH_PLACEHOLDER": "Search for attributes",
+ "UNUSED_ATTRIBUTES": "{count} Used attribute | {count} Unused attributes",
+ "EMPTY_STATE": "There are no contact custom attributes available in this account. You can create a custom attribute in settings.",
+ "YES": "Yes",
+ "NO": "No",
+ "TRIGGER": {
+ "SELECT": "Select value",
+ "INPUT": "Enter value"
+ },
+ "VALIDATIONS": {
+ "INVALID_NUMBER": "Invalid number",
+ "REQUIRED": "Valid value is required",
+ "INVALID_INPUT": "Invalid input",
+ "INVALID_URL": "Invalid URL",
+ "INVALID_DATE": "Invalid date"
+ },
+ "NO_ATTRIBUTES": "No attributes found",
+ "API": {
+ "SUCCESS_MESSAGE": "Attribute updated successfully",
+ "DELETE_SUCCESS_MESSAGE": "Attribute deleted successfully",
+ "UPDATE_ERROR": "Unable to update attribute. Please try again later",
+ "DELETE_ERROR": "Unable to delete attribute. Please try again later"
+ }
+ },
+ "MERGE": {
+ "TITLE": "Merge contact",
+ "DESCRIPTION": "Combine two profiles into one, including all attributes and conversations. In case of conflict, the primary contactās attributes will take precedence.",
+ "PRIMARY": "Primary contact",
+ "PRIMARY_HELP_LABEL": "To be saved",
+ "PRIMARY_REQUIRED_ERROR": "Please select a contact to merge with before proceeding",
+ "PARENT": "To be merged",
+ "PARENT_HELP_LABEL": "To be deleted",
+ "EMPTY_STATE": "No contacts found",
+ "PLACEHOLDER": "Search for primary contact",
+ "SEARCH_PLACEHOLDER": "Search for a contact",
+ "SEARCH_ERROR_MESSAGE": "Could not search for contacts. Please try again later.",
+ "SUCCESS_MESSAGE": "Contact merged successfully",
+ "ERROR_MESSAGE": "Could not merge contacts, try again!",
+ "IS_SEARCHING": "Searching...",
+ "BUTTONS": {
+ "CANCEL": "Cancel",
+ "CONFIRM": "Merge contact"
+ }
+ },
+ "NOTES": {
+ "PLACEHOLDER": "Add a note",
+ "WROTE": "wrote",
+ "YOU": "You",
+ "SAVE": "Save note",
+ "EMPTY_STATE": "There are no notes associated to this contact. You can add a note by typing in the box above."
+ }
+ },
+ "EMPTY_STATE": {
+ "TITLE": "No contacts found in this account",
+ "SUBTITLE": "Start adding new contacts by clicking on the button below",
+ "BUTTON_LABEL": "Add contact",
+ "SEARCH_EMPTY_STATE_TITLE": "No contacts matches your search š",
+ "LIST_EMPTY_STATE_TITLE": "No contacts available in this view š"
+ }
}
}
diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json
index 9b2c5fde6..9b1ca70d2 100644
--- a/app/javascript/dashboard/i18n/locale/en/settings.json
+++ b/app/javascript/dashboard/i18n/locale/en/settings.json
@@ -61,15 +61,29 @@
"COPY": "Copy"
},
"AUDIO_NOTIFICATIONS_SECTION": {
- "TITLE": "Audio Notifications",
- "NOTE": "Enable audio notifications in dashboard for new messages and conversations.",
+ "TITLE": "Audio Alerts",
+ "NOTE": "Enable audio alerts in dashboard for new messages and conversations.",
+ "PLAY": "Play sound",
"ALERT_TYPES": {
"NONE": "None",
"MINE": "Assigned",
- "ALL": "All"
+ "ALL": "All",
+ "ASSIGNED": "My assigned conversations",
+ "UNASSIGNED": "Unassigned conversations",
+ "NOTME": "Open conversations assigned to others"
+ },
+ "ALERT_COMBINATIONS": {
+ "NONE": "You haven't selected any options, you won't receive any audio alerts.",
+ "ASSIGNED": "You'll receive alerts for conversations assigned to you.",
+ "UNASSIGNED": "You'll receive alerts for any unassigned conversations.",
+ "NOTME": "You'll receive alerts for conversations assigned to others.",
+ "ASSIGNED+UNASSIGNED": "You'll receive alerts for your assigned conversations and any unattended ones.",
+ "ASSIGNED+NOTME": "You'll receive alerts for conversations assigned to you and to others, but not for unassigned ones.",
+ "NOTME+UNASSIGNED": "You'll receive alerts for unattended conversations and those assigned to others.",
+ "ASSIGNED+NOTME+UNASSIGNED": "You'll receive alerts for all conversations."
},
"ALERT_TYPE": {
- "TITLE": "Alert events for conversations:",
+ "TITLE": "Alert events for conversations",
"NONE": "None",
"ASSIGNED": "Assigned Conversations",
"ALL_CONVERSATIONS": "All Conversations"
@@ -81,7 +95,9 @@
"TITLE": "Alert conditions:",
"CONDITION_ONE": "Send audio alerts only if the browser window is not active",
"CONDITION_TWO": "Send alerts every 30s until all the assigned conversations are read"
- }
+ },
+ "SOUND_PERMISSION_ERROR": "Autoplay is disabled in your browser. To hear alerts automatically, enable sound permission in your browser settings or interact with the page.",
+ "READ_MORE": "Read more"
},
"EMAIL_NOTIFICATIONS_SECTION": {
"TITLE": "Email Notifications",
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue
index bc74353c2..29b1faee6 100644
--- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue
+++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsAdvancedFilters.vue
@@ -83,7 +83,7 @@ export default {
this.filterTypes = [...this.filterTypes, ...filterTypes];
this.filterGroups = filterGroups;
- if (this.getAppliedContactFilters.length) {
+ if (this.getAppliedContactFilters.length && !this.isSegmentsView) {
this.appliedFilters = [...this.getAppliedContactFilters];
} else if (!this.isSegmentsView) {
this.appliedFilters.push({
@@ -318,7 +318,7 @@ export default {
@reset-filter="resetFilter(i, appliedFilters[i])"
@remove-filter="removeFilter(i)"
/>
-
+
-import { mapGetters } from 'vuex';
-import ContactInfoPanel from '../components/ContactInfoPanel.vue';
-import ContactNotes from 'dashboard/modules/notes/NotesOnContactPage.vue';
-import SettingsHeader from '../../settings/SettingsHeader.vue';
-import Spinner from 'shared/components/Spinner.vue';
-import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
+
-
-
-
-
-
-
- {{ $t('CONTACT_PROFILE.LOADING') }}
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/pages/ContactsIndex.vue b/app/javascript/dashboard/routes/dashboard/contacts/pages/ContactsIndex.vue
new file mode 100644
index 000000000..a491d0829
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/contacts/pages/ContactsIndex.vue
@@ -0,0 +1,300 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{
+ searchQuery || !hasAppliedFilters
+ ? t('CONTACTS_LAYOUT.EMPTY_STATE.SEARCH_EMPTY_STATE_TITLE')
+ : t('CONTACTS_LAYOUT.EMPTY_STATE.LIST_EMPTY_STATE_TITLE')
+ }}
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/contacts/routes.js b/app/javascript/dashboard/routes/dashboard/contacts/routes.js
index d9abe796e..ea5238a3b 100644
--- a/app/javascript/dashboard/routes/dashboard/contacts/routes.js
+++ b/app/javascript/dashboard/routes/dashboard/contacts/routes.js
@@ -1,48 +1,60 @@
-/* eslint arrow-body-style: 0 */
import { frontendURL } from '../../../helper/URLHelper';
-import ContactsView from './components/ContactsView.vue';
+import ContactsIndex from './pages/ContactsIndex.vue';
import ContactManageView from './pages/ContactManageView.vue';
+const commonMeta = {
+ permissions: ['administrator', 'agent', 'contact_manage'],
+};
+
export const routes = [
{
path: frontendURL('accounts/:accountId/contacts'),
- name: 'contacts_dashboard',
- meta: {
- permissions: ['administrator', 'agent', 'contact_manage'],
- },
- component: ContactsView,
- },
- {
- path: frontendURL('accounts/:accountId/contacts/custom_view/:id'),
- name: 'contacts_segments_dashboard',
- meta: {
- permissions: ['administrator', 'agent', 'contact_manage'],
- },
- component: ContactsView,
- props: route => {
- return { segmentsId: route.params.id };
- },
- },
- {
- path: frontendURL('accounts/:accountId/labels/:label/contacts'),
- name: 'contacts_labels_dashboard',
- meta: {
- permissions: ['administrator', 'agent', 'contact_manage'],
- },
- component: ContactsView,
- props: route => {
- return { label: route.params.label };
- },
+ component: ContactsIndex,
+ meta: commonMeta,
+ children: [
+ {
+ path: '',
+ name: 'contacts_dashboard_index',
+ component: ContactsIndex,
+ meta: commonMeta,
+ },
+ {
+ path: 'segments/:segmentId',
+ name: 'contacts_dashboard_segments_index',
+ component: ContactsIndex,
+ meta: commonMeta,
+ },
+ {
+ path: 'labels/:label',
+ name: 'contacts_dashboard_labels_index',
+ component: ContactsIndex,
+ meta: commonMeta,
+ },
+ ],
},
{
path: frontendURL('accounts/:accountId/contacts/:contactId'),
- name: 'contact_profile_dashboard',
- meta: {
- permissions: ['administrator', 'agent', 'contact_manage'],
- },
component: ContactManageView,
- props: route => {
- return { contactId: route.params.contactId };
- },
+ meta: commonMeta,
+ children: [
+ {
+ path: '',
+ name: 'contacts_edit',
+ component: ContactManageView,
+ meta: commonMeta,
+ },
+ {
+ path: 'segments/:segmentId',
+ name: 'contacts_edit_segment',
+ component: ContactManageView,
+ meta: commonMeta,
+ },
+ {
+ path: 'labels/:label',
+ name: 'contacts_edit_label',
+ component: ContactManageView,
+ meta: commonMeta,
+ },
+ ],
},
];
diff --git a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesEditPage.vue b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesEditPage.vue
index 736b34096..3bb868a58 100644
--- a/app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesEditPage.vue
+++ b/app/javascript/dashboard/routes/dashboard/helpcenter/pages/PortalsArticlesEditPage.vue
@@ -34,10 +34,11 @@ const portalLink = computed(() => {
);
});
-const saveArticle = async ({ ...values }) => {
+const saveArticle = async ({ ...values }, isAsync = false) => {
+ const actionToDispatch = isAsync ? 'articles/updateAsync' : 'articles/update';
isUpdating.value = true;
try {
- await store.dispatch('articles/update', {
+ await store.dispatch(actionToDispatch, {
portalSlug,
articleId: articleSlug,
...values,
@@ -55,6 +56,10 @@ const saveArticle = async ({ ...values }) => {
}
};
+const saveArticleAsync = async ({ ...values }) => {
+ saveArticle({ ...values }, true);
+};
+
const isCategoryArticles = computed(() => {
return (
route.name === 'portals_categories_articles_index' ||
@@ -92,9 +97,7 @@ const previewArticle = () => {
});
};
-onMounted(() => {
- fetchArticleDetails();
-});
+onMounted(fetchArticleDetails);
@@ -103,6 +106,7 @@ onMounted(() => {
:is-updating="isUpdating"
:is-saved="isSaved"
@save-article="saveArticle"
+ @save-article-async="saveArticleAsync"
@preview-article="previewArticle"
@go-back="goBackToArticles"
/>
diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertCondition.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertCondition.vue
index 18ae1c07e..898d9afb6 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertCondition.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertCondition.vue
@@ -10,6 +10,7 @@ defineProps({
required: true,
},
});
+
const emit = defineEmits(['change']);
const onChange = (id, value) => {
emit('change', id, value);
@@ -23,18 +24,22 @@ const onChange = (id, value) => {
>
{{ label }}
-
+
-
+
{{ item.label }}
diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertEvent.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertEvent.vue
index ddc6545f6..890956f7e 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertEvent.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertEvent.vue
@@ -1,6 +1,7 @@
-
+
{{ label }}
-
+
- setValue(isChecked, option.value)"
/>
{{
$t(
@@ -61,6 +95,9 @@ const selectedValue = computed({
}}
+
+ {{ $t(alertDescription) }}
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertTone.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertTone.vue
index 232a7a74a..06d6932d4 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertTone.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/profile/AudioAlertTone.vue
@@ -1,12 +1,15 @@
-
-
+
- {{ tone.label }}
-
-
+
+ {{ tone.label }}
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/AudioNotifications.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/AudioNotifications.vue
index f09432f83..bd96f3721 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/profile/AudioNotifications.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/profile/AudioNotifications.vue
@@ -1,98 +1,91 @@
-
@@ -100,27 +93,19 @@ export default {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue
index be1d166ab..a38046580 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue
@@ -190,7 +190,6 @@ export default {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/UserProfilePicture.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/UserProfilePicture.vue
index 87ac25efa..d86329b76 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/profile/UserProfilePicture.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/profile/UserProfilePicture.vue
@@ -1,8 +1,6 @@
@@ -15,5 +9,6 @@ export default {
getter-key="agents/getAgents"
action-key="agents/get"
:download-button-label="$t('REPORT.DOWNLOAD_AGENT_REPORTS')"
+ :report-title="$t('AGENT_REPORTS.HEADER')"
/>
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue
index f692965cd..772a6810a 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/BotReports.vue
@@ -5,11 +5,13 @@ import ReportFilterSelector from './components/FilterSelector.vue';
import { GROUP_BY_FILTER } from './constants';
import ReportContainer from './ReportContainer.vue';
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
+import ReportHeader from './components/ReportHeader.vue';
export default {
name: 'BotReports',
components: {
BotMetrics,
+ ReportHeader,
ReportFilterSelector,
ReportContainer,
},
@@ -84,21 +86,20 @@ export default {
-
-
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/CsatResponses.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/CsatResponses.vue
index ca92d44a0..58a014165 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/CsatResponses.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/CsatResponses.vue
@@ -7,6 +7,8 @@ import ReportFilterSelector from './components/FilterSelector.vue';
import { generateFileName } from '../../../../helper/downloadHelper';
import { REPORTS_EVENTS } from '../../../../helper/AnalyticsHelper/events';
import { FEATURE_FLAGS } from '../../../../featureFlags';
+import V4Button from 'dashboard/components-next/button/Button.vue';
+import ReportHeader from './components/ReportHeader.vue';
export default {
name: 'CsatResponses',
@@ -14,6 +16,8 @@ export default {
CsatMetrics,
CsatTable,
ReportFilterSelector,
+ ReportHeader,
+ V4Button,
},
data() {
return {
@@ -108,26 +112,26 @@ export default {
-
-
-
-
- {{ $t('CSAT_REPORTS.DOWNLOAD') }}
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/InboxReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/InboxReports.vue
index 96d573829..100094f65 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/InboxReports.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/InboxReports.vue
@@ -1,11 +1,5 @@
-
@@ -15,5 +9,6 @@ export default {
getter-key="inboxes/getInboxes"
action-key="inboxes/get"
:download-button-label="$t('INBOX_REPORTS.DOWNLOAD_INBOX_REPORTS')"
+ :report-title="$t('INBOX_REPORTS.HEADER')"
/>
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue
index 274c13b30..858ceb4ad 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue
@@ -1,4 +1,5 @@
-
-
-
- {{ $t('REPORT.DOWNLOAD_AGENT_REPORTS') }}
-
-
-
-
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/LabelReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/LabelReports.vue
index d3792bd79..04d7dc7ff 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/LabelReports.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/LabelReports.vue
@@ -1,11 +1,5 @@
-
@@ -15,5 +9,6 @@ export default {
getter-key="labels/getLabels"
action-key="labels/get"
:download-button-label="$t('LABEL_REPORTS.DOWNLOAD_LABEL_REPORTS')"
+ :report-title="$t('LABEL_REPORTS.HEADER')"
/>
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/LiveReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/LiveReports.vue
index 589dddfea..71b001138 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/LiveReports.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/LiveReports.vue
@@ -10,10 +10,12 @@ import getUnixTime from 'date-fns/getUnixTime';
import startOfDay from 'date-fns/startOfDay';
import subDays from 'date-fns/subDays';
import { emitter } from 'shared/helpers/mitt';
+import ReportHeader from './components/ReportHeader.vue';
export default {
name: 'LiveReports',
components: {
+ ReportHeader,
AgentTable,
MetricCard,
ReportHeatmap,
@@ -123,83 +125,75 @@ export default {
-
-
-
-
-
-
-
-
- {{ name }}
-
-
- {{ metric }}
-
-
-
-
-
-
-
-
-
-
- {{ name }}
-
-
- {{ metric }}
-
-
-
-
-
-
-
+
+
+
+
-
-
- {{ $t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.DOWNLOAD_REPORT') }}
-
-
-
+
+
+ {{ name }}
+
+
+ {{ metric }}
+
+
-
-
-
+
+
+
+
+ {{ name }}
+
+
+ {{ metric }}
+
+
+
+
+
+
+ {{ $t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.DOWNLOAD_REPORT') }}
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue
index 449a68be5..52058d359 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/ReportContainer.vue
@@ -135,7 +135,7 @@ export default {
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/SLAReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/SLAReports.vue
index c065d91bd..826b12fec 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/SLAReports.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/SLAReports.vue
@@ -1,13 +1,17 @@
-
-
-
-
- {{ $t('SLA_REPORTS.DOWNLOAD_SLA_REPORTS') }}
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/TeamReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/TeamReports.vue
index 523186b8d..d441c986b 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/TeamReports.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/TeamReports.vue
@@ -1,11 +1,5 @@
-
@@ -15,5 +9,6 @@ export default {
getter-key="teams/getTeams"
action-key="teams/get"
:download-button-label="$t('TEAM_REPORTS.DOWNLOAD_TEAM_REPORTS')"
+ :report-title="$t('TEAM_REPORTS.HEADER')"
/>
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/BotMetrics.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/BotMetrics.vue
index fe8e30ba8..e8d8eccf9 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/BotMetrics.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/BotMetrics.vue
@@ -38,7 +38,7 @@ onMounted(fetchMetrics);
{
-
+
{{ metric.NAME }}
-
+
{{ displayMetric(metric.KEY) }}
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue
index d4fe666a7..f33ba9f37 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/CsatMetrics.vue
@@ -86,7 +86,7 @@ export default {
{{ $t('CSAT_REPORTS.NO_RECORDS') }}
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue
index 6eae61836..4e66f6350 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/FilterSelector.vue
@@ -178,7 +178,7 @@ export default {
-
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/Filters/v3/ActiveFilterChip.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/Filters/v3/ActiveFilterChip.vue
index 3a02deb73..2a21ab1be 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/Filters/v3/ActiveFilterChip.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/Filters/v3/ActiveFilterChip.vue
@@ -53,7 +53,6 @@ const closeDropdown = () => emit('closeDropdown');
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/Heatmap.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/Heatmap.vue
index 68c83f43e..a8c1229f4 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/Heatmap.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/Heatmap.vue
@@ -63,7 +63,7 @@ function getDayOfTheWeek(date) {
return days[dayIndex];
}
function getHeatmapLevelClass(value) {
- if (!value) return 'outline-n-weak bg-n-solid-2';
+ if (!value) return 'outline-n-container dark:bg-slate-700/40 bg-slate-50/50';
let level = [...quantileRange.value, Infinity].findIndex(
range => value <= range && value > 0
@@ -72,7 +72,7 @@ function getHeatmapLevelClass(value) {
if (level > 6) level = 5;
if (level === 0) {
- return 'outline-slate-100 dark:outline-slate-700 dark:bg-slate-700/40 bg-slate-50/50';
+ return 'outline-n-container dark:bg-slate-700/40 bg-slate-50/50';
}
const classes = [
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportHeader.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportHeader.vue
new file mode 100644
index 000000000..a42442a9d
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportHeader.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+ {{ headerTitle }}
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportMetricCard.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportMetricCard.vue
index dff88eae9..4c6aff970 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportMetricCard.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportMetricCard.vue
@@ -22,26 +22,23 @@ defineProps({
-
+
{{ label }}
{{ value }}
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportsWrapper.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportsWrapper.vue
new file mode 100644
index 000000000..2be08487d
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportsWrapper.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLAMetricCard.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLAMetricCard.vue
index 6a4bafc2e..2111b26e3 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLAMetricCard.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLAMetricCard.vue
@@ -24,7 +24,7 @@ export default {
{{ label }}
-
+
{{ value }}
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLAMetrics.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLAMetrics.vue
index cd268ec77..433c30d49 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLAMetrics.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLAMetrics.vue
@@ -22,7 +22,7 @@ defineProps({
-
+
-
+
{
-
+
{{ `#${conversationId} ` }}
-
+
{{ $t('SLA_REPORTS.WITH') }}
- {{
+ {{
conversation.contact.name
}}
@@ -61,7 +62,7 @@ const conversationLabels = computed(() => {
v-if="conversation.assignee"
:user="conversation.assignee"
/>
- ---
+ ---
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLATable.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLATable.vue
index d44389982..503ccf442 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLATable.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLATable.vue
@@ -57,9 +57,11 @@ export default {
-
+
@@ -89,10 +91,7 @@ export default {
:sla-events="slaReport.sla_events"
/>
-
+
{{ $t('SLA_REPORTS.NO_RECORDS') }}
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLAViewDetails.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLAViewDetails.vue
index ff84c2f1c..eb7a60dd1 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLAViewDetails.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/SLA/SLAViewDetails.vue
@@ -29,24 +29,23 @@ export default {
-
-
-
-
- {{ $t('SLA_REPORTS.TABLE.VIEW_DETAILS') }}
-
-
-
+
+
+
+ {{ $t('SLA_REPORTS.TABLE.VIEW_DETAILS') }}
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue
index 2057798e8..b0e6b84a0 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/WootReports.vue
@@ -1,10 +1,12 @@
-
-
-
- {{ downloadButtonLabel }}
-
-
-
-
-
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/AgentTable.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/AgentTable.vue
index d176b6377..810db7029 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/AgentTable.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/AgentTable.vue
@@ -144,10 +144,7 @@ const table = useVueTable({
-
+
@@ -169,7 +166,7 @@ const table = useVueTable({
.ve-table {
&::v-deep {
th.ve-table-header-th {
- font-size: var(--font-size-mini) !important;
+ @apply text-sm rounded-xl;
padding: var(--space-small) var(--space-two) !important;
}
diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/MetricCard.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/MetricCard.vue
index 03fe4b22c..df3011e03 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/MetricCard.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/overview/MetricCard.vue
@@ -25,25 +25,23 @@ export default {