feat: notification center (#1612)
Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
parent
e75916d562
commit
c087e75808
23 changed files with 811 additions and 12 deletions
|
@ -1,6 +1,7 @@
|
|||
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
||||
protect_from_forgery with: :null_session
|
||||
RESULTS_PER_PAGE = 15
|
||||
|
||||
protect_from_forgery with: :null_session
|
||||
before_action :fetch_notification, only: [:update]
|
||||
before_action :set_primary_actor, only: [:read_all]
|
||||
before_action :set_current_page, only: [:index]
|
||||
|
@ -8,17 +9,18 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
|||
def index
|
||||
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
|
||||
@count = notifications.count
|
||||
@notifications = notifications.page @current_page
|
||||
@notifications = notifications.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
end
|
||||
|
||||
def read_all
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
if @primary_actor
|
||||
current_user.notifications.where(account_id: current_account.id, primary_actor: @primary_actor, read_at: nil)
|
||||
.update(read_at: DateTime.now.utc)
|
||||
.update_all(read_at: DateTime.now.utc)
|
||||
else
|
||||
current_user.notifications.where(account_id: current_account.id, read_at: nil).update(read_at: DateTime.now.utc)
|
||||
current_user.notifications.where(account_id: current_account.id, read_at: nil).update_all(read_at: DateTime.now.utc)
|
||||
end
|
||||
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
|
33
app/javascript/dashboard/api/notifications.js
Normal file
33
app/javascript/dashboard/api/notifications.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class NotificationsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('notifications', { accountScoped: true });
|
||||
}
|
||||
|
||||
get(page) {
|
||||
return axios.get(`${this.url}?page=${page}`);
|
||||
}
|
||||
|
||||
getNotifications(contactId) {
|
||||
return axios.get(`${this.url}/${contactId}/notifications`);
|
||||
}
|
||||
|
||||
getUnreadCount() {
|
||||
return axios.get(`${this.url}/unread_count`);
|
||||
}
|
||||
|
||||
read(primaryActorType, primaryActorId) {
|
||||
return axios.post(`${this.url}/read_all`, {
|
||||
primary_actor_type: primaryActorType,
|
||||
primary_actor_id: primaryActorId,
|
||||
});
|
||||
}
|
||||
|
||||
readAll() {
|
||||
return axios.post(`${this.url}/read_all`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new NotificationsAPI();
|
13
app/javascript/dashboard/api/specs/notifications.spec.js
Normal file
13
app/javascript/dashboard/api/specs/notifications.spec.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import notifications from '../notifications';
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
describe('#NotificationAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(notifications).toBeInstanceOf(ApiClient);
|
||||
expect(notifications).toHaveProperty('get');
|
||||
expect(notifications).toHaveProperty('getNotifications');
|
||||
expect(notifications).toHaveProperty('getUnreadCount');
|
||||
expect(notifications).toHaveProperty('read');
|
||||
expect(notifications).toHaveProperty('readAll');
|
||||
});
|
||||
});
|
|
@ -106,13 +106,13 @@
|
|||
font-size: $font-size-medium;
|
||||
margin-top: $space-medium;
|
||||
|
||||
>span {
|
||||
> span {
|
||||
margin-left: $space-one;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-title+ul>li>a {
|
||||
.menu-title + ul > li > a {
|
||||
@include padding($space-micro null);
|
||||
color: $medium-gray;
|
||||
line-height: $global-lineheight;
|
||||
|
@ -152,6 +152,26 @@
|
|||
margin-left: auto;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
font-size: var(--font-size-big);
|
||||
margin-bottom: auto;
|
||||
margin-left: auto;
|
||||
margin-top: auto;
|
||||
position: relative;
|
||||
|
||||
.unread-badge {
|
||||
background: var(--r-300);
|
||||
border-radius: var(--space-small);
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-micro);
|
||||
font-weight: var(--font-weight-black);
|
||||
left: var(--space-slab);
|
||||
padding: 0 var(--space-smaller);
|
||||
position: absolute;
|
||||
top: var(--space-smaller);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hamburger--menu {
|
||||
|
|
|
@ -73,6 +73,13 @@
|
|||
{{ $t(`AGENT_MGMT.AGENT_TYPES.${currentRole.toUpperCase()}`) }}
|
||||
</h5>
|
||||
</div>
|
||||
<span
|
||||
class="notifications icon ion-ios-bell"
|
||||
@click.stop="showNotification"
|
||||
>
|
||||
<span v-if="unreadCount" class="unread-badge">{{ unreadCount }}</span>
|
||||
</span>
|
||||
|
||||
<span class="current-user--options icon ion-android-more-vertical" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -206,6 +213,7 @@ export default {
|
|||
currentRole: 'getCurrentRole',
|
||||
uiFlags: 'agents/getUIFlags',
|
||||
accountLabels: 'labels/getLabelsOnSidebar',
|
||||
notificationMetadata: 'notifications/getMeta',
|
||||
}),
|
||||
currentUserAvailableName() {
|
||||
return this.currentUser.name;
|
||||
|
@ -284,10 +292,20 @@ export default {
|
|||
dashboardPath() {
|
||||
return frontendURL(`accounts/${this.accountId}/dashboard`);
|
||||
},
|
||||
unreadCount() {
|
||||
if (!this.notificationMetadata.unreadCount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.notificationMetadata.unreadCount < 100
|
||||
? this.notificationMetadata.unreadCount
|
||||
: '99+';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('labels/get');
|
||||
this.$store.dispatch('inboxes/get');
|
||||
this.$store.dispatch('notifications/unReadCount');
|
||||
},
|
||||
methods: {
|
||||
filterMenuItemsByRole(menuItems) {
|
||||
|
@ -307,6 +325,9 @@ export default {
|
|||
showOptions() {
|
||||
this.showOptionsMenu = !this.showOptionsMenu;
|
||||
},
|
||||
showNotification() {
|
||||
this.$router.push(`/app/accounts/${this.accountId}/notifications`);
|
||||
},
|
||||
changeAccount() {
|
||||
this.showAccountModal = true;
|
||||
},
|
||||
|
|
|
@ -8,11 +8,13 @@ export const getSidebarItems = accountId => ({
|
|||
'inbox_conversation',
|
||||
'conversation_through_inbox',
|
||||
'contacts_dashboard',
|
||||
'notifications_dashboard',
|
||||
'settings_account_reports',
|
||||
'profile_settings',
|
||||
'profile_settings_index',
|
||||
'label_conversations',
|
||||
'conversations_through_label',
|
||||
'notifications_index',
|
||||
],
|
||||
menuItems: {
|
||||
assignedToMe: {
|
||||
|
@ -31,6 +33,13 @@ export const getSidebarItems = accountId => ({
|
|||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||
toStateName: 'contacts_dashboard',
|
||||
},
|
||||
notifications: {
|
||||
icon: 'ion-ios-bell',
|
||||
label: 'NOTIFICATIONS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/notifications`),
|
||||
toStateName: 'notifications_dashboard',
|
||||
},
|
||||
report: {
|
||||
icon: 'ion-arrow-graph-up-right',
|
||||
label: 'REPORTS',
|
||||
|
|
|
@ -51,5 +51,24 @@
|
|||
"ENTER_TO_REMOVE": "Press enter to remove",
|
||||
"SELECT_ONE": "Select one"
|
||||
}
|
||||
},
|
||||
"NOTIFICATIONS_PAGE": {
|
||||
"HEADER": "Notifications",
|
||||
"MARK_ALL_DONE": "Mark All Done",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading notifications...",
|
||||
"404": "No Notifications",
|
||||
"TABLE_HEADER": [
|
||||
"Name",
|
||||
"Phone Number",
|
||||
"Conversations",
|
||||
"Last Contacted"
|
||||
]
|
||||
},
|
||||
"TYPE_LABEL": {
|
||||
"conversation_creation": "New conversation",
|
||||
"conversation_assignment": "Conversation Assigned",
|
||||
"assigned_conversation_new_message": "New Message"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,6 +118,7 @@
|
|||
"HOME": "Home",
|
||||
"AGENTS": "Agents",
|
||||
"INBOXES": "Inboxes",
|
||||
"NOTIFICATIONS": "Notifications",
|
||||
"CANNED_RESPONSES": "Canned Responses",
|
||||
"INTEGRATIONS": "Integrations",
|
||||
"ACCOUNT_SETTINGS": "Account Settings",
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
:on-click-contact="openContactInfoPanel"
|
||||
:active-contact-id="selectedContactId"
|
||||
/>
|
||||
<contacts-footer
|
||||
<table-footer
|
||||
:on-page-change="onPageChange"
|
||||
:current-page="Number(meta.currentPage)"
|
||||
:total-count="meta.count"
|
||||
|
@ -34,13 +34,13 @@ import { mapGetters } from 'vuex';
|
|||
import ContactsHeader from './Header';
|
||||
import ContactsTable from './ContactsTable';
|
||||
import ContactInfoPanel from './ContactInfoPanel';
|
||||
import ContactsFooter from './Footer';
|
||||
import TableFooter from 'dashboard/components/widgets/TableFooter';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContactsHeader,
|
||||
ContactsTable,
|
||||
ContactsFooter,
|
||||
TableFooter,
|
||||
ContactInfoPanel,
|
||||
},
|
||||
data() {
|
||||
|
|
|
@ -2,6 +2,7 @@ import AppContainer from './Dashboard';
|
|||
import settings from './settings/settings.routes';
|
||||
import conversation from './conversation/conversation.routes';
|
||||
import { routes as contactRoutes } from './contacts/routes';
|
||||
import { routes as notificationRoutes } from './notifications/routes';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
|
||||
export default {
|
||||
|
@ -9,7 +10,12 @@ export default {
|
|||
{
|
||||
path: frontendURL('accounts/:account_id'),
|
||||
component: AppContainer,
|
||||
children: [...conversation.routes, ...settings.routes, ...contactRoutes],
|
||||
children: [
|
||||
...conversation.routes,
|
||||
...settings.routes,
|
||||
...contactRoutes,
|
||||
...notificationRoutes,
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
<template>
|
||||
<section class="notification--table-wrap">
|
||||
<woot-submit-button
|
||||
v-if="notificationMetadata.unreadCount"
|
||||
class="button nice success button--fixed-right-top"
|
||||
:button-text="$t('NOTIFICATIONS_PAGE.MARK_ALL_DONE')"
|
||||
:loading="isUpdating"
|
||||
@click="onMarkAllDoneClick"
|
||||
>
|
||||
</woot-submit-button>
|
||||
|
||||
<table class="woot-table notifications-table">
|
||||
<tbody v-show="!isLoading">
|
||||
<tr
|
||||
v-for="notificationItem in notifications"
|
||||
:key="notificationItem.id"
|
||||
@click="() => onClickNotification(notificationItem)"
|
||||
>
|
||||
<td>
|
||||
<div class="notification--thumbnail">
|
||||
<thumbnail
|
||||
:src="notificationItem.primary_actor.meta.sender.thumbnail"
|
||||
size="36px"
|
||||
:username="notificationItem.primary_actor.meta.sender.name"
|
||||
:status="
|
||||
notificationItem.primary_actor.meta.sender.availability_status
|
||||
"
|
||||
/>
|
||||
<div>
|
||||
<h4 class="notification--name">
|
||||
{{ `#${notificationItem.id}` }}
|
||||
</h4>
|
||||
<p class="notification--title">
|
||||
{{ notificationItem.push_message_title }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="label">
|
||||
{{
|
||||
$t(
|
||||
`NOTIFICATIONS_PAGE.TYPE_LABEL.${notificationItem.notification_type}`
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{{ dynamicTime(notificationItem.created_at) }}
|
||||
</td>
|
||||
<td>
|
||||
<div
|
||||
v-if="!notificationItem.read_at"
|
||||
class="notification--unread-indicator"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<empty-state
|
||||
v-if="showEmptyResult"
|
||||
:title="$t('NOTIFICATIONS_PAGE.LIST.404')"
|
||||
/>
|
||||
<div v-if="isLoading" class="notifications--loader">
|
||||
<spinner />
|
||||
<span>{{ $t('NOTIFICATIONS_PAGE.LIST.LOADING_MESSAGE') }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import timeMixin from '../../../../mixins/time';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
Spinner,
|
||||
EmptyState,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notifications: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isUpdating: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
onClickNotification: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
onMarkAllDoneClick: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
notificationMetadata: 'notifications/getMeta',
|
||||
}),
|
||||
showEmptyResult() {
|
||||
return !this.isLoading && this.notifications.length === 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/mixins';
|
||||
|
||||
.notification--name {
|
||||
font-size: var(--font-size-small);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.notification--title {
|
||||
font-size: var(--font-size-mini);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notification--table-wrap {
|
||||
@include scroll-on-hover;
|
||||
flex: 1 1;
|
||||
height: 100%;
|
||||
padding: var(--space-normal);
|
||||
}
|
||||
|
||||
.notifications-table {
|
||||
> tbody {
|
||||
> tr {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: var(--b-50);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background: var(--b-100);
|
||||
}
|
||||
|
||||
> td {
|
||||
&.conversation-count-item {
|
||||
padding-left: var(--space-medium);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.notification--thumbnail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.user-thumbnail-box {
|
||||
margin-right: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notifications--loader {
|
||||
font-size: var(--font-size-default);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--space-big);
|
||||
}
|
||||
|
||||
.notification--unread-indicator {
|
||||
width: var(--space-one);
|
||||
height: var(--space-one);
|
||||
border-radius: 50%;
|
||||
background: var(--color-woot);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<div class="columns notification--page">
|
||||
<div class="notification--content medium-12">
|
||||
<notification-table
|
||||
:notifications="records"
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:is-updating="uiFlags.isUpdating"
|
||||
:on-click-notification="openConversation"
|
||||
:on-mark-all-done-click="onMarkAllDoneClick"
|
||||
/>
|
||||
<table-footer
|
||||
:on-page-change="onPageChange"
|
||||
:current-page="Number(meta.currentPage)"
|
||||
:total-count="meta.count"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import TableFooter from 'dashboard/components/widgets/TableFooter';
|
||||
|
||||
import NotificationTable from './NotificationTable';
|
||||
export default {
|
||||
components: {
|
||||
NotificationTable,
|
||||
TableFooter,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
meta: 'notifications/getMeta',
|
||||
records: 'notifications/getNotifications',
|
||||
uiFlags: 'notifications/getUIFlags',
|
||||
}),
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('notifications/get', { page: 1 });
|
||||
},
|
||||
methods: {
|
||||
onPageChange(page) {
|
||||
window.history.pushState({}, null, `${this.$route.path}?page=${page}`);
|
||||
this.$store.dispatch('notifications/get', { page });
|
||||
},
|
||||
openConversation(notification) {
|
||||
const {
|
||||
primary_actor_id: primaryActorId,
|
||||
primary_actor_type: primaryActorType,
|
||||
primary_actor: { id: conversationId },
|
||||
} = notification;
|
||||
|
||||
this.$store.dispatch('notifications/read', {
|
||||
primaryActorId,
|
||||
primaryActorType,
|
||||
unreadCount: this.meta.unreadCount,
|
||||
});
|
||||
|
||||
this.$router.push(
|
||||
`/app/accounts/${this.accountId}/conversations/${conversationId}`
|
||||
);
|
||||
},
|
||||
onMarkAllDoneClick() {
|
||||
this.$store.dispatch('notifications/readAll');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notification--page {
|
||||
background: var(--white);
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.notification--content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,24 @@
|
|||
/* eslint arrow-body-style: 0 */
|
||||
import NotificationsView from './components/NotificationsView.vue';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import SettingsWrapper from '../settings/Wrapper';
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/notifications'),
|
||||
component: SettingsWrapper,
|
||||
props: {
|
||||
headerTitle: 'NOTIFICATIONS_PAGE.HEADER',
|
||||
icon: 'ion-ios-bell',
|
||||
showNewButton: false,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'notifications_index',
|
||||
component: NotificationsView,
|
||||
roles: ['administrator', 'agent'],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
|
@ -7,6 +7,7 @@ import auth from './modules/auth';
|
|||
import cannedResponse from './modules/cannedResponse';
|
||||
import contactConversations from './modules/contactConversations';
|
||||
import contacts from './modules/contacts';
|
||||
import notifications from './modules/notifications';
|
||||
import conversationLabels from './modules/conversationLabels';
|
||||
import conversationMetadata from './modules/conversationMetadata';
|
||||
import conversationPage from './modules/conversationPage';
|
||||
|
@ -32,6 +33,7 @@ export default new Vuex.Store({
|
|||
cannedResponse,
|
||||
contactConversations,
|
||||
contacts,
|
||||
notifications,
|
||||
conversationLabels,
|
||||
conversationMetadata,
|
||||
conversationPage,
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import types from '../../mutation-types';
|
||||
import NotificationsAPI from '../../../api/notifications';
|
||||
|
||||
export const actions = {
|
||||
get: async ({ commit }, { page = 1 } = {}) => {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: true });
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
data: { payload, meta },
|
||||
},
|
||||
} = await NotificationsAPI.get(page);
|
||||
commit(types.CLEAR_NOTIFICATIONS);
|
||||
commit(types.SET_NOTIFICATIONS, payload);
|
||||
commit(types.SET_NOTIFICATIONS_META, meta);
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false });
|
||||
}
|
||||
},
|
||||
unReadCount: async ({ commit } = {}) => {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: true });
|
||||
try {
|
||||
const { data } = await NotificationsAPI.getUnreadCount();
|
||||
commit(types.SET_NOTIFICATIONS_UNREAD_COUNT, data);
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: false });
|
||||
}
|
||||
},
|
||||
read: async (
|
||||
{ commit },
|
||||
{ primaryActorType, primaryActorId, unreadCount }
|
||||
) => {
|
||||
try {
|
||||
await NotificationsAPI.read(primaryActorType, primaryActorId);
|
||||
commit(types.SET_NOTIFICATIONS_UNREAD_COUNT, unreadCount - 1);
|
||||
commit(types.UPDATE_NOTIFICATION, primaryActorId);
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
readAll: async ({ commit }) => {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: true });
|
||||
try {
|
||||
await NotificationsAPI.readAll();
|
||||
commit(types.SET_NOTIFICATIONS_UNREAD_COUNT, 0);
|
||||
commit(types.UPDATE_ALL_NOTIFICATIONS);
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false });
|
||||
} catch (error) {
|
||||
commit(types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false });
|
||||
throw new Error(error);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
export const getters = {
|
||||
getNotifications($state) {
|
||||
return Object.values($state.records).sort((n1, n2) => n2.id - n1.id);
|
||||
},
|
||||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
},
|
||||
getNotification: $state => id => {
|
||||
const notification = $state.records[id];
|
||||
return notification || {};
|
||||
},
|
||||
getMeta: $state => {
|
||||
return $state.meta;
|
||||
},
|
||||
};
|
|
@ -0,0 +1,26 @@
|
|||
import { getters } from './getters';
|
||||
import { actions } from './actions';
|
||||
import { mutations } from './mutations';
|
||||
|
||||
const state = {
|
||||
meta: {
|
||||
count: 0,
|
||||
currentPage: 1,
|
||||
unReadCount: 0,
|
||||
},
|
||||
records: {},
|
||||
uiFlags: {
|
||||
isFetching: false,
|
||||
isFetchingItem: false,
|
||||
isUpdating: false,
|
||||
isUpdatingUnreadCount: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations,
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
import Vue from 'vue';
|
||||
import types from '../../mutation-types';
|
||||
|
||||
export const mutations = {
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG]($state, data) {
|
||||
$state.uiFlags = {
|
||||
...$state.uiFlags,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
[types.CLEAR_NOTIFICATIONS]: $state => {
|
||||
Vue.set($state, 'records', {});
|
||||
},
|
||||
[types.SET_NOTIFICATIONS_META]: ($state, data) => {
|
||||
const {
|
||||
count,
|
||||
current_page: currentPage,
|
||||
unread_count: unreadCount,
|
||||
} = data;
|
||||
|
||||
Vue.set($state.meta, 'count', count);
|
||||
Vue.set($state.meta, 'currentPage', currentPage);
|
||||
Vue.set($state.meta, 'unreadCount', unreadCount);
|
||||
},
|
||||
[types.SET_NOTIFICATIONS_UNREAD_COUNT]: ($state, count) => {
|
||||
Vue.set($state.meta, 'unreadCount', count);
|
||||
},
|
||||
[types.SET_NOTIFICATIONS]: ($state, data) => {
|
||||
data.forEach(notification => {
|
||||
Vue.set($state.records, notification.id, {
|
||||
...($state.records[notification.id] || {}),
|
||||
...notification,
|
||||
});
|
||||
});
|
||||
},
|
||||
[types.UPDATE_NOTIFICATION]: ($state, primaryActorId) => {
|
||||
Object.values($state.records).forEach(item => {
|
||||
if (item.primary_actor_id === primaryActorId) {
|
||||
Vue.set($state.records[item.id], 'read_at', true);
|
||||
}
|
||||
});
|
||||
},
|
||||
[types.UPDATE_ALL_NOTIFICATIONS]: $state => {
|
||||
Object.values($state.records).forEach(item => {
|
||||
Vue.set($state.records[item.id], 'read_at', true);
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,93 @@
|
|||
import axios from 'axios';
|
||||
import { actions } from '../../notifications/actions';
|
||||
import types from '../../../mutation-types';
|
||||
|
||||
const commit = jest.fn();
|
||||
global.axios = axios;
|
||||
jest.mock('axios');
|
||||
|
||||
describe('#actions', () => {
|
||||
describe('#get', () => {
|
||||
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.get({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: true }],
|
||||
[types.CLEAR_NOTIFICATIONS],
|
||||
[types.SET_NOTIFICATIONS, [{ id: 1 }]],
|
||||
[
|
||||
types.SET_NOTIFICATIONS_META,
|
||||
{ count: 3, current_page: 1, unread_count: 2 },
|
||||
],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.get({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: true }],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isFetching: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#unReadCount', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.get.mockResolvedValue({ data: 1 });
|
||||
await actions.unReadCount({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: true }],
|
||||
[types.SET_NOTIFICATIONS_UNREAD_COUNT, 1],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.get.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await actions.unReadCount({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: true }],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdatingUnreadCount: false }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#read', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.post.mockResolvedValue({});
|
||||
await actions.read({ commit }, { unreadCount: 2, primaryActorId: 1 });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UNREAD_COUNT, 1],
|
||||
[types.UPDATE_NOTIFICATION, 1],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(actions.read({ commit })).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#readAll', () => {
|
||||
it('sends correct actions if API is success', async () => {
|
||||
axios.post.mockResolvedValue({ data: 1 });
|
||||
await actions.readAll({ commit });
|
||||
expect(commit.mock.calls).toEqual([
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: true }],
|
||||
[types.SET_NOTIFICATIONS_UNREAD_COUNT, 0],
|
||||
[types.UPDATE_ALL_NOTIFICATIONS],
|
||||
[types.SET_NOTIFICATIONS_UI_FLAG, { isUpdating: false }],
|
||||
]);
|
||||
});
|
||||
it('sends correct actions if API is error', async () => {
|
||||
axios.post.mockRejectedValue({ message: 'Incorrect header' });
|
||||
await expect(actions.readAll({ commit })).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
import { getters } from '../../notifications/getters';
|
||||
|
||||
describe('#getters', () => {
|
||||
it('getNotifications', () => {
|
||||
const state = {
|
||||
records: {
|
||||
1: { id: 1 },
|
||||
2: { id: 2 },
|
||||
3: { id: 3 },
|
||||
},
|
||||
};
|
||||
expect(getters.getNotifications(state)).toEqual([
|
||||
{ id: 3 },
|
||||
{ id: 2 },
|
||||
{ id: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('getUIFlags', () => {
|
||||
const state = {
|
||||
uiFlags: {
|
||||
isFetching: true,
|
||||
},
|
||||
};
|
||||
expect(getters.getUIFlags(state)).toEqual({
|
||||
isFetching: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('getNotification', () => {
|
||||
const state = {
|
||||
records: {
|
||||
1: { id: 1 },
|
||||
},
|
||||
};
|
||||
expect(getters.getNotification(state)(1)).toEqual({ id: 1 });
|
||||
expect(getters.getNotification(state)(2)).toEqual({});
|
||||
});
|
||||
|
||||
it('getMeta', () => {
|
||||
const state = {
|
||||
meta: { unreadCount: 1 },
|
||||
};
|
||||
expect(getters.getMeta(state)).toEqual({ unreadCount: 1 });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
import types from '../../../mutation-types';
|
||||
import { mutations } from '../../notifications/mutations';
|
||||
|
||||
describe('#mutations', () => {
|
||||
describe('#SET_NOTIFICATIONS_UI_FLAG', () => {
|
||||
it('set notification ui flag', () => {
|
||||
const state = { uiFlags: { isFetching: true } };
|
||||
mutations[types.SET_NOTIFICATIONS_UI_FLAG](state, { isFetching: false });
|
||||
expect(state.uiFlags).toEqual({ isFetching: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#CLEAR_NOTIFICATIONS', () => {
|
||||
it('clear notifications', () => {
|
||||
const state = { records: { 1: { id: 1 } } };
|
||||
mutations[types.CLEAR_NOTIFICATIONS](state);
|
||||
expect(state.records).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_NOTIFICATIONS_META', () => {
|
||||
it('set notifications meta data', () => {
|
||||
const state = { meta: {} };
|
||||
mutations[types.SET_NOTIFICATIONS_META](state, {
|
||||
count: 3,
|
||||
current_page: 1,
|
||||
unread_count: 2,
|
||||
});
|
||||
expect(state.meta).toEqual({
|
||||
count: 3,
|
||||
currentPage: 1,
|
||||
unreadCount: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_NOTIFICATIONS_UNREAD_COUNT', () => {
|
||||
it('set notifications unread count', () => {
|
||||
const state = { meta: { unreadCount: 4 } };
|
||||
mutations[types.SET_NOTIFICATIONS_UNREAD_COUNT](state, 3);
|
||||
expect(state.meta).toEqual({ unreadCount: 3 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('#SET_NOTIFICATIONS', () => {
|
||||
it('set notifications ', () => {
|
||||
const state = { records: {} };
|
||||
mutations[types.SET_NOTIFICATIONS](state, [
|
||||
{ id: 1 },
|
||||
{ id: 2 },
|
||||
{ id: 3 },
|
||||
{ id: 4 },
|
||||
]);
|
||||
expect(state.records).toEqual({
|
||||
1: { id: 1 },
|
||||
2: { id: 2 },
|
||||
3: { id: 3 },
|
||||
4: { id: 4 },
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#UPDATE_NOTIFICATION', () => {
|
||||
it('update notifications ', () => {
|
||||
const state = {
|
||||
records: {
|
||||
1: { id: 1, primary_actor_id: 1 },
|
||||
},
|
||||
};
|
||||
mutations[types.UPDATE_NOTIFICATION](state, 1);
|
||||
expect(state.records).toEqual({
|
||||
1: { id: 1, primary_actor_id: 1, read_at: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#UPDATE_ALL_NOTIFICATIONS', () => {
|
||||
it('update all notifications ', () => {
|
||||
const state = {
|
||||
records: {
|
||||
1: { id: 1, primary_actor_id: 1 },
|
||||
2: { id: 2, primary_actor_id: 2 },
|
||||
},
|
||||
};
|
||||
mutations[types.UPDATE_ALL_NOTIFICATIONS](state);
|
||||
expect(state.records).toEqual({
|
||||
1: { id: 1, primary_actor_id: 1, read_at: true },
|
||||
2: { id: 2, primary_actor_id: 2, read_at: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -98,6 +98,18 @@ export default {
|
|||
EDIT_CONTACT: 'EDIT_CONTACT',
|
||||
UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE',
|
||||
|
||||
// Notifications
|
||||
SET_NOTIFICATIONS_META: 'SET_NOTIFICATIONS_META',
|
||||
SET_NOTIFICATIONS_UNREAD_COUNT: 'SET_NOTIFICATIONS_UNREAD_COUNT',
|
||||
SET_NOTIFICATIONS_UI_FLAG: 'SET_NOTIFICATIONS_UI_FLAG',
|
||||
UPDATE_NOTIFICATION: 'UPDATE_NOTIFICATION',
|
||||
UPDATE_ALL_NOTIFICATIONS: 'UPDATE_ALL_NOTIFICATIONS',
|
||||
SET_NOTIFICATIONS_ITEM: 'SET_NOTIFICATIONS_ITEM',
|
||||
SET_NOTIFICATIONS: 'SET_NOTIFICATIONS',
|
||||
CLEAR_NOTIFICATIONS: 'CLEAR_NOTIFICATIONS',
|
||||
EDIT_NOTIFICATIONS: 'EDIT_NOTIFICATIONS',
|
||||
UPDATE_NOTIFICATIONS_PRESENCE: 'UPDATE_NOTIFICATIONS_PRESENCE',
|
||||
|
||||
// Contact Conversation
|
||||
SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG',
|
||||
SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS',
|
||||
|
|
Loading…
Reference in a new issue