mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
feat: Inbox list API integration (#8825)
* feat: Inbox view * feat: Bind real values * chore: code cleanup * feat: add observer * fix: Inbox icon * chore: more code cleanup * chore: Replace conversation id * chore: Minor fix * chore: Hide from side bar * chore: Fix eslint * chore: Minor fix * fix: dark mode color * chore: Minor fix * feat: Add description for each notification types * chore: remove commented code * Update InboxList.vue * Update InboxView.vue * chore: fix specs * fix: specs * Update InboxView.vue --------- Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="conversations-list-wrap flex-basis-clamp flex-shrink-0 flex-basis-custom overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
class="conversations-list-wrap flex-basis-clamp flex-shrink-0 overflow-hidden flex flex-col border-r rtl:border-r-0 rtl:border-l border-slate-50 dark:border-slate-800/50"
|
||||||
:class="{
|
:class="{
|
||||||
hide: !showConversationList,
|
hide: !showConversationList,
|
||||||
'list--full-width': isOnExpandedLayout,
|
'list--full-width': isOnExpandedLayout,
|
||||||
|
|||||||
@@ -90,9 +90,6 @@
|
|||||||
"conversation_mention": "Mention"
|
"conversation_mention": "Mention"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"INBOX_PAGE": {
|
|
||||||
"HEADER": "Inbox"
|
|
||||||
},
|
|
||||||
"NETWORK": {
|
"NETWORK": {
|
||||||
"NOTIFICATION": {
|
"NOTIFICATION": {
|
||||||
"OFFLINE": "Offline"
|
"OFFLINE": "Offline"
|
||||||
|
|||||||
17
app/javascript/dashboard/i18n/locale/en/inbox.json
Normal file
17
app/javascript/dashboard/i18n/locale/en/inbox.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"INBOX": {
|
||||||
|
"LIST": {
|
||||||
|
"TITLE": "Inbox",
|
||||||
|
"LOADING": "Fetching notifications",
|
||||||
|
"EOF": "All notifications loaded 🎉",
|
||||||
|
"404": "There are no active notifications in this group."
|
||||||
|
},
|
||||||
|
"TYPES": {
|
||||||
|
"CONVERSATION_MENTION": "You have been mentioned in a conversation",
|
||||||
|
"CONVERSATION_CREATION": "New conversation created",
|
||||||
|
"CONVERSATION_ASSIGNMENT": "A conversation has been assigned to you",
|
||||||
|
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "New message in an assigned conversation",
|
||||||
|
"PARTICIPATING_CONVERSATION_NEW_MESSAGE": "New message in a conversation you are participating in"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import settings from './settings.json';
|
|||||||
import signup from './signup.json';
|
import signup from './signup.json';
|
||||||
import teamsSettings from './teamsSettings.json';
|
import teamsSettings from './teamsSettings.json';
|
||||||
import whatsappTemplates from './whatsappTemplates.json';
|
import whatsappTemplates from './whatsappTemplates.json';
|
||||||
|
import inbox from './inbox.json';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
...advancedFilters,
|
...advancedFilters,
|
||||||
@@ -62,4 +63,5 @@ export default {
|
|||||||
...signup,
|
...signup,
|
||||||
...teamsSettings,
|
...teamsSettings,
|
||||||
...whatsappTemplates,
|
...whatsappTemplates,
|
||||||
|
...inbox,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -193,6 +193,7 @@
|
|||||||
"CURRENTLY_VIEWING_ACCOUNT": "Currently viewing:",
|
"CURRENTLY_VIEWING_ACCOUNT": "Currently viewing:",
|
||||||
"SWITCH": "Switch",
|
"SWITCH": "Switch",
|
||||||
"CONVERSATIONS": "Conversations",
|
"CONVERSATIONS": "Conversations",
|
||||||
|
"INBOX": "Inbox",
|
||||||
"ALL_CONVERSATIONS": "All Conversations",
|
"ALL_CONVERSATIONS": "All Conversations",
|
||||||
"MENTIONED_CONVERSATIONS": "Mentions",
|
"MENTIONED_CONVERSATIONS": "Mentions",
|
||||||
"PARTICIPATING_CONVERSATIONS": "Participating",
|
"PARTICIPATING_CONVERSATIONS": "Participating",
|
||||||
|
|||||||
@@ -52,19 +52,77 @@ describe('#dateFormat', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('#shortTimestamp', () => {
|
describe('#shortTimestamp', () => {
|
||||||
it('returns correct value', () => {
|
// Test cases when withAgo is false or not provided
|
||||||
|
it('returns correct value without ago', () => {
|
||||||
expect(TimeMixin.methods.shortTimestamp('less than a minute ago')).toEqual(
|
expect(TimeMixin.methods.shortTimestamp('less than a minute ago')).toEqual(
|
||||||
'now'
|
'now'
|
||||||
);
|
);
|
||||||
expect(TimeMixin.methods.shortTimestamp(' minute ago')).toEqual('m');
|
expect(TimeMixin.methods.shortTimestamp('1 minute ago')).toEqual('1m');
|
||||||
expect(TimeMixin.methods.shortTimestamp(' minutes ago')).toEqual('m');
|
expect(TimeMixin.methods.shortTimestamp('12 minutes ago')).toEqual('12m');
|
||||||
expect(TimeMixin.methods.shortTimestamp(' hour ago')).toEqual('h');
|
expect(TimeMixin.methods.shortTimestamp('a minute ago')).toEqual('1m');
|
||||||
expect(TimeMixin.methods.shortTimestamp(' hours ago')).toEqual('h');
|
expect(TimeMixin.methods.shortTimestamp('an hour ago')).toEqual('1h');
|
||||||
expect(TimeMixin.methods.shortTimestamp(' day ago')).toEqual('d');
|
expect(TimeMixin.methods.shortTimestamp('1 hour ago')).toEqual('1h');
|
||||||
expect(TimeMixin.methods.shortTimestamp(' days ago')).toEqual('d');
|
expect(TimeMixin.methods.shortTimestamp('2 hours ago')).toEqual('2h');
|
||||||
expect(TimeMixin.methods.shortTimestamp(' month ago')).toEqual('mo');
|
expect(TimeMixin.methods.shortTimestamp('1 day ago')).toEqual('1d');
|
||||||
expect(TimeMixin.methods.shortTimestamp(' months ago')).toEqual('mo');
|
expect(TimeMixin.methods.shortTimestamp('a day ago')).toEqual('1d');
|
||||||
expect(TimeMixin.methods.shortTimestamp(' year ago')).toEqual('y');
|
expect(TimeMixin.methods.shortTimestamp('3 days ago')).toEqual('3d');
|
||||||
expect(TimeMixin.methods.shortTimestamp(' years ago')).toEqual('y');
|
expect(TimeMixin.methods.shortTimestamp('a month ago')).toEqual('1mo');
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('1 month ago')).toEqual('1mo');
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('2 months ago')).toEqual('2mo');
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('a year ago')).toEqual('1y');
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('1 year ago')).toEqual('1y');
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('4 years ago')).toEqual('4y');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test cases when withAgo is true
|
||||||
|
it('returns correct value with ago', () => {
|
||||||
|
expect(
|
||||||
|
TimeMixin.methods.shortTimestamp('less than a minute ago', true)
|
||||||
|
).toEqual('now');
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('1 minute ago', true)).toEqual(
|
||||||
|
'1m ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('12 minutes ago', true)).toEqual(
|
||||||
|
'12m ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('a minute ago', true)).toEqual(
|
||||||
|
'1m ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('an hour ago', true)).toEqual(
|
||||||
|
'1h ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('1 hour ago', true)).toEqual(
|
||||||
|
'1h ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('2 hours ago', true)).toEqual(
|
||||||
|
'2h ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('1 day ago', true)).toEqual(
|
||||||
|
'1d ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('a day ago', true)).toEqual(
|
||||||
|
'1d ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('3 days ago', true)).toEqual(
|
||||||
|
'3d ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('a month ago', true)).toEqual(
|
||||||
|
'1mo ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('1 month ago', true)).toEqual(
|
||||||
|
'1mo ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('2 months ago', true)).toEqual(
|
||||||
|
'2mo ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('a year ago', true)).toEqual(
|
||||||
|
'1y ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('1 year ago', true)).toEqual(
|
||||||
|
'1y ago'
|
||||||
|
);
|
||||||
|
expect(TimeMixin.methods.shortTimestamp('4 years ago', true)).toEqual(
|
||||||
|
'4y ago'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,25 +28,36 @@ export default {
|
|||||||
const unixTime = fromUnixTime(time);
|
const unixTime = fromUnixTime(time);
|
||||||
return format(unixTime, dateFormat);
|
return format(unixTime, dateFormat);
|
||||||
},
|
},
|
||||||
shortTimestamp(time) {
|
shortTimestamp(time, withAgo = false) {
|
||||||
|
// This function takes a time string and converts it to a short time string
|
||||||
|
// with the following format: 1m, 1h, 1d, 1mo, 1y
|
||||||
|
// The function also takes an optional boolean parameter withAgo
|
||||||
|
// which will add the word "ago" to the end of the time string
|
||||||
|
const suffix = withAgo ? ' ago' : '';
|
||||||
|
const timeMappings = {
|
||||||
|
'less than a minute ago': 'now',
|
||||||
|
'a minute ago': `1m${suffix}`,
|
||||||
|
'an hour ago': `1h${suffix}`,
|
||||||
|
'a day ago': `1d${suffix}`,
|
||||||
|
'a month ago': `1mo${suffix}`,
|
||||||
|
'a year ago': `1y${suffix}`,
|
||||||
|
};
|
||||||
|
// Check if the time string is one of the specific cases
|
||||||
|
if (timeMappings[time]) {
|
||||||
|
return timeMappings[time];
|
||||||
|
}
|
||||||
const convertToShortTime = time
|
const convertToShortTime = time
|
||||||
.replace(/about|over|almost|/g, '')
|
.replace(/about|over|almost|/g, '')
|
||||||
.replace('less than a minute ago', 'now')
|
.replace(' minute ago', `m${suffix}`)
|
||||||
.replace(' minute ago', 'm')
|
.replace(' minutes ago', `m${suffix}`)
|
||||||
.replace(' minutes ago', 'm')
|
.replace(' hour ago', `h${suffix}`)
|
||||||
.replace('a minute ago', 'm')
|
.replace(' hours ago', `h${suffix}`)
|
||||||
.replace('an hour ago', 'h')
|
.replace(' day ago', `d${suffix}`)
|
||||||
.replace(' hour ago', 'h')
|
.replace(' days ago', `d${suffix}`)
|
||||||
.replace(' hours ago', 'h')
|
.replace(' month ago', `mo${suffix}`)
|
||||||
.replace(' day ago', 'd')
|
.replace(' months ago', `mo${suffix}`)
|
||||||
.replace('a day ago', 'd')
|
.replace(' year ago', `y${suffix}`)
|
||||||
.replace(' days ago', 'd')
|
.replace(' years ago', `y${suffix}`);
|
||||||
.replace('a month ago', 'mo')
|
|
||||||
.replace(' months ago', 'mo')
|
|
||||||
.replace(' month ago', 'mo')
|
|
||||||
.replace('a year ago', 'y')
|
|
||||||
.replace(' year ago', 'y')
|
|
||||||
.replace(' years ago', 'y');
|
|
||||||
return convertToShortTime;
|
return convertToShortTime;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
/* eslint arrow-body-style: 0 */
|
/* eslint arrow-body-style: 0 */
|
||||||
import { frontendURL } from '../../../helper/URLHelper';
|
import { frontendURL } from '../../../helper/URLHelper';
|
||||||
const ConversationView = () => import('./ConversationView');
|
const ConversationView = () => import('./ConversationView');
|
||||||
|
const InboxView = () => import('../inbox/InboxView.vue');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
routes: [
|
routes: [
|
||||||
|
{
|
||||||
|
path: frontendURL('accounts/:accountId/inbox'),
|
||||||
|
name: 'inbox',
|
||||||
|
roles: ['administrator', 'agent'],
|
||||||
|
component: InboxView,
|
||||||
|
props: () => {},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: frontendURL('accounts/:accountId/dashboard'),
|
path: frontendURL('accounts/:accountId/dashboard'),
|
||||||
name: 'home',
|
name: 'home',
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { routes as contactRoutes } from './contacts/routes';
|
|||||||
import { routes as notificationRoutes } from './notifications/routes';
|
import { routes as notificationRoutes } from './notifications/routes';
|
||||||
import { frontendURL } from '../../helper/URLHelper';
|
import { frontendURL } from '../../helper/URLHelper';
|
||||||
import helpcenterRoutes from './helpcenter/helpcenter.routes';
|
import helpcenterRoutes from './helpcenter/helpcenter.routes';
|
||||||
import { routes as inboxRoutes } from './inbox/routes';
|
|
||||||
|
|
||||||
const AppContainer = () => import('./Dashboard.vue');
|
const AppContainer = () => import('./Dashboard.vue');
|
||||||
const Suspended = () => import('./suspended/Index.vue');
|
const Suspended = () => import('./suspended/Index.vue');
|
||||||
@@ -22,7 +21,6 @@ export default {
|
|||||||
...contactRoutes,
|
...contactRoutes,
|
||||||
...searchRoutes,
|
...searchRoutes,
|
||||||
...notificationRoutes,
|
...notificationRoutes,
|
||||||
...inboxRoutes,
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,70 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
// import { defineProps } from 'vue';
|
|
||||||
|
|
||||||
import PriorityIcon from './components/PriorityIcon.vue';
|
|
||||||
import StatusIcon from './components/StatusIcon.vue';
|
|
||||||
import InboxNameAndId from './components/InboxNameAndId.vue';
|
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
|
||||||
|
|
||||||
// const props = defineProps({
|
|
||||||
// notificationItem: {
|
|
||||||
// type: Object,
|
|
||||||
// default: () => {},
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
const assigneeMeta = {
|
|
||||||
thumbnail: '',
|
|
||||||
name: 'Michael Johnson',
|
|
||||||
};
|
|
||||||
const { thumbnail, name } = assigneeMeta || {};
|
|
||||||
const status = 'open';
|
|
||||||
const priority = 'high';
|
|
||||||
const inboxTypeMessage = 'Mentioned by Michael';
|
|
||||||
const inboxMessage = 'What is the best way to get started?';
|
|
||||||
const inbox = {
|
|
||||||
inbox_id: 16787,
|
|
||||||
inbox_name: 'Chatwoot Support',
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
class="flex max-w-[360px] flex-col pl-5 pr-3 gap-2.5 py-3 w-full bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-500 hover:bg-slate-25 dark:hover:bg-slate-800 cursor-pointer"
|
|
||||||
>
|
|
||||||
<div class="flex relative items-center justify-between w-full">
|
|
||||||
<div
|
|
||||||
class="absolute -left-3.5 flex w-2 h-2 rounded bg-woot-500 dark:bg-woot-500"
|
|
||||||
/>
|
|
||||||
<InboxNameAndId :inbox="inbox" />
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<PriorityIcon :priority="priority" />
|
|
||||||
<StatusIcon :status="status" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-row justify-between items-center w-full">
|
|
||||||
<div class="flex gap-1.5 items-center max-w-[80%]">
|
|
||||||
<Thumbnail
|
|
||||||
v-if="assigneeMeta"
|
|
||||||
:src="thumbnail"
|
|
||||||
:username="name"
|
|
||||||
size="20px"
|
|
||||||
/>
|
|
||||||
<div class="flex min-w-0">
|
|
||||||
<span
|
|
||||||
class="font-medium text-slate-800 dark:text-slate-100 text-xs overflow-hidden text-ellipsis whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{{ inboxTypeMessage }}<span v-if="inboxTypeMessage">:</span>
|
|
||||||
<span class="font-normal">{{ inboxMessage }}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="font-medium text-slate-600 dark:text-slate-300 text-xs whitespace-nowrap"
|
|
||||||
>
|
|
||||||
10h ago
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
103
app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue
Normal file
103
app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import InboxCard from './components/InboxCard.vue';
|
||||||
|
import { ACCOUNT_EVENTS } from '../../../helper/AnalyticsHelper/events';
|
||||||
|
import IntersectionObserver from 'dashboard/components/IntersectionObserver.vue';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
InboxCard,
|
||||||
|
IntersectionObserver,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
infiniteLoaderOptions: {
|
||||||
|
root: this.$refs.notificationList,
|
||||||
|
rootMargin: '100px 0px 100px 0px',
|
||||||
|
},
|
||||||
|
page: 1,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
accountId: 'getCurrentAccountId',
|
||||||
|
meta: 'notifications/getMeta',
|
||||||
|
records: 'notifications/getNotifications',
|
||||||
|
uiFlags: 'notifications/getUIFlags',
|
||||||
|
}),
|
||||||
|
showEndOfList() {
|
||||||
|
return this.uiFlags.isAllNotificationsLoaded && !this.uiFlags.isFetching;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('notifications/clear');
|
||||||
|
this.$store.dispatch('notifications/index', { page: 1 });
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openConversation(notification) {
|
||||||
|
const {
|
||||||
|
primary_actor_id: primaryActorId,
|
||||||
|
primary_actor_type: primaryActorType,
|
||||||
|
primary_actor: { id: conversationId },
|
||||||
|
notification_type: notificationType,
|
||||||
|
} = notification;
|
||||||
|
|
||||||
|
this.$track(ACCOUNT_EVENTS.OPEN_CONVERSATION_VIA_NOTIFICATION, {
|
||||||
|
notificationType,
|
||||||
|
});
|
||||||
|
this.$store.dispatch('notifications/read', {
|
||||||
|
primaryActorId,
|
||||||
|
primaryActorType,
|
||||||
|
unreadCount: this.meta.unreadCount,
|
||||||
|
});
|
||||||
|
this.$router.push(
|
||||||
|
`/app/accounts/${this.accountId}/conversations/${conversationId}`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onMarkAllDoneClick() {
|
||||||
|
this.$track(ACCOUNT_EVENTS.MARK_AS_READ_NOTIFICATIONS);
|
||||||
|
this.$store.dispatch('notifications/readAll');
|
||||||
|
},
|
||||||
|
loadMoreNotifications() {
|
||||||
|
if (this.uiFlags.isAllNotificationsLoaded) return;
|
||||||
|
this.$store.dispatch('notifications/index', { page: this.page + 1 });
|
||||||
|
this.page += 1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex text-xl w-full pl-5 pr-3 py-2 h-14 items-center font-medium text-slate-900 dark:text-slate-25 border-b border-slate-50 dark:border-slate-800/50"
|
||||||
|
>
|
||||||
|
{{ $t('INBOX.LIST.TITLE') }}
|
||||||
|
</div>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<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 { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
InboxList,
|
||||||
|
InboxItemHeader,
|
||||||
|
ConversationBox,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
currentAccountId: 'getCurrentAccountId',
|
||||||
|
notifications: 'notifications/getNotifications',
|
||||||
|
}),
|
||||||
|
isInboxViewEnabled() {
|
||||||
|
return this.$store.getters['accounts/isFeatureEnabledGlobally'](
|
||||||
|
this.currentAccountId,
|
||||||
|
FEATURE_FLAGS.INBOX_VIEW
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</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>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
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"
|
||||||
|
@click="openConversation(notificationItem)"
|
||||||
|
>
|
||||||
|
<div class="flex relative items-center justify-between w-full">
|
||||||
|
<div
|
||||||
|
v-if="isUnread"
|
||||||
|
class="absolute -left-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="20px"
|
||||||
|
/>
|
||||||
|
<div class="flex min-w-0">
|
||||||
|
<span
|
||||||
|
class="font-medium text-slate-800 dark:text-slate-50 text-sm overflow-hidden text-ellipsis whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<span class="font-normal text-sm">
|
||||||
|
{{ pushTitle }}
|
||||||
|
</span>
|
||||||
|
</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>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import PriorityIcon from './PriorityIcon.vue';
|
||||||
|
import StatusIcon from './StatusIcon.vue';
|
||||||
|
import InboxNameAndId from './InboxNameAndId.vue';
|
||||||
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||||
|
import timeMixin from 'dashboard/mixins/time';
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
PriorityIcon,
|
||||||
|
StatusIcon,
|
||||||
|
InboxNameAndId,
|
||||||
|
Thumbnail,
|
||||||
|
},
|
||||||
|
mixins: [timeMixin],
|
||||||
|
props: {
|
||||||
|
notificationItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
primaryActor() {
|
||||||
|
return this.notificationItem?.primary_actor;
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openConversation(notification) {
|
||||||
|
this.$emit('open-conversation', notification);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,33 +1,34 @@
|
|||||||
<script setup>
|
<script>
|
||||||
import PaginationButton from './PaginationButton.vue';
|
import PaginationButton from './PaginationButton.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
export default {
|
||||||
totalLength: {
|
components: {
|
||||||
type: Number,
|
PaginationButton,
|
||||||
default: 0,
|
|
||||||
},
|
},
|
||||||
currentIndex: {
|
props: {
|
||||||
type: Number,
|
totalLength: {
|
||||||
default: 0,
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
currentIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onSnooze() {},
|
||||||
|
onDelete() {},
|
||||||
},
|
},
|
||||||
});
|
|
||||||
|
|
||||||
const onSnooze = () => {
|
|
||||||
// TODO: Implement snooze
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDelete = () => {
|
|
||||||
// TODO: Implement delete
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex gap-2 py-2 pl-4 pr-2 justify-between items-center w-full border-b border-slate-200 dark:border-slate-500"
|
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
|
<pagination-button
|
||||||
:total-length="props.totalLength"
|
:total-length="totalLength"
|
||||||
:current-index="props.currentIndex"
|
:current-index="currentIndex"
|
||||||
/>
|
/>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<woot-button
|
<woot-button
|
||||||
|
|||||||
@@ -1,33 +1,43 @@
|
|||||||
<script setup>
|
<script>
|
||||||
import { defineProps } from 'vue';
|
import { getInboxClassByType } from 'dashboard/helper/inbox';
|
||||||
|
|
||||||
const props = defineProps({
|
export default {
|
||||||
inbox: {
|
props: {
|
||||||
type: Object,
|
inbox: {
|
||||||
default: () => {},
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
conversationId: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
computed: {
|
||||||
|
inboxIcon() {
|
||||||
const { inbox } = props;
|
const { phone_number: phoneNumber, channel_type: type } = this.inbox;
|
||||||
const { inbox_id: inboxId, inbox_name: inboxName } = inbox;
|
const classByType = getInboxClassByType(type, phoneNumber);
|
||||||
|
return classByType;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="inline-flex items-center rounded-[4px] border border-slate-100 dark:border-slate-600 divide-x divide-slate-100 dark:divide-slate-600 bg-none"
|
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 class="flex items-center gap-0.5 py-0.5 px-1.5">
|
<div v-if="inbox" class="flex items-center gap-0.5 py-0.5 px-1.5">
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
class="text-slate-600 dark:text-slate-300"
|
class="text-slate-600 dark:text-slate-300"
|
||||||
icon="globe-desktop"
|
:icon="inboxIcon"
|
||||||
size="14"
|
size="14"
|
||||||
/>
|
/>
|
||||||
<span class="font-medium text-slate-600 dark:text-slate-300 text-xs">
|
<span class="font-medium text-slate-600 dark:text-slate-200 text-xs">
|
||||||
{{ inboxName }}
|
{{ inbox.name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center py-0.5 px-1.5">
|
<div class="flex items-center py-0.5 px-1.5">
|
||||||
<span class="font-medium text-slate-600 dark:text-slate-300 text-xs">
|
<span class="font-medium text-slate-600 dark:text-slate-200 text-xs">
|
||||||
{{ inboxId }}
|
{{ conversationId }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<h1>Inbox View</h1>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup></script>
|
|
||||||
@@ -1,20 +1,25 @@
|
|||||||
<script setup>
|
<script>
|
||||||
import { CONVERSATION_PRIORITY } from 'shared/constants/messages';
|
import { CONVERSATION_PRIORITY } from 'shared/constants/messages';
|
||||||
import { defineProps } from 'vue';
|
export default {
|
||||||
|
props: {
|
||||||
const props = defineProps({
|
priority: {
|
||||||
priority: {
|
type: String,
|
||||||
type: String,
|
default: '',
|
||||||
default: '',
|
},
|
||||||
},
|
},
|
||||||
});
|
data() {
|
||||||
|
return {
|
||||||
|
CONVERSATION_PRIORITY,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="inline-flex items-center justify-center rounded-md">
|
<div class="inline-flex items-center justify-center rounded-md">
|
||||||
<!-- High Priority -->
|
<!-- High Priority -->
|
||||||
<svg
|
<svg
|
||||||
v-if="props.priority === CONVERSATION_PRIORITY.HIGH"
|
v-if="priority === CONVERSATION_PRIORITY.HIGH"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
@@ -29,7 +34,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
<!-- Low Priority -->
|
<!-- Low Priority -->
|
||||||
<svg
|
<svg
|
||||||
v-if="props.priority === CONVERSATION_PRIORITY.LOW"
|
v-if="priority === CONVERSATION_PRIORITY.LOW"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
@@ -44,7 +49,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
<!-- Medium Priority -->
|
<!-- Medium Priority -->
|
||||||
<svg
|
<svg
|
||||||
v-if="props.priority === CONVERSATION_PRIORITY.MEDIUM"
|
v-if="priority === CONVERSATION_PRIORITY.MEDIUM"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
@@ -59,7 +64,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
<!-- Urgent Priority -->
|
<!-- Urgent Priority -->
|
||||||
<svg
|
<svg
|
||||||
v-if="props.priority === CONVERSATION_PRIORITY.URGENT"
|
v-if="priority === CONVERSATION_PRIORITY.URGENT"
|
||||||
class="h-4 w-4"
|
class="h-4 w-4"
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
<script setup>
|
|
||||||
import { CONVERSATION_STATUS } from 'shared/constants/messages';
|
|
||||||
import { defineProps } from 'vue';
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
status: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="inline-flex items-center justify-center rounded-md">
|
<div class="inline-flex items-center justify-center rounded-md">
|
||||||
<!-- Pending -->
|
<!-- Pending -->
|
||||||
<svg
|
<svg
|
||||||
v-if="props.status === CONVERSATION_STATUS.PENDING"
|
v-if="status === CONVERSATION_STATUS.PENDING"
|
||||||
class="h-3.5 w-3.5"
|
class="h-3.5 w-3.5"
|
||||||
width="18"
|
width="18"
|
||||||
height="18"
|
height="18"
|
||||||
@@ -29,7 +17,7 @@ const props = defineProps({
|
|||||||
</svg>
|
</svg>
|
||||||
<!-- Open -->
|
<!-- Open -->
|
||||||
<svg
|
<svg
|
||||||
v-if="props.status === CONVERSATION_STATUS.OPEN"
|
v-if="status === CONVERSATION_STATUS.OPEN"
|
||||||
class="h-3.5 w-3.5"
|
class="h-3.5 w-3.5"
|
||||||
width="19"
|
width="19"
|
||||||
height="19"
|
height="19"
|
||||||
@@ -45,7 +33,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
<!-- Snoozed -->
|
<!-- Snoozed -->
|
||||||
<svg
|
<svg
|
||||||
v-if="props.status === CONVERSATION_STATUS.SNOOZED"
|
v-if="status === CONVERSATION_STATUS.SNOOZED"
|
||||||
class="h-3.5 w-3.5"
|
class="h-3.5 w-3.5"
|
||||||
width="18"
|
width="18"
|
||||||
height="18"
|
height="18"
|
||||||
@@ -61,7 +49,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
<!-- Resolved -->
|
<!-- Resolved -->
|
||||||
<svg
|
<svg
|
||||||
v-if="props.status === CONVERSATION_STATUS.RESOLVED"
|
v-if="status === CONVERSATION_STATUS.RESOLVED"
|
||||||
class="h-3.5 w-3.5"
|
class="h-3.5 w-3.5"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -78,3 +66,21 @@ const props = defineProps({
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { CONVERSATION_STATUS } from 'shared/constants/messages';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
CONVERSATION_STATUS,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
/* eslint arrow-body-style: 0 */
|
|
||||||
import { frontendURL } from '../../../helper/URLHelper';
|
|
||||||
const SettingsWrapper = () => import('../settings/Wrapper.vue');
|
|
||||||
const InboxView = () => import('./components/InboxView.vue');
|
|
||||||
|
|
||||||
export const routes = [
|
|
||||||
{
|
|
||||||
path: frontendURL('accounts/:accountId/inbox'),
|
|
||||||
component: SettingsWrapper,
|
|
||||||
props: {
|
|
||||||
headerTitle: 'INBOX_PAGE.HEADER',
|
|
||||||
icon: 'alert',
|
|
||||||
showNewButton: false,
|
|
||||||
showSidemenuIcon: false,
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
name: 'inbox_index',
|
|
||||||
component: InboxView,
|
|
||||||
roles: ['administrator', 'agent'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -5,7 +5,6 @@ import dashboard from './dashboard/dashboard.routes';
|
|||||||
import store from '../store';
|
import store from '../store';
|
||||||
import { validateLoggedInRoutes } from '../helper/routeHelpers';
|
import { validateLoggedInRoutes } from '../helper/routeHelpers';
|
||||||
import AnalyticsHelper from '../helper/AnalyticsHelper';
|
import AnalyticsHelper from '../helper/AnalyticsHelper';
|
||||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
|
||||||
|
|
||||||
const routes = [...dashboard.routes];
|
const routes = [...dashboard.routes];
|
||||||
|
|
||||||
@@ -42,16 +41,6 @@ export const validateAuthenticateRoutePermission = (to, next, { getters }) => {
|
|||||||
return '/app/login';
|
return '/app/login';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
const isInboxViewEnabled = store.getters['accounts/isFeatureEnabledGlobally'](
|
|
||||||
user.account_id,
|
|
||||||
FEATURE_FLAGS.INBOX_VIEW
|
|
||||||
);
|
|
||||||
if (to.name === 'inbox_index' && !isInboxViewEnabled) {
|
|
||||||
return next(frontendURL(`accounts/${user.account_id}/dashboard`));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!to.name) {
|
if (!to.name) {
|
||||||
return next(frontendURL(`accounts/${user.account_id}/dashboard`));
|
return next(frontendURL(`accounts/${user.account_id}/dashboard`));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,24 @@ export const actions = {
|
|||||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false });
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
index: async ({ commit }, { page = 1 } = {}) => {
|
||||||
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: true });
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: {
|
||||||
|
data: { payload, meta },
|
||||||
|
},
|
||||||
|
} = await NotificationsAPI.get(page);
|
||||||
|
commit(types.SET_NOTIFICATIONS, payload);
|
||||||
|
commit(types.SET_NOTIFICATIONS_META, meta);
|
||||||
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false });
|
||||||
|
if (payload.length < 15) {
|
||||||
|
commit(types.SET_ALL_NOTIFICATIONS_LOADED);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
unReadCount: async ({ commit } = {}) => {
|
unReadCount: async ({ commit } = {}) => {
|
||||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: true });
|
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: true });
|
||||||
try {
|
try {
|
||||||
@@ -59,4 +77,7 @@ export const actions = {
|
|||||||
deleteNotification({ commit }, data) {
|
deleteNotification({ commit }, data) {
|
||||||
commit(types.DELETE_NOTIFICATION, data);
|
commit(types.DELETE_NOTIFICATION, data);
|
||||||
},
|
},
|
||||||
|
clear({ commit }) {
|
||||||
|
commit(types.CLEAR_NOTIFICATIONS);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const state = {
|
|||||||
isFetchingItem: false,
|
isFetchingItem: false,
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
isUpdatingUnreadCount: false,
|
isUpdatingUnreadCount: false,
|
||||||
|
isAllNotificationsLoaded: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const mutations = {
|
|||||||
},
|
},
|
||||||
[types.CLEAR_NOTIFICATIONS]: $state => {
|
[types.CLEAR_NOTIFICATIONS]: $state => {
|
||||||
Vue.set($state, 'records', {});
|
Vue.set($state, 'records', {});
|
||||||
|
Vue.set($state.uiFlags, 'isAllNotificationsLoaded', false);
|
||||||
},
|
},
|
||||||
[types.SET_NOTIFICATIONS_META]: ($state, data) => {
|
[types.SET_NOTIFICATIONS_META]: ($state, data) => {
|
||||||
const {
|
const {
|
||||||
@@ -61,4 +62,7 @@ export const mutations = {
|
|||||||
Vue.set($state.meta, 'unreadCount', unreadCount);
|
Vue.set($state.meta, 'unreadCount', unreadCount);
|
||||||
Vue.set($state.meta, 'count', count);
|
Vue.set($state.meta, 'count', count);
|
||||||
},
|
},
|
||||||
|
[types.SET_ALL_NOTIFICATIONS_LOADED]: $state => {
|
||||||
|
Vue.set($state.uiFlags, 'isAllNotificationsLoaded', true);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,6 +39,38 @@ describe('#actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#index', () => {
|
||||||
|
it('sends correct actions if API is success', async () => {
|
||||||
|
axios.get.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
data: {
|
||||||
|
payload: [{ id: 1 }],
|
||||||
|
meta: { count: 3, current_page: 1, unread_count: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await actions.index({ commit });
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: true }],
|
||||||
|
[types.SET_NOTIFICATIONS, [{ id: 1 }]],
|
||||||
|
[
|
||||||
|
types.SET_NOTIFICATIONS_META,
|
||||||
|
{ count: 3, current_page: 1, unread_count: 2 },
|
||||||
|
],
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false }],
|
||||||
|
[types.SET_ALL_NOTIFICATIONS_LOADED],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('sends correct actions if API is error', async () => {
|
||||||
|
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||||
|
await actions.index({ commit });
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: true }],
|
||||||
|
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#unReadCount', () => {
|
describe('#unReadCount', () => {
|
||||||
it('sends correct actions if API is success', async () => {
|
it('sends correct actions if API is success', async () => {
|
||||||
axios.get.mockResolvedValue({ data: 1 });
|
axios.get.mockResolvedValue({ data: 1 });
|
||||||
@@ -107,4 +139,11 @@ describe('#actions', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('clear', () => {
|
||||||
|
it('sends correct actions', async () => {
|
||||||
|
await actions.clear({ commit });
|
||||||
|
expect(commit.mock.calls).toEqual([[types.CLEAR_NOTIFICATIONS]]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ describe('#mutations', () => {
|
|||||||
|
|
||||||
describe('#CLEAR_NOTIFICATIONS', () => {
|
describe('#CLEAR_NOTIFICATIONS', () => {
|
||||||
it('clear notifications', () => {
|
it('clear notifications', () => {
|
||||||
const state = { records: { 1: { id: 1 } } };
|
const state = {
|
||||||
|
records: { 1: { id: 1 } },
|
||||||
|
uiFlags: { isAllNotificationsLoaded: true },
|
||||||
|
};
|
||||||
mutations[types.CLEAR_NOTIFICATIONS](state);
|
mutations[types.CLEAR_NOTIFICATIONS](state);
|
||||||
expect(state.records).toEqual({});
|
expect(state.records).toEqual({});
|
||||||
});
|
});
|
||||||
@@ -141,4 +144,12 @@ describe('#mutations', () => {
|
|||||||
expect(state.meta.count).toEqual(232);
|
expect(state.meta.count).toEqual(232);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#SET_ALL_NOTIFICATIONS_LOADED', () => {
|
||||||
|
it('set all notifications loaded', () => {
|
||||||
|
const state = { uiFlags: { isAllNotificationsLoaded: false } };
|
||||||
|
mutations[types.SET_ALL_NOTIFICATIONS_LOADED](state);
|
||||||
|
expect(state.uiFlags).toEqual({ isAllNotificationsLoaded: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ export default {
|
|||||||
CLEAR_NOTIFICATIONS: 'CLEAR_NOTIFICATIONS',
|
CLEAR_NOTIFICATIONS: 'CLEAR_NOTIFICATIONS',
|
||||||
EDIT_NOTIFICATIONS: 'EDIT_NOTIFICATIONS',
|
EDIT_NOTIFICATIONS: 'EDIT_NOTIFICATIONS',
|
||||||
UPDATE_NOTIFICATIONS_PRESENCE: 'UPDATE_NOTIFICATIONS_PRESENCE',
|
UPDATE_NOTIFICATIONS_PRESENCE: 'UPDATE_NOTIFICATIONS_PRESENCE',
|
||||||
|
SET_ALL_NOTIFICATIONS_LOADED: 'SET_ALL_NOTIFICATIONS_LOADED',
|
||||||
|
|
||||||
// Contact Conversation
|
// Contact Conversation
|
||||||
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
||||||
|
|||||||
@@ -125,6 +125,7 @@
|
|||||||
"location-outline": "M5.843 4.568a8.707 8.707 0 1 1 12.314 12.314l-1.187 1.174c-.875.858-2.01 1.962-3.406 3.312a2.25 2.25 0 0 1-3.128 0l-3.491-3.396c-.439-.431-.806-.794-1.102-1.09a8.707 8.707 0 0 1 0-12.314Zm11.253 1.06A7.207 7.207 0 1 0 6.904 15.822L8.39 17.29a753.98 753.98 0 0 0 3.088 3 .75.75 0 0 0 1.043 0l3.394-3.3c.47-.461.863-.85 1.18-1.168a7.207 7.207 0 0 0 0-10.192ZM12 7.999a3.002 3.002 0 1 1 0 6.004 3.002 3.002 0 0 1 0-6.003Zm0 1.5a1.501 1.501 0 1 0 0 3.004 1.501 1.501 0 0 0 0-3.003Z",
|
"location-outline": "M5.843 4.568a8.707 8.707 0 1 1 12.314 12.314l-1.187 1.174c-.875.858-2.01 1.962-3.406 3.312a2.25 2.25 0 0 1-3.128 0l-3.491-3.396c-.439-.431-.806-.794-1.102-1.09a8.707 8.707 0 0 1 0-12.314Zm11.253 1.06A7.207 7.207 0 1 0 6.904 15.822L8.39 17.29a753.98 753.98 0 0 0 3.088 3 .75.75 0 0 0 1.043 0l3.394-3.3c.47-.461.863-.85 1.18-1.168a7.207 7.207 0 0 0 0-10.192ZM12 7.999a3.002 3.002 0 1 1 0 6.004 3.002 3.002 0 0 1 0-6.003Zm0 1.5a1.501 1.501 0 1 0 0 3.004 1.501 1.501 0 0 0 0-3.003Z",
|
||||||
"lock-closed-outline": "M12 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 20 10.25v9.5A2.25 2.25 0 0 1 17.75 22H6.25A2.25 2.25 0 0 1 4 19.75v-9.5A2.25 2.25 0 0 1 6.25 8H8V6a4 4 0 0 1 4-4Zm5.75 7.5H6.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h11.5a.75.75 0 0 0 .75-.75v-9.5a.75.75 0 0 0-.75-.75Zm-5.75 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 9.5 6v2h5V6A2.5 2.5 0 0 0 12 3.5Z",
|
"lock-closed-outline": "M12 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 20 10.25v9.5A2.25 2.25 0 0 1 17.75 22H6.25A2.25 2.25 0 0 1 4 19.75v-9.5A2.25 2.25 0 0 1 6.25 8H8V6a4 4 0 0 1 4-4Zm5.75 7.5H6.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h11.5a.75.75 0 0 0 .75-.75v-9.5a.75.75 0 0 0-.75-.75Zm-5.75 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 9.5 6v2h5V6A2.5 2.5 0 0 0 12 3.5Z",
|
||||||
"lock-shield-outline": "M10 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 18 10.25V11c-.319 0-.637.11-.896.329l-.107.1c-.164.17-.33.323-.496.457L16.5 10.25a.75.75 0 0 0-.75-.75H4.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h9.888a6.024 6.024 0 0 0 1.54 1.5H4.25A2.25 2.25 0 0 1 2 19.75v-9.5A2.25 2.25 0 0 1 4.25 8H6V6a4 4 0 0 1 4-4Zm8.284 10.122c.992 1.036 2.091 1.545 3.316 1.545.193 0 .355.143.392.332l.008.084v2.501c0 2.682-1.313 4.506-3.873 5.395a.385.385 0 0 1-.253 0c-2.476-.86-3.785-2.592-3.87-5.13L14 16.585v-2.5c0-.23.18-.417.4-.417 1.223 0 2.323-.51 3.318-1.545a.389.389 0 0 1 .566 0ZM10 13.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 7.5 6v2h5V6A2.5 2.5 0 0 0 10 3.5Z",
|
"lock-shield-outline": "M10 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 18 10.25V11c-.319 0-.637.11-.896.329l-.107.1c-.164.17-.33.323-.496.457L16.5 10.25a.75.75 0 0 0-.75-.75H4.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h9.888a6.024 6.024 0 0 0 1.54 1.5H4.25A2.25 2.25 0 0 1 2 19.75v-9.5A2.25 2.25 0 0 1 4.25 8H6V6a4 4 0 0 1 4-4Zm8.284 10.122c.992 1.036 2.091 1.545 3.316 1.545.193 0 .355.143.392.332l.008.084v2.501c0 2.682-1.313 4.506-3.873 5.395a.385.385 0 0 1-.253 0c-2.476-.86-3.785-2.592-3.87-5.13L14 16.585v-2.5c0-.23.18-.417.4-.417 1.223 0 2.323-.51 3.318-1.545a.389.389 0 0 1 .566 0ZM10 13.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 7.5 6v2h5V6A2.5 2.5 0 0 0 10 3.5Z",
|
||||||
|
"mail-inbox-outline": "M6.25 3h11.5a3.25 3.25 0 0 1 3.245 3.066L21 6.25v11.5a3.25 3.25 0 0 1-3.066 3.245L17.75 21H6.25a3.25 3.25 0 0 1-3.245-3.066L3 17.75V6.25a3.25 3.25 0 0 1 3.066-3.245L6.25 3h11.5h-11.5ZM4.5 14.5v3.25a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V14.5h-3.825a3.752 3.752 0 0 1-3.475 2.995l-.2.005a3.752 3.752 0 0 1-3.632-2.812l-.043-.188H4.5v3.25v-3.25Zm13.25-10H6.25a1.75 1.75 0 0 0-1.744 1.606L4.5 6.25V13H9a.75.75 0 0 1 .743.648l.007.102a2.25 2.25 0 0 0 4.495.154l.005-.154a.75.75 0 0 1 .648-.743L15 13h4.5V6.25a1.75 1.75 0 0 0-1.607-1.744L17.75 4.5Z",
|
||||||
"mail-inbox-all-outline": "M6.25 3h11.5a3.25 3.25 0 0 1 3.245 3.066L21 6.25v11.5a3.25 3.25 0 0 1-3.066 3.245L17.75 21H6.25a3.25 3.25 0 0 1-3.245-3.066L3 17.75V6.25a3.25 3.25 0 0 1 3.066-3.245L6.25 3Zm2.075 11.5H4.5v3.25a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V14.5h-3.825a3.752 3.752 0 0 1-3.475 2.995l-.2.005a3.752 3.752 0 0 1-3.632-2.812l-.043-.188Zm9.425-10H6.25a1.75 1.75 0 0 0-1.744 1.606L4.5 6.25V13H9a.75.75 0 0 1 .743.648l.007.102a2.25 2.25 0 0 0 4.495.154l.005-.154a.75.75 0 0 1 .648-.743L15 13h4.5V6.25a1.75 1.75 0 0 0-1.607-1.744L17.75 4.5Zm-11 5h10.5a.75.75 0 0 1 .102 1.493L17.25 11H6.75a.75.75 0 0 1-.102-1.493L6.75 9.5h10.5-10.5Zm0-3h10.5a.75.75 0 0 1 .102 1.493L17.25 8H6.75a.75.75 0 0 1-.102-1.493L6.75 6.5h10.5-10.5Z",
|
"mail-inbox-all-outline": "M6.25 3h11.5a3.25 3.25 0 0 1 3.245 3.066L21 6.25v11.5a3.25 3.25 0 0 1-3.066 3.245L17.75 21H6.25a3.25 3.25 0 0 1-3.245-3.066L3 17.75V6.25a3.25 3.25 0 0 1 3.066-3.245L6.25 3Zm2.075 11.5H4.5v3.25a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V14.5h-3.825a3.752 3.752 0 0 1-3.475 2.995l-.2.005a3.752 3.752 0 0 1-3.632-2.812l-.043-.188Zm9.425-10H6.25a1.75 1.75 0 0 0-1.744 1.606L4.5 6.25V13H9a.75.75 0 0 1 .743.648l.007.102a2.25 2.25 0 0 0 4.495.154l.005-.154a.75.75 0 0 1 .648-.743L15 13h4.5V6.25a1.75 1.75 0 0 0-1.607-1.744L17.75 4.5Zm-11 5h10.5a.75.75 0 0 1 .102 1.493L17.25 11H6.75a.75.75 0 0 1-.102-1.493L6.75 9.5h10.5-10.5Zm0-3h10.5a.75.75 0 0 1 .102 1.493L17.25 8H6.75a.75.75 0 0 1-.102-1.493L6.75 6.5h10.5-10.5Z",
|
||||||
"mail-unread-outline": "M16 6.5H5.25a1.75 1.75 0 0 0-1.744 1.606l-.004.1L11 12.153l6.03-3.174a3.489 3.489 0 0 0 2.97.985v6.786a3.25 3.25 0 0 1-3.066 3.245L16.75 20H5.25a3.25 3.25 0 0 1-3.245-3.066L2 16.75v-8.5a3.25 3.25 0 0 1 3.066-3.245L5.25 5h11.087A3.487 3.487 0 0 0 16 6.5Zm2.5 3.399-7.15 3.765a.75.75 0 0 1-.603.042l-.096-.042L3.5 9.9v6.85a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V9.899ZM19.5 4a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z",
|
"mail-unread-outline": "M16 6.5H5.25a1.75 1.75 0 0 0-1.744 1.606l-.004.1L11 12.153l6.03-3.174a3.489 3.489 0 0 0 2.97.985v6.786a3.25 3.25 0 0 1-3.066 3.245L16.75 20H5.25a3.25 3.25 0 0 1-3.245-3.066L2 16.75v-8.5a3.25 3.25 0 0 1 3.066-3.245L5.25 5h11.087A3.487 3.487 0 0 0 16 6.5Zm2.5 3.399-7.15 3.765a.75.75 0 0 1-.603.042l-.096-.042L3.5 9.9v6.85a1.75 1.75 0 0 0 1.606 1.744l.144.006h11.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V9.899ZM19.5 4a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Z",
|
||||||
"mail-outline": "M5.25 4h13.5a3.25 3.25 0 0 1 3.245 3.066L22 7.25v9.5a3.25 3.25 0 0 1-3.066 3.245L18.75 20H5.25a3.25 3.25 0 0 1-3.245-3.066L2 16.75v-9.5a3.25 3.25 0 0 1 3.066-3.245L5.25 4h13.5-13.5ZM20.5 9.373l-8.15 4.29a.75.75 0 0 1-.603.043l-.096-.042L3.5 9.374v7.376a1.75 1.75 0 0 0 1.606 1.744l.144.006h13.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V9.373ZM18.75 5.5H5.25a1.75 1.75 0 0 0-1.744 1.606L3.5 7.25v.429l8.5 4.473 8.5-4.474V7.25a1.75 1.75 0 0 0-1.607-1.744L18.75 5.5Z",
|
"mail-outline": "M5.25 4h13.5a3.25 3.25 0 0 1 3.245 3.066L22 7.25v9.5a3.25 3.25 0 0 1-3.066 3.245L18.75 20H5.25a3.25 3.25 0 0 1-3.245-3.066L2 16.75v-9.5a3.25 3.25 0 0 1 3.066-3.245L5.25 4h13.5-13.5ZM20.5 9.373l-8.15 4.29a.75.75 0 0 1-.603.043l-.096-.042L3.5 9.374v7.376a1.75 1.75 0 0 0 1.606 1.744l.144.006h13.5a1.75 1.75 0 0 0 1.744-1.607l.006-.143V9.373ZM18.75 5.5H5.25a1.75 1.75 0 0 0-1.744 1.606L3.5 7.25v.429l8.5 4.473 8.5-4.474V7.25a1.75 1.75 0 0 0-1.607-1.744L18.75 5.5Z",
|
||||||
|
|||||||
Reference in New Issue
Block a user