Merge branch 'develop' into feat/sla-3

This commit is contained in:
Vishnu Narayanan
2024-02-08 12:16:09 +05:30
committed by GitHub
436 changed files with 6377 additions and 3753 deletions
@@ -71,3 +71,5 @@ export const ICON_APPEARANCE = `<svg role="img" class="ninja-icon ninja-icon--fl
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>`;
@@ -15,3 +15,6 @@ 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';
// Inbox Commands (Notifications)
export const CMD_SNOOZE_NOTIFICATION = 'CMD_SNOOZE_NOTIFICATION';
@@ -12,6 +12,7 @@
<script>
import 'ninja-keys';
import conversationHotKeysMixin from './conversationHotKeys';
import inboxHotKeysMixin from './inboxHotKeys';
import goToCommandHotKeys from './goToCommandHotKeys';
import appearanceHotKeys from './appearanceHotKeys';
import agentMixin from 'dashboard/mixins/agentMixin';
@@ -25,6 +26,7 @@ export default {
adminMixin,
agentMixin,
conversationHotKeysMixin,
inboxHotKeysMixin,
conversationLabelMixin,
conversationTeamMixin,
appearanceHotKeys,
@@ -42,6 +44,7 @@ export default {
},
hotKeys() {
return [
...this.inboxHotKeys,
...this.conversationHotKeys,
...this.goToCommandHotKeys,
...this.goToAppearanceHotKeys,
@@ -30,7 +30,10 @@ import {
UNMUTE_ACTION,
MUTE_ACTION,
} from './commandBarActions';
import { isAConversationRoute } from '../../../helper/routeHelpers';
import {
isAConversationRoute,
isAInboxViewRoute,
} from '../../../helper/routeHelpers';
export default {
mixins: [aiMixin],
watch: {
@@ -325,7 +328,10 @@ export default {
},
conversationHotKeys() {
if (isAConversationRoute(this.$route.name)) {
if (
isAConversationRoute(this.$route.name) ||
isAInboxViewRoute(this.$route.name)
) {
const defaultConversationHotKeys = [
...this.statusActions,
...this.conversationAdditionalActions,
@@ -0,0 +1,81 @@
import wootConstants from 'dashboard/constants/globals';
import { CMD_SNOOZE_NOTIFICATION } from './commandBarBusEvents';
import { ICON_SNOOZE_NOTIFICATION } from './CommandBarIcons';
import { isAInboxViewRoute } from 'dashboard/helper/routeHelpers';
const SNOOZE_OPTIONS = wootConstants.SNOOZE_OPTIONS;
const INBOX_SNOOZE_EVENTS = [
{
id: 'snooze_notification',
title: 'COMMAND_BAR.COMMANDS.SNOOZE_NOTIFICATION',
icon: ICON_SNOOZE_NOTIFICATION,
children: Object.values(SNOOZE_OPTIONS),
},
{
id: SNOOZE_OPTIONS.AN_HOUR_FROM_NOW,
title: 'COMMAND_BAR.COMMANDS.AN_HOUR_FROM_NOW',
parent: 'snooze_notification',
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
icon: ICON_SNOOZE_NOTIFICATION,
handler: () =>
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.AN_HOUR_FROM_NOW),
},
{
id: SNOOZE_OPTIONS.UNTIL_TOMORROW,
title: 'COMMAND_BAR.COMMANDS.UNTIL_TOMORROW',
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
parent: 'snooze_notification',
icon: ICON_SNOOZE_NOTIFICATION,
handler: () =>
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_TOMORROW),
},
{
id: SNOOZE_OPTIONS.UNTIL_NEXT_WEEK,
title: 'COMMAND_BAR.COMMANDS.UNTIL_NEXT_WEEK',
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
parent: 'snooze_notification',
icon: ICON_SNOOZE_NOTIFICATION,
handler: () =>
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_NEXT_WEEK),
},
{
id: SNOOZE_OPTIONS.UNTIL_NEXT_MONTH,
title: 'COMMAND_BAR.COMMANDS.UNTIL_NEXT_MONTH',
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
parent: 'snooze_notification',
icon: ICON_SNOOZE_NOTIFICATION,
handler: () =>
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_NEXT_MONTH),
},
{
id: SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME,
title: 'COMMAND_BAR.COMMANDS.CUSTOM',
section: 'COMMAND_BAR.SECTIONS.SNOOZE_NOTIFICATION',
parent: 'snooze_notification',
icon: ICON_SNOOZE_NOTIFICATION,
handler: () =>
bus.$emit(CMD_SNOOZE_NOTIFICATION, SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME),
},
];
export default {
computed: {
inboxHotKeys() {
if (isAInboxViewRoute(this.$route.name)) {
return this.prepareActions(INBOX_SNOOZE_EVENTS);
}
return [];
},
},
methods: {
prepareActions(actions) {
return actions.map(action => ({
...action,
title: this.$t(action.title),
section: this.$t(action.section),
}));
},
},
};
@@ -182,6 +182,7 @@ import { getCountryFlag } from 'dashboard/helper/flag';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import {
isAConversationRoute,
isAInboxViewRoute,
getConversationDashboardRoute,
} from '../../../../helper/routeHelpers';
@@ -303,6 +304,10 @@ export default {
this.$router.push({
name: getConversationDashboardRoute(this.$route.name),
});
} else if (isAInboxViewRoute(this.$route.name)) {
this.$router.push({
name: 'inbox_view',
});
} else if (this.$route.name !== 'contacts_dashboard') {
this.$router.push({
name: 'contacts_dashboard',
@@ -1,9 +1,28 @@
/* eslint arrow-body-style: 0 */
import { frontendURL } from '../../../helper/URLHelper';
const ConversationView = () => import('./ConversationView');
const InboxView = () => import('../inbox/InboxView.vue');
export default {
routes: [
{
path: frontendURL('accounts/:accountId/inbox-view'),
name: 'inbox_view',
roles: ['administrator', 'agent'],
component: InboxView,
props: () => {
return { inboxId: 0 };
},
},
{
path: frontendURL('accounts/:accountId/inbox-view/:conversation_id'),
name: 'inbox_view_conversation',
roles: ['administrator', 'agent'],
component: InboxView,
props: route => {
return { inboxId: 0, conversationId: route.params.conversation_id };
},
},
{
path: frontendURL('accounts/:accountId/dashboard'),
name: 'home',
@@ -1,7 +1,7 @@
<template>
<div class="w-full">
<div
class="my-0 py-0 px-4 grid grid-cols-6 md:grid-cols-7 lg:grid-cols-8 gap-4 border-b border-slate-100 dark:border-slate-700 sticky top-16 bg-white dark:bg-slate-900"
class="my-0 py-0 px-4 grid grid-cols-6 z-10 md:grid-cols-7 lg:grid-cols-8 gap-4 border-b border-slate-100 dark:border-slate-700 sticky top-16 bg-white dark:bg-slate-900"
:class="{ draggable: onCategoryPage }"
>
<div
@@ -1,6 +1,6 @@
<template>
<div
class="flex px-4 items-center justify-between w-full h-16 pt-2 sticky top-0 z-10 bg-white dark:bg-slate-900"
class="flex px-4 items-center justify-between w-full h-16 pt-2 sticky top-0 z-50 bg-white dark:bg-slate-900"
>
<div class="flex items-center">
<woot-sidemenu-icon />
@@ -0,0 +1,200 @@
<template>
<div
class="flex flex-col h-full w-full ltr:border-r border-slate-50 dark:border-slate-800/50"
:class="isOnExpandedLayout ? '' : 'min-w-[360px] max-w-[360px]'"
>
<inbox-list-header @filter="onFilterChange" />
<div
ref="notificationList"
class="flex flex-col w-full h-[calc(100%-56px)] overflow-x-hidden overflow-y-auto"
>
<inbox-card
v-for="notificationItem in notifications"
:key="notificationItem.id"
:notification-item="notificationItem"
@mark-notification-as-read="markNotificationAsRead"
@mark-notification-as-unread="markNotificationAsUnRead"
@delete-notification="deleteNotification"
/>
<div v-if="uiFlags.isFetching" class="text-center">
<span class="spinner mt-4 mb-4" />
</div>
<p
v-if="showEmptyState"
class="text-center text-slate-400 text-sm dark:text-slate-400 p-4 font-medium"
>
{{ $t('INBOX.LIST.NO_NOTIFICATIONS') }}
</p>
<p
v-if="showEndOfListMessage"
class="text-center text-slate-400 dark:text-slate-400 p-4"
>
{{ $t('INBOX.LIST.EOF') }}
</p>
<intersection-observer
v-if="!showEndOfList && !uiFlags.isFetching"
:options="infiniteLoaderOptions"
@observed="loadMoreNotifications"
/>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import wootConstants from 'dashboard/constants/globals';
import InboxCard from './components/InboxCard.vue';
import InboxListHeader from './components/InboxListHeader.vue';
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue';
import alertMixin from 'shared/mixins/alertMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
export default {
components: {
InboxCard,
InboxListHeader,
IntersectionObserver,
},
mixins: [alertMixin, uiSettingsMixin],
props: {
conversationId: {
type: [String, Number],
default: 0,
},
isOnExpandedLayout: {
type: Boolean,
default: false,
},
},
data() {
return {
infiniteLoaderOptions: {
root: this.$refs.notificationList,
rootMargin: '100px 0px 100px 0px',
},
page: 1,
status: '',
type: '',
sortOrder: wootConstants.INBOX_SORT_BY.NEWEST,
};
},
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
meta: 'notifications/getMeta',
uiFlags: 'notifications/getUIFlags',
notification: 'notifications/getFilteredNotifications',
}),
inboxFilters() {
return {
page: this.page,
status: this.status,
type: this.type,
sortOrder: this.sortOrder,
};
},
notifications() {
return this.notification(this.inboxFilters);
},
showEndOfList() {
return this.uiFlags.isAllNotificationsLoaded && !this.uiFlags.isFetching;
},
showEmptyState() {
return !this.uiFlags.isFetching && !this.notifications.length;
},
showEndOfListMessage() {
return this.showEndOfList && this.notifications.length;
},
},
mounted() {
this.setSavedFilter();
this.fetchNotifications();
},
methods: {
fetchNotifications() {
this.page = 1;
this.$store.dispatch('notifications/clear');
const filter = this.inboxFilters;
this.$store.dispatch('notifications/index', filter);
},
redirectToInbox() {
if (!this.conversationId) return;
if (this.$route.name === 'inbox_view') return;
this.$router.push({ name: 'inbox_view' });
},
loadMoreNotifications() {
if (this.uiFlags.isAllNotificationsLoaded) return;
this.$store.dispatch('notifications/index', {
page: this.page + 1,
status: this.status,
type: this.type,
sortOrder: this.sortOrder,
});
this.page += 1;
},
markNotificationAsRead(notification) {
this.$track(INBOX_EVENTS.MARK_NOTIFICATION_AS_READ);
const {
id,
primary_actor_id: primaryActorId,
primary_actor_type: primaryActorType,
} = notification;
this.$store
.dispatch('notifications/read', {
id,
primaryActorId,
primaryActorType,
unreadCount: this.meta.unreadCount,
})
.then(() => {
this.showAlert(this.$t('INBOX.ALERTS.MARK_AS_READ'));
});
},
markNotificationAsUnRead(notification) {
this.$track(INBOX_EVENTS.MARK_NOTIFICATION_AS_UNREAD);
this.redirectToInbox();
const { id } = notification;
this.$store
.dispatch('notifications/unread', {
id,
})
.then(() => {
this.showAlert(this.$t('INBOX.ALERTS.MARK_AS_UNREAD'));
});
},
deleteNotification(notification) {
this.$track(INBOX_EVENTS.DELETE_NOTIFICATION);
this.redirectToInbox();
this.$store
.dispatch('notifications/delete', {
notification,
unread_count: this.meta.unreadCount,
count: this.meta.count,
})
.then(() => {
this.showAlert(this.$t('INBOX.ALERTS.DELETE'));
});
},
onFilterChange(option) {
if (option.type === wootConstants.INBOX_FILTER_TYPE.STATUS) {
this.status = option.selected ? option.key : '';
}
if (option.type === wootConstants.INBOX_FILTER_TYPE.TYPE) {
this.type = option.selected ? option.key : '';
}
if (option.type === wootConstants.INBOX_FILTER_TYPE.SORT_ORDER) {
this.sortOrder = option.key;
}
this.fetchNotifications();
},
setSavedFilter() {
const { inbox_filter_by: filterBy = {} } = this.uiSettings;
const { status, type, sort_by: sortBy } = filterBy;
this.status = status;
this.type = type;
this.sortOrder = sortBy || wootConstants.INBOX_SORT_BY.NEWEST;
},
},
};
</script>
@@ -0,0 +1,217 @@
<template>
<section class="flex w-full h-full bg-white dark:bg-slate-900">
<inbox-list
v-show="showConversationList"
:conversation-id="conversationId"
:is-on-expanded-layout="isOnExpandedLayout"
/>
<div
v-if="showInboxMessageView"
class="flex flex-col h-full"
:class="isOnExpandedLayout ? 'w-full' : 'w-[calc(100%-360px)]'"
>
<inbox-item-header
:total-length="totalNotifications"
:current-index="activeNotificationIndex"
:active-notification="activeNotification"
@next="onClickNext"
@prev="onClickPrev"
/>
<conversation-box
class="h-[calc(100%-56px)]"
is-inbox-view
:inbox-id="inboxId"
:is-contact-panel-open="isContactPanelOpen"
:is-on-expanded-layout="isOnExpandedLayout"
@contact-panel-toggle="onToggleContactPanel"
/>
</div>
<div
v-if="!showInboxMessageView && !isOnExpandedLayout"
class="text-center bg-slate-25 dark:bg-slate-800 justify-center w-full h-full flex items-center"
>
<span v-if="uiFlags.isFetching" class="spinner mt-4 mb-4" />
<div v-else class="flex flex-col items-center gap-2">
<fluent-icon
icon="mail-inbox"
size="40"
class="text-slate-600 dark:text-slate-400"
/>
<span class="text-slate-500 text-sm font-medium dark:text-slate-300">
{{ $t('INBOX.LIST.NOTE') }}
</span>
</div>
</div>
</section>
</template>
<script>
import { mapGetters } from 'vuex';
import InboxList from './InboxList.vue';
import InboxItemHeader from './components/InboxItemHeader.vue';
import ConversationBox from 'dashboard/components/widgets/conversation/ConversationBox.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import wootConstants from 'dashboard/constants/globals';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
export default {
components: {
InboxList,
InboxItemHeader,
ConversationBox,
},
mixins: [uiSettingsMixin],
props: {
inboxId: {
type: [String, Number],
default: 0,
},
conversationId: {
type: [String, Number],
default: 0,
},
},
computed: {
...mapGetters({
currentAccountId: 'getCurrentAccountId',
notifications: 'notifications/getNotifications',
currentChat: 'getSelectedChat',
allConversation: 'getAllConversations',
uiFlags: 'notifications/getUIFlags',
}),
activeNotification() {
return this.notifications.find(
n => n.primary_actor.id === Number(this.conversationId)
);
},
isInboxViewEnabled() {
return this.$store.getters['accounts/isFeatureEnabledGlobally'](
this.currentAccountId,
FEATURE_FLAGS.INBOX_VIEW
);
},
showConversationList() {
return this.isOnExpandedLayout ? !this.conversationId : true;
},
isFetchingInitialData() {
return this.uiFlags.isFetching && !this.notifications.length;
},
showInboxMessageView() {
return (
Boolean(this.conversationId) &&
Boolean(this.currentChat.id) &&
!this.isFetchingInitialData
);
},
totalNotifications() {
return this.notifications?.length ?? 0;
},
activeNotificationIndex() {
const conversationId = Number(this.conversationId);
const notificationIndex = this.notifications.findIndex(
n => n.primary_actor.id === conversationId
);
return notificationIndex >= 0 ? notificationIndex + 1 : 0;
},
isOnExpandedLayout() {
const {
LAYOUT_TYPES: { CONDENSED },
} = wootConstants;
const { conversation_display_type: conversationDisplayType = CONDENSED } =
this.uiSettings;
return conversationDisplayType !== CONDENSED;
},
isContactPanelOpen() {
if (this.currentChat.id) {
const { is_contact_sidebar_open: isContactSidebarOpen } =
this.uiSettings;
return isContactSidebarOpen;
}
return false;
},
},
watch: {
conversationId: {
immediate: true,
handler() {
this.fetchConversationById();
},
},
},
mounted() {
// Open inbox view if inbox view feature is enabled, else redirect to dashboard
// TODO: Remove this code once inbox view feature is enabled for all accounts
if (!this.isInboxViewEnabled) {
this.$router.push({
name: 'home',
});
}
this.$store.dispatch('agents/get');
},
methods: {
async fetchConversationById() {
if (!this.conversationId) return;
const chat = this.findConversation();
if (!chat) {
await this.$store.dispatch('getConversation', this.conversationId);
}
this.setActiveChat();
},
setActiveChat() {
const selectedConversation = this.findConversation();
if (!selectedConversation) return;
this.$store
.dispatch('setActiveChat', { data: selectedConversation })
.then(() => {
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE);
});
},
findConversation() {
const conversationId = Number(this.conversationId);
return this.allConversation.find(c => c.id === conversationId);
},
navigateToConversation(activeIndex, direction) {
const indexOffset = direction === 'next' ? 0 : -2;
const targetNotification = this.notifications[activeIndex + indexOffset];
if (targetNotification) {
const {
id,
primary_actor_id: primaryActorId,
primary_actor_type: primaryActorType,
primary_actor: { id: conversationId, meta: { unreadCount } = {} },
notification_type: notificationType,
} = targetNotification;
this.$track(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
notificationType,
});
this.$store.dispatch('notifications/read', {
id,
primaryActorId,
primaryActorType,
unreadCount,
});
this.$router.push({
name: 'inbox_view_conversation',
params: { conversation_id: conversationId },
});
}
},
onClickNext() {
this.navigateToConversation(this.activeNotificationIndex, 'next');
},
onClickPrev() {
this.navigateToConversation(this.activeNotificationIndex, 'prev');
},
onToggleContactPanel() {
this.updateUISettings({
is_contact_sidebar_open: !this.isContactPanelOpen,
});
},
},
};
</script>
@@ -0,0 +1,236 @@
<template>
<div
role="button"
class="flex flex-col ltr:pl-5 rtl:pl-3 rtl:pr-5 ltr:pr-3 gap-2.5 py-3 w-full border-b border-slate-50 dark:border-slate-800/50 hover:bg-slate-25 dark:hover:bg-slate-800 cursor-pointer"
:class="
isInboxCardActive
? 'bg-slate-25 dark:bg-slate-800 click-animation'
: 'bg-white dark:bg-slate-900'
"
@contextmenu="openContextMenu($event)"
@click="openConversation(notificationItem)"
>
<div class="flex relative items-center justify-between w-full">
<div
v-if="isUnread"
class="absolute ltr:-left-3.5 rtl:-right-3.5 flex w-2 h-2 rounded bg-woot-500 dark:bg-woot-500"
/>
<InboxNameAndId :inbox="inbox" :conversation-id="primaryActor.id" />
<div class="flex gap-2">
<PriorityIcon :priority="primaryActor.priority" />
<StatusIcon :status="primaryActor.status" />
</div>
</div>
<div class="flex flex-row justify-between items-center w-full">
<div class="flex gap-1.5 items-center max-w-[calc(100%-70px)]">
<Thumbnail
v-if="assigneeMeta"
:src="assigneeMeta.thumbnail"
:username="assigneeMeta.name"
size="16px"
class="relative bottom-0.5"
/>
<div class="flex min-w-0">
<span
class="text-slate-800 dark:text-slate-50 text-sm overflow-hidden text-ellipsis whitespace-nowrap"
:class="isUnread ? 'font-medium' : 'font-normal'"
>
{{ pushTitle }}
</span>
</div>
</div>
<span
class="font-medium max-w-[60px] text-slate-600 dark:text-slate-300 text-xs whitespace-nowrap"
>
{{ lastActivityAt }}
</span>
</div>
<div v-if="snoozedUntilTime" class="flex items-center">
<span class="text-woot-500 dark:text-woot-500 text-xs font-medium">
{{ snoozedDisplayText }}
</span>
</div>
<inbox-context-menu
v-if="isContextMenuOpen"
:context-menu-position="contextMenuPosition"
:menu-items="menuItems"
@close="closeContextMenu"
@click="handleAction"
/>
</div>
</template>
<script>
import PriorityIcon from './PriorityIcon.vue';
import StatusIcon from './StatusIcon.vue';
import InboxNameAndId from './InboxNameAndId.vue';
import InboxContextMenu from './InboxContextMenu.vue';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import timeMixin from 'dashboard/mixins/time';
import { snoozedReopenTime } from 'dashboard/helper/snoozeHelpers';
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
export default {
components: {
PriorityIcon,
InboxContextMenu,
StatusIcon,
InboxNameAndId,
Thumbnail,
},
mixins: [timeMixin],
props: {
notificationItem: {
type: Object,
default: () => {},
},
},
data() {
return {
isContextMenuOpen: false,
contextMenuPosition: { x: null, y: null },
};
},
computed: {
primaryActor() {
return this.notificationItem?.primary_actor;
},
isInboxCardActive() {
return this.$route.params.conversation_id === this.primaryActor?.id;
},
inbox() {
return this.$store.getters['inboxes/getInbox'](
this.primaryActor.inbox_id
);
},
isUnread() {
return !this.notificationItem?.read_at;
},
meta() {
return this.primaryActor?.meta;
},
assigneeMeta() {
return this.meta?.assignee;
},
pushTitle() {
return this.$t(
`INBOX.TYPES.${this.notificationItem.notification_type.toUpperCase()}`
);
},
lastActivityAt() {
const dynamicTime = this.dynamicTime(
this.notificationItem?.last_activity_at
);
return this.shortTimestamp(dynamicTime, true);
},
menuItems() {
const items = [
{
key: 'delete',
label: this.$t('INBOX.MENU_ITEM.DELETE'),
},
];
if (!this.isUnread) {
items.push({
key: 'mark_as_unread',
label: this.$t('INBOX.MENU_ITEM.MARK_AS_UNREAD'),
});
} else {
items.push({
key: 'mark_as_read',
label: this.$t('INBOX.MENU_ITEM.MARK_AS_READ'),
});
}
return items;
},
snoozedUntilTime() {
const { snoozed_until: snoozedUntil } = this.notificationItem;
return snoozedUntil;
},
snoozedDisplayText() {
if (this.snoozedUntilTime) {
return `${this.$t('INBOX.LIST.SNOOZED_UNTIL')} ${snoozedReopenTime(
this.snoozedUntilTime
)}`;
}
return '';
},
},
unmounted() {
this.closeContextMenu();
},
methods: {
openConversation(notification) {
const {
id,
primary_actor_id: primaryActorId,
primary_actor_type: primaryActorType,
primary_actor: { id: conversationId, inbox_id: inboxId },
notification_type: notificationType,
} = notification;
if (this.$route.params.conversation_id !== conversationId) {
this.$track(INBOX_EVENTS.OPEN_CONVERSATION_VIA_INBOX, {
notificationType,
});
this.$store.dispatch('notifications/read', {
id,
primaryActorId,
primaryActorType,
unreadCount: this.meta.unreadCount,
});
this.$router.push({
name: 'inbox_view_conversation',
params: { inboxId, conversation_id: conversationId },
});
}
},
closeContextMenu() {
this.isContextMenuOpen = false;
this.contextMenuPosition = { x: null, y: null };
},
openContextMenu(e) {
this.closeContextMenu();
e.preventDefault();
this.contextMenuPosition = {
x: e.pageX || e.clientX,
y: e.pageY || e.clientY,
};
this.isContextMenuOpen = true;
},
handleAction(key) {
switch (key) {
case 'mark_as_read':
this.$emit('mark-notification-as-read', this.notificationItem);
break;
case 'mark_as_unread':
this.$emit('mark-notification-as-unread', this.notificationItem);
break;
case 'delete':
this.$emit('delete-notification', this.notificationItem);
break;
default:
}
},
},
};
</script>
<style scoped>
.click-animation {
animation: click-animation 0.3s ease-in-out;
}
@keyframes click-animation {
0% {
transform: scale(1);
}
50% {
transform: scale(0.99);
}
100% {
transform: scale(1);
}
}
</style>
@@ -0,0 +1,46 @@
<template>
<woot-context-menu
:x="contextMenuPosition.x"
:y="contextMenuPosition.y"
@close="handleClose"
>
<div
class="bg-white dark:bg-slate-900 w-40 py-1 border shadow-md border-slate-100 dark:border-slate-700/50 rounded-xl"
>
<menu-item
v-for="item in menuItems"
:key="item.key"
:label="item.label"
@click="onMenuItemClick(item.key)"
/>
</div>
</woot-context-menu>
</template>
<script>
import MenuItem from './MenuItem.vue';
export default {
components: {
MenuItem,
},
props: {
contextMenuPosition: {
type: Object,
default: () => ({}),
},
menuItems: {
type: Array,
default: () => [],
},
},
methods: {
handleClose() {
this.$emit('close');
},
onMenuItemClick(key) {
this.$emit('click', key);
this.handleClose();
},
},
};
</script>
@@ -0,0 +1,198 @@
<template>
<div
class="flex flex-col bg-white z-50 dark:bg-slate-900 w-[170px] border shadow-md border-slate-100 dark:border-slate-700/50 rounded-xl divide-y divide-slate-100 dark:divide-slate-700/50"
>
<div class="flex items-center justify-between h-11 p-3 rounded-t-lg">
<div class="flex gap-1.5">
<fluent-icon
icon="arrow-sort"
type="outline"
size="16"
class="text-slate-700 dark:text-slate-100"
/>
<span class="font-medium text-xs text-slate-800 dark:text-slate-100">
{{ $t('INBOX.DISPLAY_MENU.SORT') }}
</span>
</div>
<div class="relative">
<div
role="button"
class="border h-5 flex gap-1 rounded-md items-center pr-1.5 pl-1 py-0.5 w-[70px] justify-between border-slate-100 dark:border-slate-700/50"
@click="openSortMenu"
>
<span class="text-xs font-medium text-slate-600 dark:text-slate-300">
{{ activeSortOption }}
</span>
<fluent-icon
icon="chevron-down"
size="12"
class="text-slate-600 dark:text-slate-200"
/>
</div>
<div
v-if="showSortMenu"
class="absolute flex flex-col gap-0.5 bg-white z-60 dark:bg-slate-800 rounded-md p-0.5 top-0 w-[70px] border border-slate-100 dark:border-slate-700/50"
>
<div
v-for="option in sortOptions"
:key="option.key"
role="button"
class="flex rounded-[4px] h-5 w-full items-center justify-between p-0.5 gap-1"
:class="{
'bg-woot-50 dark:bg-woot-700/50': activeSort === option.key,
}"
@click.stop="onSortOptionClick(option)"
>
<span
class="text-xs font-medium hover:text-woot-600 dark:hover:text-woot-600"
:class="{
'text-woot-600 dark:text-woot-600': activeSort === option.key,
'text-slate-600 dark:text-slate-300': activeSort !== option.key,
}"
>
{{ option.name }}
</span>
<fluent-icon
v-if="activeSort === option.key"
icon="checkmark"
size="14"
class="text-woot-600 dark:text-woot-600"
/>
</div>
</div>
</div>
</div>
<div>
<span
class="font-medium text-xs py-4 px-3 text-slate-400 dark:text-slate-400"
>
{{ $t('INBOX.DISPLAY_MENU.DISPLAY') }}
</span>
<div
class="flex flex-col divide-y divide-slate-100 dark:divide-slate-700/50"
>
<div
v-for="option in displayOptions"
:key="option.key"
class="flex items-center px-3 py-2 gap-1.5 h-9"
>
<input
:id="option.key"
type="checkbox"
:name="option.key"
:checked="option.selected"
class="m-0 border-[1.5px] shadow border-slate-200 dark:border-slate-600 appearance-none rounded-[4px] w-4 h-4 dark:bg-slate-800 focus:ring-1 focus:ring-slate-100 dark:focus:ring-slate-700 checked:bg-woot-600 dark:checked:bg-woot-600 after:content-[''] after:text-white checked:after:content-['✓'] after:flex after:items-center after:justify-center checked:border-t checked:border-woot-700 dark:checked:border-woot-300 checked:border-b-0 checked:border-r-0 checked:border-l-0 after:text-center after:text-xs after:font-bold after:relative after:-top-[1.5px]"
@change="updateDisplayOption(option)"
/>
<label
:for="option.key"
class="text-xs font-medium text-slate-800 !ml-0 !mr-0 dark:text-slate-100"
>
{{ option.name }}
</label>
</div>
</div>
</div>
</div>
</template>
<script>
import wootConstants from 'dashboard/constants/globals';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
export default {
mixins: [uiSettingsMixin],
data() {
return {
showSortMenu: false,
displayOptions: [
{
name: this.$t('INBOX.DISPLAY_MENU.DISPLAY_OPTIONS.SNOOZED'),
key: wootConstants.INBOX_DISPLAY_BY.SNOOZED,
selected: false,
type: wootConstants.INBOX_FILTER_TYPE.STATUS,
},
{
name: this.$t('INBOX.DISPLAY_MENU.DISPLAY_OPTIONS.READ'),
key: wootConstants.INBOX_DISPLAY_BY.READ,
selected: false,
type: wootConstants.INBOX_FILTER_TYPE.TYPE,
},
],
sortOptions: [
{
name: this.$t('INBOX.DISPLAY_MENU.SORT_OPTIONS.NEWEST'),
key: wootConstants.INBOX_SORT_BY.NEWEST,
type: wootConstants.INBOX_FILTER_TYPE.SORT_ORDER,
},
{
name: this.$t('INBOX.DISPLAY_MENU.SORT_OPTIONS.OLDEST'),
key: wootConstants.INBOX_SORT_BY.OLDEST,
type: wootConstants.INBOX_FILTER_TYPE.SORT_ORDER,
},
],
activeSort: wootConstants.INBOX_SORT_BY.NEWEST,
activeDisplayFilter: {
status: '',
type: '',
},
};
},
computed: {
activeSortOption() {
return (
this.sortOptions.find(option => option.key === this.activeSort)?.name ||
''
);
},
},
mounted() {
this.setSavedFilter();
},
methods: {
updateDisplayOption(option) {
this.displayOptions.forEach(displayOption => {
if (displayOption.key === option.key) {
displayOption.selected = !option.selected;
this.activeDisplayFilter[displayOption.type] = displayOption.selected
? displayOption.key
: '';
this.saveSelectedDisplayFilter();
this.$emit('filter', option);
}
});
},
openSortMenu() {
this.showSortMenu = !this.showSortMenu;
},
onSortOptionClick(option) {
this.activeSort = option.key;
this.showSortMenu = false;
this.saveSelectedDisplayFilter();
this.$emit('filter', option);
},
saveSelectedDisplayFilter() {
this.updateUISettings({
inbox_filter_by: {
...this.activeDisplayFilter,
sort_by: this.activeSort || wootConstants.INBOX_SORT_BY.NEWEST,
},
});
},
setSavedFilter() {
const { inbox_filter_by: filterBy = {} } = this.uiSettings;
const { status, type, sort_by: sortBy } = filterBy;
this.activeSort = sortBy || wootConstants.INBOX_SORT_BY.NEWEST;
this.displayOptions.forEach(option => {
option.selected =
option.type === wootConstants.INBOX_FILTER_TYPE.STATUS
? option.key === status
: option.key === type;
this.activeDisplayFilter[option.type] = option.selected
? option.key
: '';
});
},
},
};
</script>
@@ -0,0 +1,145 @@
<template>
<div
class="flex gap-2 py-2 ltr:pl-4 rtl:pl-2 h-14 ltr:pr-2 rtl:pr-4 rtl:border-r justify-between items-center w-full border-b border-slate-50 dark:border-slate-800/50"
>
<pagination-button
v-if="totalLength > 1"
:total-length="totalLength"
:current-index="currentIndex"
@next="onClickNext"
@prev="onClickPrev"
/>
<div v-else />
<div class="flex items-center gap-2">
<woot-button
variant="hollow"
size="small"
color-scheme="secondary"
icon="snooze"
@click="openSnoozeNotificationModal"
>
{{ $t('INBOX.ACTION_HEADER.SNOOZE') }}
</woot-button>
<woot-button
icon="delete"
size="small"
color-scheme="secondary"
variant="hollow"
@click="deleteNotification"
>
{{ $t('INBOX.ACTION_HEADER.DELETE') }}
</woot-button>
</div>
<woot-modal
:show.sync="showCustomSnoozeModal"
:on-close="hideCustomSnoozeModal"
>
<custom-snooze-modal
@close="hideCustomSnoozeModal"
@choose-time="scheduleCustomSnooze"
/>
</woot-modal>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import { getUnixTime } from 'date-fns';
import { CMD_SNOOZE_NOTIFICATION } from 'dashboard/routes/dashboard/commands/commandBarBusEvents';
import wootConstants from 'dashboard/constants/globals';
import { findSnoozeTime } from 'dashboard/helper/snoozeHelpers';
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import PaginationButton from './PaginationButton.vue';
import CustomSnoozeModal from 'dashboard/components/CustomSnoozeModal.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
PaginationButton,
CustomSnoozeModal,
},
mixins: [alertMixin],
props: {
totalLength: {
type: Number,
default: 0,
},
currentIndex: {
type: Number,
default: 0,
},
activeNotification: {
type: Object,
default: null,
},
},
data() {
return {
showCustomSnoozeModal: false,
};
},
computed: {
...mapGetters({
meta: 'notifications/getMeta',
}),
},
mounted() {
bus.$on(CMD_SNOOZE_NOTIFICATION, this.onCmdSnoozeNotification);
},
destroyed() {
bus.$off(CMD_SNOOZE_NOTIFICATION, this.onCmdSnoozeNotification);
},
methods: {
openSnoozeNotificationModal() {
const ninja = document.querySelector('ninja-keys');
ninja.open({ parent: 'snooze_notification' });
},
hideCustomSnoozeModal() {
this.showCustomSnoozeModal = false;
},
snoozeNotification(snoozedUntil) {
this.$store
.dispatch('notifications/snooze', {
id: this.activeNotification?.id,
snoozedUntil,
})
.then(() => {
this.showAlert(this.$t('INBOX.ALERTS.SNOOZE'));
});
},
onCmdSnoozeNotification(snoozeType) {
if (snoozeType === wootConstants.SNOOZE_OPTIONS.UNTIL_CUSTOM_TIME) {
this.showCustomSnoozeModal = true;
} else {
const snoozedUntil = findSnoozeTime(snoozeType) || null;
this.snoozeNotification(snoozedUntil);
}
},
scheduleCustomSnooze(customSnoozeTime) {
this.showCustomSnoozeModal = false;
if (customSnoozeTime) {
const snoozedUntil = getUnixTime(customSnoozeTime) || null;
this.snoozeNotification(snoozedUntil);
}
},
deleteNotification() {
this.$track(INBOX_EVENTS.DELETE_NOTIFICATION);
this.$store
.dispatch('notifications/delete', {
notification: this.activeNotification,
unread_count: this.meta.unreadCount,
count: this.meta.count,
})
.then(() => {
this.showAlert(this.$t('INBOX.ALERTS.DELETE'));
});
this.$router.push({ name: 'inbox_view' });
},
onClickNext() {
this.$emit('next');
},
onClickPrev() {
this.$emit('prev');
},
},
};
</script>
@@ -0,0 +1,123 @@
<template>
<div
class="flex w-full ltr:pl-4 rtl:pl-2 rtl:pr-4 ltr:pr-2 py-2 h-14 justify-between items-center border-b border-slate-50 dark:border-slate-800/50"
>
<div class="flex items-center gap-1.5">
<h1 class="font-medium text-slate-900 dark:text-slate-25 text-xl">
{{ $t('INBOX.LIST.TITLE') }}
</h1>
<div class="relative">
<div
role="button"
class="flex gap-1 items-center py-1 px-2 border border-slate-100 dark:border-slate-700/50 rounded-md"
@click="openInboxDisplayMenu"
>
<span
class="text-slate-600 dark:text-slate-200 text-xs text-center font-medium"
>
{{ $t('INBOX.LIST.DISPLAY_DROPDOWN') }}
</span>
<fluent-icon
icon="chevron-down"
size="12"
class="text-slate-600 dark:text-slate-200"
/>
</div>
<inbox-display-menu
v-if="showInboxDisplayMenu"
v-on-clickaway="openInboxDisplayMenu"
class="absolute top-8"
@filter="onFilterChange"
/>
</div>
</div>
<div class="flex relative gap-1 items-center">
<!-- <woot-button
variant="clear"
size="small"
color-scheme="secondary"
icon="filter"
@click="openInboxFilter"
/> -->
<woot-button
variant="clear"
size="small"
color-scheme="secondary"
icon="mail-inbox"
@click="openInboxOptionsMenu"
/>
<inbox-option-menu
v-if="showInboxOptionMenu"
v-on-clickaway="openInboxOptionsMenu"
class="absolute top-9"
@option-click="onInboxOptionMenuClick"
/>
</div>
</div>
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import InboxOptionMenu from './InboxOptionMenu.vue';
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import InboxDisplayMenu from './InboxDisplayMenu.vue';
import alertMixin from 'shared/mixins/alertMixin';
export default {
components: {
InboxOptionMenu,
InboxDisplayMenu,
},
mixins: [clickaway, alertMixin],
data() {
return {
showInboxDisplayMenu: false,
showInboxOptionMenu: false,
};
},
methods: {
markAllRead() {
this.$track(INBOX_EVENTS.MARK_ALL_NOTIFICATIONS_AS_READ);
this.$store.dispatch('notifications/readAll').then(() => {
this.showAlert(this.$t('INBOX.ALERTS.MARK_ALL_READ'));
});
},
deleteAll() {
this.$store.dispatch('notifications/deleteAll').then(() => {
this.showAlert(this.$t('INBOX.ALERTS.DELETE_ALL'));
});
},
deleteAllRead() {
this.$store.dispatch('notifications/deleteAllRead').then(() => {
this.showAlert(this.$t('INBOX.ALERTS.DELETE_ALL_READ'));
});
},
openInboxDisplayMenu() {
this.showInboxDisplayMenu = !this.showInboxDisplayMenu;
},
openInboxOptionsMenu() {
this.showInboxOptionMenu = !this.showInboxOptionMenu;
},
onInboxOptionMenuClick(key) {
this.showInboxOptionMenu = false;
if (key === 'mark_all_read') {
this.markAllRead();
}
if (key === 'delete_all') {
this.deleteAll();
}
if (key === 'delete_all_read') {
this.deleteAllRead();
}
},
onFilterChange(option) {
this.$emit('filter', option);
this.showInboxDisplayMenu = false;
if (this.$route.name === 'inbox_view') return;
this.$router.push({ name: 'inbox_view' });
},
},
};
</script>
<style></style>
@@ -0,0 +1,44 @@
<script>
import { getInboxClassByType } from 'dashboard/helper/inbox';
export default {
props: {
inbox: {
type: Object,
default: () => {},
},
conversationId: {
type: Number,
default: 0,
},
},
computed: {
inboxIcon() {
const { phone_number: phoneNumber, channel_type: type } = this.inbox;
const classByType = getInboxClassByType(type, phoneNumber);
return classByType;
},
},
};
</script>
<template>
<div
class="inline-flex items-center rounded-[4px] border border-slate-100 dark:border-slate-700/50 divide-x divide-slate-100 dark:divide-slate-700/50 bg-none"
>
<div v-if="inbox" class="flex items-center gap-0.5 py-0.5 px-1.5">
<fluent-icon
class="text-slate-600 dark:text-slate-300"
:icon="inboxIcon"
size="14"
/>
<span class="font-medium text-slate-600 dark:text-slate-200 text-xs">
{{ inbox.name }}
</span>
</div>
<div class="flex items-center py-0.5 px-1.5">
<span class="font-medium text-slate-600 dark:text-slate-200 text-xs">
{{ conversationId }}
</span>
</div>
</div>
</template>
@@ -0,0 +1,46 @@
<template>
<div
class="flex flex-col gap-1 bg-white z-50 dark:bg-slate-900 w-40 py-1 border shadow-md border-slate-100 dark:border-slate-700/50 rounded-xl divide-y divide-slate-100 dark:divide-slate-700/50"
>
<div class="flex flex-col">
<menu-item
v-for="item in menuItems"
:key="item.key"
:label="item.label"
@click="onClick(item.key)"
/>
</div>
</div>
</template>
<script>
import MenuItem from './MenuItem.vue';
export default {
components: {
MenuItem,
},
data() {
return {
menuItems: [
{
key: 'mark_all_read',
label: this.$t('INBOX.MENU_ITEM.MARK_ALL_READ'),
},
{
key: 'delete_all',
label: this.$t('INBOX.MENU_ITEM.DELETE_ALL'),
},
{
key: 'delete_all_read',
label: this.$t('INBOX.MENU_ITEM.DELETE_ALL_READ'),
},
],
};
},
methods: {
onClick(key) {
this.$emit('option-click', key);
},
},
};
</script>
@@ -0,0 +1,25 @@
<script setup>
import { defineProps, defineEmits } from 'vue';
defineProps({
label: {
type: String,
default: '',
},
});
const emits = defineEmits(['click']);
const onMenuItemClick = () => {
emits('click');
};
</script>
<template>
<div
role="button"
class="py-1 px-2 w-full h-8 font-medium text-xs text-slate-800 dark:text-slate-100 flex items-center whitespace-nowrap text-ellipsis overflow-hidden hover:text-woot-600 dark:hover:text-woot-500 cursor-pointer rounded-md"
@click.stop="onMenuItemClick"
>
{{ label }}
</div>
</template>
@@ -0,0 +1,70 @@
<template>
<div class="flex gap-2 items-center">
<div class="flex gap-1 items-center">
<woot-button
size="tiny"
variant="hollow"
color-scheme="secondary"
icon="chevron-up"
:disabled="isUpDisabled"
@click="handleUpClick"
/>
<woot-button
size="tiny"
variant="hollow"
color-scheme="secondary"
icon="chevron-down"
:disabled="isDownDisabled"
@click="handleDownClick"
/>
</div>
<div class="flex items-center gap-1 whitespace-nowrap">
<span class="text-sm font-medium text-gray-600">
{{ totalLength <= 1 ? '1' : currentIndex }}
</span>
<span
v-if="totalLength > 1"
class="text-sm text-slate-400 relative -top-px"
>
/
</span>
<span v-if="totalLength > 1" class="text-sm text-slate-400">
{{ totalLength }}
</span>
</div>
</div>
</template>
<script>
export default {
props: {
totalLength: {
type: Number,
default: 0,
},
currentIndex: {
type: Number,
default: 0,
},
},
computed: {
isUpDisabled() {
return this.currentIndex === 1;
},
isDownDisabled() {
return this.currentIndex === this.totalLength || this.totalLength <= 1;
},
},
methods: {
handleUpClick() {
if (this.currentIndex > 1) {
this.$emit('prev');
}
},
handleDownClick() {
if (this.currentIndex < this.totalLength) {
this.$emit('next');
}
},
},
};
</script>
@@ -0,0 +1,80 @@
<script>
import { CONVERSATION_PRIORITY } from 'shared/constants/messages';
export default {
props: {
priority: {
type: String,
default: '',
},
},
data() {
return {
CONVERSATION_PRIORITY,
};
},
};
</script>
<template>
<div class="inline-flex items-center justify-center rounded-md">
<!-- High Priority -->
<svg
v-if="priority === CONVERSATION_PRIORITY.HIGH"
class="h-4 w-4"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="4" y="12" width="4" height="8" rx="1" fill="#FFC291" />
<rect x="10" y="8" width="4" height="12" rx="1" fill="#FFC291" />
<rect x="16" y="4" width="4" height="16" rx="1" fill="#FFC291" />
</svg>
<!-- Low Priority -->
<svg
v-if="priority === CONVERSATION_PRIORITY.LOW"
class="h-4 w-4"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="4" y="12" width="4" height="8" rx="1" fill="#FFC291" />
<rect x="10" y="8" width="4" height="12" rx="1" fill="#DDDDDD" />
<rect x="16" y="4" width="4" height="16" rx="1" fill="#DDDDDD" />
</svg>
<!-- Medium Priority -->
<svg
v-if="priority === CONVERSATION_PRIORITY.MEDIUM"
class="h-4 w-4"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="4" y="12" width="4" height="8" rx="1" fill="#FFC291" />
<rect x="10" y="8" width="4" height="12" rx="1" fill="#FFC291" />
<rect x="16" y="4" width="4" height="16" rx="1" fill="#DDDDDD" />
</svg>
<!-- Urgent Priority -->
<svg
v-if="priority === CONVERSATION_PRIORITY.URGENT"
class="h-4 w-4"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="4" y="12" width="4" height="8" rx="1" fill="#E5484D" />
<rect x="10" y="8" width="4" height="12" rx="1" fill="#E5484D" />
<rect x="16" y="4" width="4" height="16" rx="1" fill="#E5484D" />
</svg>
</div>
</template>
@@ -0,0 +1,86 @@
<template>
<div class="inline-flex items-center justify-center rounded-md">
<!-- Pending -->
<svg
v-if="status === CONVERSATION_STATUS.PENDING"
class="h-3.5 w-3.5"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.1 0.0449978V1.8558C4.5486 2.2986 1.8 5.328 1.8 9C1.8 12.9762 5.0238 16.2 9 16.2C10.6641 16.2 12.195 15.6357 13.4154 14.688L14.6961 15.9687C13.1445 17.2377 11.16 18 9 18C4.0293 18 0 13.9707 0 9C0 4.3335 3.5523 0.495898 8.1 0.0449978ZM17.955 9.9C17.775 11.7099 17.0604 13.3623 15.9687 14.6952L14.688 13.4154C15.462 12.4191 15.9804 11.2149 16.1442 9.9H17.9559H17.955ZM9.9018 0.0449978C14.1534 0.467098 17.5338 3.8484 17.9568 8.1H16.1451C15.7392 4.8438 13.158 2.2626 9.9018 1.8558V0.0440979V0.0449978Z"
class="fill-[#B9BBC6]"
/>
</svg>
<!-- Open -->
<svg
v-if="status === CONVERSATION_STATUS.OPEN"
class="h-3.5 w-3.5"
width="19"
height="19"
viewBox="0 0 19 19"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.375 18.875C4.19733 18.875 0 14.6776 0 9.5C0 4.32233 4.19733 0.125 9.375 0.125C14.5526 0.125 18.75 4.32233 18.75 9.5C18.75 14.6776 14.5526 18.875 9.375 18.875ZM9.375 17C13.5172 17 16.875 13.6422 16.875 9.5C16.875 5.35786 13.5172 2 9.375 2C5.23286 2 1.875 5.35786 1.875 9.5C1.875 13.6422 5.23286 17 9.375 17Z"
class="fill-[#ED8A5C]"
/>
</svg>
<!-- Snoozed -->
<svg
v-if="status === CONVERSATION_STATUS.SNOOZED"
class="h-3.5 w-3.5"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9 18C4.0293 18 0 13.9707 0 9C0 4.0293 4.0293 0 9 0C13.9707 0 18 4.0293 18 9C18 13.9707 13.9707 18 9 18ZM2.9961 12.9825C3.58766 13.8676 4.36812 14.6105 5.28129 15.1577C6.19446 15.7049 7.21761 16.0428 8.27707 16.147C9.33652 16.2513 10.4059 16.1193 11.4082 15.7606C12.4105 15.4019 13.3208 14.8254 14.0736 14.0726C14.8263 13.3198 15.4027 12.4094 15.7613 11.4071C16.12 10.4047 16.2518 9.33532 16.1475 8.27588C16.0431 7.21644 15.7052 6.19332 15.1579 5.2802C14.6106 4.36707 13.8676 3.58668 12.9825 2.9952C13.3706 4.3796 13.383 5.84237 13.0186 7.23318C12.6542 8.62399 11.926 9.89269 10.9089 10.9089C9.89277 11.9258 8.62423 12.6539 7.23359 13.0183C5.84296 13.3828 4.38037 13.3704 2.9961 12.9825Z"
class="fill-[#0B68CB]"
/>
</svg>
<!-- Resolved -->
<svg
v-if="status === CONVERSATION_STATUS.RESOLVED"
class="h-3.5 w-3.5"
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="3 3 17.92 17.92"
>
<path
d="M11.96 20.92C7.01152 20.92 3 16.9084 3 11.96C3 7.01152 7.01152 3 11.96 3C16.9084 3 20.92 7.01152 20.92 11.96C20.92 16.9084 16.9084 20.92 11.96 20.92ZM11.96 19.128C15.9188 19.128 19.128 15.9188 19.128 11.96C19.128 8.00122 15.9188 4.792 11.96 4.792C8.00122 4.792 4.792 8.00122 4.792 11.96C4.792 15.9188 8.00122 19.128 11.96 19.128Z"
class="fill-[#5BB98C]"
/>
<path
d="M11.9599 17.9333C15.2589 17.9333 17.9332 15.2589 17.9332 11.96C17.9332 8.66098 15.2589 5.98663 11.9599 5.98663C8.66092 5.98663 5.98657 8.66098 5.98657 11.96C5.98657 15.2589 8.66092 17.9333 11.9599 17.9333Z"
class="fill-[#5BB98C]"
/>
</svg>
</div>
</template>
<script>
import { CONVERSATION_STATUS } from 'shared/constants/messages';
export default {
props: {
status: {
type: String,
default: '',
},
},
data() {
return {
CONVERSATION_STATUS,
};
},
};
</script>
@@ -188,6 +188,7 @@ export default {
notificationType,
});
this.$store.dispatch('notifications/read', {
id: notification.id,
primaryActorId,
primaryActorType,
unreadCount: this.meta.unreadCount,
@@ -58,6 +58,7 @@ export default {
notificationType,
});
this.$store.dispatch('notifications/read', {
id: notification.id,
primaryActorId,
primaryActorType,
unreadCount: this.meta.unreadCount,
@@ -130,15 +130,23 @@ export default {
this.showAlert(this.$t('AGENT_MGMT.ADD.API.SUCCESS_MESSAGE'));
this.onClose();
} catch (error) {
const { response: { data: { error: errorResponse = '' } = {} } = {} } =
error;
const {
response: {
data: {
error: errorResponse = '',
attributes: attributes = [],
message: attrError = '',
} = {},
} = {},
} = error;
let errorMessage = '';
if (error.response.status === 422) {
if (error.response.status === 422 && !attributes.includes('base')) {
errorMessage = this.$t('AGENT_MGMT.ADD.API.EXIST_MESSAGE');
} else {
errorMessage = this.$t('AGENT_MGMT.ADD.API.ERROR_MESSAGE');
}
this.showAlert(errorResponse || errorMessage);
this.showAlert(errorResponse || attrError || errorMessage);
}
},
},
@@ -143,9 +143,6 @@ export default {
imap_login: this.login,
imap_password: this.password,
imap_enable_ssl: this.isSSLEnabled,
imap_inbox_synced_at: this.isIMAPEnabled
? new Date().toISOString()
: undefined,
},
};