mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-03 20:48:07 +00:00
feat: Inbox page view (#8841)
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
<conversation-header
|
<conversation-header
|
||||||
v-if="currentChat.id"
|
v-if="currentChat.id"
|
||||||
:chat="currentChat"
|
:chat="currentChat"
|
||||||
|
:is-inbox-view="isInboxView"
|
||||||
:is-contact-panel-open="isContactPanelOpen"
|
:is-contact-panel-open="isContactPanelOpen"
|
||||||
:show-back-button="isOnExpandedLayout"
|
:show-back-button="isOnExpandedLayout"
|
||||||
@contact-panel-toggle="onToggleContactPanel"
|
@contact-panel-toggle="onToggleContactPanel"
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
<messages-view
|
<messages-view
|
||||||
v-if="currentChat.id"
|
v-if="currentChat.id"
|
||||||
:inbox-id="inboxId"
|
:inbox-id="inboxId"
|
||||||
|
:is-inbox-view="isInboxView"
|
||||||
:is-contact-panel-open="isContactPanelOpen"
|
:is-contact-panel-open="isContactPanelOpen"
|
||||||
@contact-panel-toggle="onToggleContactPanel"
|
@contact-panel-toggle="onToggleContactPanel"
|
||||||
/>
|
/>
|
||||||
@@ -80,6 +82,10 @@ export default {
|
|||||||
default: '',
|
default: '',
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
isInboxView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
isContactPanelOpen: {
|
isContactPanelOpen: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import Thumbnail from '../Thumbnail.vue';
|
|||||||
import wootConstants from 'dashboard/constants/globals';
|
import wootConstants from 'dashboard/constants/globals';
|
||||||
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
|
||||||
import { conversationReopenTime } from 'dashboard/helper/snoozeHelpers';
|
import { conversationReopenTime } from 'dashboard/helper/snoozeHelpers';
|
||||||
|
import { frontendURL } from 'dashboard/helper/URLHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -109,6 +110,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
isInboxView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
@@ -123,6 +128,9 @@ export default {
|
|||||||
params: { accountId, inbox_id: inboxId, label, teamId },
|
params: { accountId, inbox_id: inboxId, label, teamId },
|
||||||
name,
|
name,
|
||||||
} = this.$route;
|
} = this.$route;
|
||||||
|
if (this.isInboxView) {
|
||||||
|
return frontendURL(`accounts/${accountId}/inbox`);
|
||||||
|
}
|
||||||
return conversationListPageURL({
|
return conversationListPageURL({
|
||||||
accountId,
|
accountId,
|
||||||
inboxId,
|
inboxId,
|
||||||
|
|||||||
@@ -12,7 +12,10 @@
|
|||||||
variant="smooth"
|
variant="smooth"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
color-scheme="secondary"
|
color-scheme="secondary"
|
||||||
class="rounded-bl-calc rtl:rotate-180 rounded-tl-calc fixed top-[9.5rem] md:top-[6.25rem] z-10 bg-white dark:bg-slate-700 border-slate-50 dark:border-slate-600 border-solid border border-r-0 box-border"
|
class="rounded-bl-calc rtl:rotate-180 rounded-tl-calc fixed z-10 bg-white dark:bg-slate-700 border-slate-50 dark:border-slate-600 border-solid border border-r-0 box-border"
|
||||||
|
:class="
|
||||||
|
isInboxView ? 'top-52 md:top-40' : 'top-[9.5rem] md:top-[6.25rem]'
|
||||||
|
"
|
||||||
:icon="isRightOrLeftIcon"
|
:icon="isRightOrLeftIcon"
|
||||||
@click="onToggleContactPanel"
|
@click="onToggleContactPanel"
|
||||||
/>
|
/>
|
||||||
@@ -142,6 +145,10 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
isInboxView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -5,7 +5,12 @@
|
|||||||
"DISPLAY_DROPDOWN": "Display",
|
"DISPLAY_DROPDOWN": "Display",
|
||||||
"LOADING": "Fetching notifications",
|
"LOADING": "Fetching notifications",
|
||||||
"EOF": "All notifications loaded 🎉",
|
"EOF": "All notifications loaded 🎉",
|
||||||
"404": "There are no active notifications in this group."
|
"404": "There are no active notifications in this group.",
|
||||||
|
"NOTE": "Notifications from all subscribed inboxes"
|
||||||
|
},
|
||||||
|
"ACTION_HEADER": {
|
||||||
|
"SNOOZE": "Snooze notification",
|
||||||
|
"DELETE": "Delete notification"
|
||||||
},
|
},
|
||||||
"TYPES": {
|
"TYPES": {
|
||||||
"CONVERSATION_MENTION": "You have been mentioned in a conversation",
|
"CONVERSATION_MENTION": "You have been mentioned in a conversation",
|
||||||
|
|||||||
@@ -10,7 +10,18 @@ export default {
|
|||||||
name: 'inbox',
|
name: 'inbox',
|
||||||
roles: ['administrator', 'agent'],
|
roles: ['administrator', 'agent'],
|
||||||
component: InboxView,
|
component: InboxView,
|
||||||
props: () => {},
|
props: () => {
|
||||||
|
return { inboxId: 0 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/inbox/: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'),
|
path: frontendURL('accounts/:accountId/dashboard'),
|
||||||
|
|||||||
@@ -1,8 +1,44 @@
|
|||||||
|
<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 />
|
||||||
|
<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 records"
|
||||||
|
: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="showEndOfList"
|
||||||
|
class="text-center text-slate-300 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>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import InboxCard from './components/InboxCard.vue';
|
import InboxCard from './components/InboxCard.vue';
|
||||||
import InboxListHeader from './components/InboxListHeader.vue';
|
import InboxListHeader from './components/InboxListHeader.vue';
|
||||||
import { INBOX_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue';
|
import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -10,6 +46,16 @@ export default {
|
|||||||
InboxListHeader,
|
InboxListHeader,
|
||||||
IntersectionObserver,
|
IntersectionObserver,
|
||||||
},
|
},
|
||||||
|
props: {
|
||||||
|
conversationId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
isOnExpandedLayout: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
infiniteLoaderOptions: {
|
infiniteLoaderOptions: {
|
||||||
@@ -30,19 +76,15 @@ export default {
|
|||||||
return this.uiFlags.isAllNotificationsLoaded && !this.uiFlags.isFetching;
|
return this.uiFlags.isAllNotificationsLoaded && !this.uiFlags.isFetching;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.dispatch('notifications/clear');
|
this.$store.dispatch('notifications/clear');
|
||||||
this.$store.dispatch('notifications/index', { page: 1 });
|
this.$store.dispatch('notifications/index', { page: 1 });
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openConversation(notification) {
|
redirectToInbox() {
|
||||||
const { notification_type: notificationType } = notification;
|
if (!this.conversationId) return;
|
||||||
this.$track(INBOX_EVENTS.OPEN_CONVERSATION_VIA_NOTIFICATION, {
|
if (this.$route.name === 'inbox') return;
|
||||||
notificationType,
|
this.$router.push({ name: 'inbox' });
|
||||||
});
|
|
||||||
|
|
||||||
this.markNotificationAsRead(notification);
|
|
||||||
},
|
},
|
||||||
onMarkAllDoneClick() {
|
onMarkAllDoneClick() {
|
||||||
this.$track(INBOX_EVENTS.MARK_ALL_NOTIFICATIONS_AS_READ);
|
this.$track(INBOX_EVENTS.MARK_ALL_NOTIFICATIONS_AS_READ);
|
||||||
@@ -69,6 +111,7 @@ export default {
|
|||||||
},
|
},
|
||||||
markNotificationAsUnRead(notification) {
|
markNotificationAsUnRead(notification) {
|
||||||
this.$track(INBOX_EVENTS.MARK_NOTIFICATION_AS_UNREAD);
|
this.$track(INBOX_EVENTS.MARK_NOTIFICATION_AS_UNREAD);
|
||||||
|
this.redirectToInbox();
|
||||||
const { id } = notification;
|
const { id } = notification;
|
||||||
this.$store.dispatch('notifications/unread', {
|
this.$store.dispatch('notifications/unread', {
|
||||||
id,
|
id,
|
||||||
@@ -76,6 +119,7 @@ export default {
|
|||||||
},
|
},
|
||||||
deleteNotification(notification) {
|
deleteNotification(notification) {
|
||||||
this.$track(INBOX_EVENTS.DELETE_NOTIFICATION);
|
this.$track(INBOX_EVENTS.DELETE_NOTIFICATION);
|
||||||
|
this.redirectToInbox();
|
||||||
this.$store.dispatch('notifications/delete', {
|
this.$store.dispatch('notifications/delete', {
|
||||||
notification,
|
notification,
|
||||||
unread_count: this.meta.unreadCount,
|
unread_count: this.meta.unreadCount,
|
||||||
@@ -85,38 +129,3 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex flex-col min-w-[360px] w-full max-w-[360px] h-full ltr:border-r border-slate-50 dark:border-slate-800/50"
|
|
||||||
>
|
|
||||||
<inbox-list-header />
|
|
||||||
<div
|
|
||||||
ref="notificationList"
|
|
||||||
class="flex flex-col w-full h-full overflow-x-hidden overflow-y-auto"
|
|
||||||
>
|
|
||||||
<inbox-card
|
|
||||||
v-for="notificationItem in records"
|
|
||||||
:key="notificationItem.id"
|
|
||||||
:notification-item="notificationItem"
|
|
||||||
@open-conversation="openConversation"
|
|
||||||
@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="showEndOfList"
|
|
||||||
class="text-center text-slate-300 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>
|
|
||||||
|
|||||||
@@ -1,9 +1,55 @@
|
|||||||
|
<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 w-full h-full">
|
||||||
|
<inbox-item-header
|
||||||
|
:total-length="totalNotifications"
|
||||||
|
:current-index="activeNotificationIndex"
|
||||||
|
@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-row items-center gap-1">
|
||||||
|
<fluent-icon
|
||||||
|
icon="mail-inbox"
|
||||||
|
size="18"
|
||||||
|
class="text-slate-700 dark:text-slate-400"
|
||||||
|
/>
|
||||||
|
<span class="text-slate-700 text-sm font-medium dark:text-slate-400">
|
||||||
|
{{ $t('INBOX.LIST.NOTE') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import InboxList from './InboxList.vue';
|
import InboxList from './InboxList.vue';
|
||||||
import InboxItemHeader from './components/InboxItemHeader.vue';
|
import InboxItemHeader from './components/InboxItemHeader.vue';
|
||||||
import ConversationBox from 'dashboard/components/widgets/conversation/ConversationBox.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 { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
|
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@@ -11,10 +57,24 @@ export default {
|
|||||||
InboxItemHeader,
|
InboxItemHeader,
|
||||||
ConversationBox,
|
ConversationBox,
|
||||||
},
|
},
|
||||||
|
mixins: [uiSettingsMixin],
|
||||||
|
props: {
|
||||||
|
inboxId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
currentAccountId: 'getCurrentAccountId',
|
currentAccountId: 'getCurrentAccountId',
|
||||||
notifications: 'notifications/getNotifications',
|
notifications: 'notifications/getNotifications',
|
||||||
|
currentChat: 'getSelectedChat',
|
||||||
|
allConversation: 'getAllConversations',
|
||||||
|
uiFlags: 'notifications/getUIFlags',
|
||||||
}),
|
}),
|
||||||
isInboxViewEnabled() {
|
isInboxViewEnabled() {
|
||||||
return this.$store.getters['accounts/isFeatureEnabledGlobally'](
|
return this.$store.getters['accounts/isFeatureEnabledGlobally'](
|
||||||
@@ -22,6 +82,53 @@ export default {
|
|||||||
FEATURE_FLAGS.INBOX_VIEW
|
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() {
|
mounted() {
|
||||||
// Open inbox view if inbox view feature is enabled, else redirect to dashboard
|
// Open inbox view if inbox view feature is enabled, else redirect to dashboard
|
||||||
@@ -32,14 +139,66 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
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 {
|
||||||
|
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', {
|
||||||
|
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>
|
</script>
|
||||||
<template>
|
|
||||||
<section class="flex w-full h-full bg-white dark:bg-slate-900">
|
|
||||||
<InboxList />
|
|
||||||
<div class="flex flex-col w-full h-full">
|
|
||||||
<InboxItemHeader :total-length="28" :current-index="1" />
|
|
||||||
<ConversationBox class="h-full" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
class="flex flex-col pl-5 pr-3 gap-2.5 py-3 w-full bg-white dark:bg-slate-900 border-b border-slate-50 dark:border-slate-800/50 hover:bg-slate-25 dark:hover:bg-slate-800 cursor-pointer"
|
class="flex flex-col pl-5 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)"
|
@contextmenu="openContextMenu($event)"
|
||||||
@click="openConversation(notificationItem)"
|
@click="openConversation(notificationItem)"
|
||||||
>
|
>
|
||||||
@@ -58,6 +63,7 @@ import InboxNameAndId from './InboxNameAndId.vue';
|
|||||||
import InboxContextMenu from './InboxContextMenu.vue';
|
import InboxContextMenu from './InboxContextMenu.vue';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
import timeMixin from 'dashboard/mixins/time';
|
import timeMixin from 'dashboard/mixins/time';
|
||||||
|
import { INBOX_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
PriorityIcon,
|
PriorityIcon,
|
||||||
@@ -83,6 +89,9 @@ export default {
|
|||||||
primaryActor() {
|
primaryActor() {
|
||||||
return this.notificationItem?.primary_actor;
|
return this.notificationItem?.primary_actor;
|
||||||
},
|
},
|
||||||
|
isInboxCardActive() {
|
||||||
|
return this.$route.params.conversation_id === this.primaryActor?.id;
|
||||||
|
},
|
||||||
inbox() {
|
inbox() {
|
||||||
return this.$store.getters['inboxes/getInbox'](
|
return this.$store.getters['inboxes/getInbox'](
|
||||||
this.primaryActor.inbox_id
|
this.primaryActor.inbox_id
|
||||||
@@ -135,7 +144,31 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openConversation(notification) {
|
openConversation(notification) {
|
||||||
this.$emit('open-conversation', 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() {
|
closeContextMenu() {
|
||||||
this.isContextMenuOpen = false;
|
this.isContextMenuOpen = false;
|
||||||
@@ -167,3 +200,19 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</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>
|
||||||
|
|||||||
@@ -1,3 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex gap-2 py-2 pl-4 h-14 pr-2 justify-between items-center w-full border-b border-slate-50 dark:border-slate-800/50"
|
||||||
|
>
|
||||||
|
<pagination-button
|
||||||
|
:total-length="totalLength"
|
||||||
|
:current-index="currentIndex"
|
||||||
|
@next="onClickNext"
|
||||||
|
@prev="onClickPrev"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<woot-button
|
||||||
|
variant="hollow"
|
||||||
|
size="small"
|
||||||
|
color-scheme="secondary"
|
||||||
|
icon="snooze"
|
||||||
|
@click="onSnooze"
|
||||||
|
>
|
||||||
|
{{ $t('INBOX.ACTION_HEADER.SNOOZE') }}
|
||||||
|
</woot-button>
|
||||||
|
<woot-button
|
||||||
|
icon="delete"
|
||||||
|
size="small"
|
||||||
|
color-scheme="secondary"
|
||||||
|
variant="hollow"
|
||||||
|
@click="onDelete"
|
||||||
|
>
|
||||||
|
{{ $t('INBOX.ACTION_HEADER.DELETE') }}
|
||||||
|
</woot-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import PaginationButton from './PaginationButton.vue';
|
import PaginationButton from './PaginationButton.vue';
|
||||||
|
|
||||||
@@ -18,37 +50,12 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
onSnooze() {},
|
onSnooze() {},
|
||||||
onDelete() {},
|
onDelete() {},
|
||||||
|
onClickNext() {
|
||||||
|
this.$emit('next');
|
||||||
|
},
|
||||||
|
onClickPrev() {
|
||||||
|
this.$emit('prev');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex gap-2 py-2 pl-4 h-14 pr-2 justify-between items-center w-full border-b border-slate-50 dark:border-slate-800/50"
|
|
||||||
>
|
|
||||||
<pagination-button
|
|
||||||
:total-length="totalLength"
|
|
||||||
:current-index="currentIndex"
|
|
||||||
/>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<woot-button
|
|
||||||
variant="hollow"
|
|
||||||
size="small"
|
|
||||||
color-scheme="secondary"
|
|
||||||
icon="snooze"
|
|
||||||
@click="onSnooze"
|
|
||||||
>
|
|
||||||
Snooze
|
|
||||||
</woot-button>
|
|
||||||
<woot-button
|
|
||||||
icon="delete"
|
|
||||||
size="small"
|
|
||||||
color-scheme="secondary"
|
|
||||||
variant="hollow"
|
|
||||||
@click="onDelete"
|
|
||||||
>
|
|
||||||
Delete notification
|
|
||||||
</woot-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|||||||
@@ -1,40 +1,3 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
totalLength: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
currentIndex: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const totalItems = ref(props.totalLength);
|
|
||||||
const currentPage = ref(Math.floor(props.currentIndex / totalItems.value) + 1);
|
|
||||||
|
|
||||||
const isUpDisabled = computed(() => currentPage.value === 1);
|
|
||||||
|
|
||||||
const isDownDisabled = computed(
|
|
||||||
() => currentPage.value === totalItems.value || totalItems.value <= 1
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleUpClick = () => {
|
|
||||||
if (currentPage.value > 1) {
|
|
||||||
currentPage.value -= 1;
|
|
||||||
// need to update it based on usage
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleDownClick = () => {
|
|
||||||
if (currentPage.value < totalItems.value) {
|
|
||||||
currentPage.value += 1;
|
|
||||||
// need to update it based on usage
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<div class="flex gap-1 items-center">
|
<div class="flex gap-1 items-center">
|
||||||
@@ -57,17 +20,51 @@ const handleDownClick = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 whitespace-nowrap">
|
<div class="flex items-center gap-1 whitespace-nowrap">
|
||||||
<span class="text-sm font-medium text-gray-600">
|
<span class="text-sm font-medium text-gray-600">
|
||||||
{{ totalItems <= 1 ? '1' : currentPage }}
|
{{ totalLength <= 1 ? '1' : currentIndex }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="totalItems > 1"
|
v-if="totalLength > 1"
|
||||||
class="text-sm text-slate-400 relative -top-px"
|
class="text-sm text-slate-400 relative -top-px"
|
||||||
>
|
>
|
||||||
/
|
/
|
||||||
</span>
|
</span>
|
||||||
<span v-if="totalItems > 1" class="text-sm text-slate-400">
|
<span v-if="totalLength > 1" class="text-sm text-slate-400">
|
||||||
{{ totalItems }}
|
{{ totalLength }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user