mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-30 18:47:51 +00:00 
			
		
		
		
	feat: notification center (#1612)
Co-authored-by: Pranav <pranav@chatwoot.com>
This commit is contained in:
		| @@ -1,6 +1,7 @@ | |||||||
| class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController | 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 :fetch_notification, only: [:update] | ||||||
|   before_action :set_primary_actor, only: [:read_all] |   before_action :set_primary_actor, only: [:read_all] | ||||||
|   before_action :set_current_page, only: [:index] |   before_action :set_current_page, only: [:index] | ||||||
| @@ -8,17 +9,18 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro | |||||||
|   def index |   def index | ||||||
|     @unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count |     @unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count | ||||||
|     @count = notifications.count |     @count = notifications.count | ||||||
|     @notifications = notifications.page @current_page |     @notifications = notifications.page(@current_page).per(RESULTS_PER_PAGE) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def read_all |   def read_all | ||||||
|  |     # rubocop:disable Rails/SkipsModelValidations | ||||||
|     if @primary_actor |     if @primary_actor | ||||||
|       current_user.notifications.where(account_id: current_account.id, primary_actor: @primary_actor, read_at: nil) |       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 |     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 |     end | ||||||
|  |     # rubocop:enable Rails/SkipsModelValidations | ||||||
|     head :ok |     head :ok | ||||||
|   end |   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; |     font-size: $font-size-medium; | ||||||
|     margin-top: $space-medium; |     margin-top: $space-medium; | ||||||
|  |  | ||||||
|     >span { |     > span { | ||||||
|       margin-left: $space-one; |       margin-left: $space-one; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .menu-title+ul>li>a { | .menu-title + ul > li > a { | ||||||
|   @include padding($space-micro null); |   @include padding($space-micro null); | ||||||
|   color: $medium-gray; |   color: $medium-gray; | ||||||
|   line-height: $global-lineheight; |   line-height: $global-lineheight; | ||||||
| @@ -152,6 +152,26 @@ | |||||||
|     margin-left: auto; |     margin-left: auto; | ||||||
|     margin-top: 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 { | .hamburger--menu { | ||||||
|   | |||||||
| @@ -73,6 +73,13 @@ | |||||||
|             {{ $t(`AGENT_MGMT.AGENT_TYPES.${currentRole.toUpperCase()}`) }} |             {{ $t(`AGENT_MGMT.AGENT_TYPES.${currentRole.toUpperCase()}`) }} | ||||||
|           </h5> |           </h5> | ||||||
|         </div> |         </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" /> |         <span class="current-user--options icon ion-android-more-vertical" /> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| @@ -134,7 +141,7 @@ | |||||||
|               /> |               /> | ||||||
|             </label> |             </label> | ||||||
|           </div> |           </div> | ||||||
|           <div class="modal-footer  medium-12 columns"> |           <div class="modal-footer medium-12 columns"> | ||||||
|             <div class="medium-12 columns"> |             <div class="medium-12 columns"> | ||||||
|               <woot-submit-button |               <woot-submit-button | ||||||
|                 :disabled=" |                 :disabled=" | ||||||
| @@ -206,6 +213,7 @@ export default { | |||||||
|       currentRole: 'getCurrentRole', |       currentRole: 'getCurrentRole', | ||||||
|       uiFlags: 'agents/getUIFlags', |       uiFlags: 'agents/getUIFlags', | ||||||
|       accountLabels: 'labels/getLabelsOnSidebar', |       accountLabels: 'labels/getLabelsOnSidebar', | ||||||
|  |       notificationMetadata: 'notifications/getMeta', | ||||||
|     }), |     }), | ||||||
|     currentUserAvailableName() { |     currentUserAvailableName() { | ||||||
|       return this.currentUser.name; |       return this.currentUser.name; | ||||||
| @@ -284,10 +292,20 @@ export default { | |||||||
|     dashboardPath() { |     dashboardPath() { | ||||||
|       return frontendURL(`accounts/${this.accountId}/dashboard`); |       return frontendURL(`accounts/${this.accountId}/dashboard`); | ||||||
|     }, |     }, | ||||||
|  |     unreadCount() { | ||||||
|  |       if (!this.notificationMetadata.unreadCount) { | ||||||
|  |         return 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return this.notificationMetadata.unreadCount < 100 | ||||||
|  |         ? this.notificationMetadata.unreadCount | ||||||
|  |         : '99+'; | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   mounted() { |   mounted() { | ||||||
|     this.$store.dispatch('labels/get'); |     this.$store.dispatch('labels/get'); | ||||||
|     this.$store.dispatch('inboxes/get'); |     this.$store.dispatch('inboxes/get'); | ||||||
|  |     this.$store.dispatch('notifications/unReadCount'); | ||||||
|   }, |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     filterMenuItemsByRole(menuItems) { |     filterMenuItemsByRole(menuItems) { | ||||||
| @@ -307,6 +325,9 @@ export default { | |||||||
|     showOptions() { |     showOptions() { | ||||||
|       this.showOptionsMenu = !this.showOptionsMenu; |       this.showOptionsMenu = !this.showOptionsMenu; | ||||||
|     }, |     }, | ||||||
|  |     showNotification() { | ||||||
|  |       this.$router.push(`/app/accounts/${this.accountId}/notifications`); | ||||||
|  |     }, | ||||||
|     changeAccount() { |     changeAccount() { | ||||||
|       this.showAccountModal = true; |       this.showAccountModal = true; | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -8,11 +8,13 @@ export const getSidebarItems = accountId => ({ | |||||||
|       'inbox_conversation', |       'inbox_conversation', | ||||||
|       'conversation_through_inbox', |       'conversation_through_inbox', | ||||||
|       'contacts_dashboard', |       'contacts_dashboard', | ||||||
|  |       'notifications_dashboard', | ||||||
|       'settings_account_reports', |       'settings_account_reports', | ||||||
|       'profile_settings', |       'profile_settings', | ||||||
|       'profile_settings_index', |       'profile_settings_index', | ||||||
|       'label_conversations', |       'label_conversations', | ||||||
|       'conversations_through_label', |       'conversations_through_label', | ||||||
|  |       'notifications_index', | ||||||
|     ], |     ], | ||||||
|     menuItems: { |     menuItems: { | ||||||
|       assignedToMe: { |       assignedToMe: { | ||||||
| @@ -31,6 +33,13 @@ export const getSidebarItems = accountId => ({ | |||||||
|         toState: frontendURL(`accounts/${accountId}/contacts`), |         toState: frontendURL(`accounts/${accountId}/contacts`), | ||||||
|         toStateName: 'contacts_dashboard', |         toStateName: 'contacts_dashboard', | ||||||
|       }, |       }, | ||||||
|  |       notifications: { | ||||||
|  |         icon: 'ion-ios-bell', | ||||||
|  |         label: 'NOTIFICATIONS', | ||||||
|  |         hasSubMenu: false, | ||||||
|  |         toState: frontendURL(`accounts/${accountId}/notifications`), | ||||||
|  |         toStateName: 'notifications_dashboard', | ||||||
|  |       }, | ||||||
|       report: { |       report: { | ||||||
|         icon: 'ion-arrow-graph-up-right', |         icon: 'ion-arrow-graph-up-right', | ||||||
|         label: 'REPORTS', |         label: 'REPORTS', | ||||||
|   | |||||||
| @@ -51,5 +51,24 @@ | |||||||
|       "ENTER_TO_REMOVE": "Press enter to remove", |       "ENTER_TO_REMOVE": "Press enter to remove", | ||||||
|       "SELECT_ONE": "Select one" |       "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", |     "HOME": "Home", | ||||||
|     "AGENTS": "Agents", |     "AGENTS": "Agents", | ||||||
|     "INBOXES": "Inboxes", |     "INBOXES": "Inboxes", | ||||||
|  |     "NOTIFICATIONS": "Notifications", | ||||||
|     "CANNED_RESPONSES": "Canned Responses", |     "CANNED_RESPONSES": "Canned Responses", | ||||||
|     "INTEGRATIONS": "Integrations", |     "INTEGRATIONS": "Integrations", | ||||||
|     "ACCOUNT_SETTINGS": "Account Settings", |     "ACCOUNT_SETTINGS": "Account Settings", | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ | |||||||
|         :on-click-contact="openContactInfoPanel" |         :on-click-contact="openContactInfoPanel" | ||||||
|         :active-contact-id="selectedContactId" |         :active-contact-id="selectedContactId" | ||||||
|       /> |       /> | ||||||
|       <contacts-footer |       <table-footer | ||||||
|         :on-page-change="onPageChange" |         :on-page-change="onPageChange" | ||||||
|         :current-page="Number(meta.currentPage)" |         :current-page="Number(meta.currentPage)" | ||||||
|         :total-count="meta.count" |         :total-count="meta.count" | ||||||
| @@ -34,13 +34,13 @@ import { mapGetters } from 'vuex'; | |||||||
| import ContactsHeader from './Header'; | import ContactsHeader from './Header'; | ||||||
| import ContactsTable from './ContactsTable'; | import ContactsTable from './ContactsTable'; | ||||||
| import ContactInfoPanel from './ContactInfoPanel'; | import ContactInfoPanel from './ContactInfoPanel'; | ||||||
| import ContactsFooter from './Footer'; | import TableFooter from 'dashboard/components/widgets/TableFooter'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|     ContactsHeader, |     ContactsHeader, | ||||||
|     ContactsTable, |     ContactsTable, | ||||||
|     ContactsFooter, |     TableFooter, | ||||||
|     ContactInfoPanel, |     ContactInfoPanel, | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import AppContainer from './Dashboard'; | |||||||
| import settings from './settings/settings.routes'; | import settings from './settings/settings.routes'; | ||||||
| import conversation from './conversation/conversation.routes'; | import conversation from './conversation/conversation.routes'; | ||||||
| import { routes as contactRoutes } from './contacts/routes'; | import { routes as contactRoutes } from './contacts/routes'; | ||||||
|  | import { routes as notificationRoutes } from './notifications/routes'; | ||||||
| import { frontendURL } from '../../helper/URLHelper'; | import { frontendURL } from '../../helper/URLHelper'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
| @@ -9,7 +10,12 @@ export default { | |||||||
|     { |     { | ||||||
|       path: frontendURL('accounts/:account_id'), |       path: frontendURL('accounts/:account_id'), | ||||||
|       component: AppContainer, |       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 cannedResponse from './modules/cannedResponse'; | ||||||
| import contactConversations from './modules/contactConversations'; | import contactConversations from './modules/contactConversations'; | ||||||
| import contacts from './modules/contacts'; | import contacts from './modules/contacts'; | ||||||
|  | import notifications from './modules/notifications'; | ||||||
| import conversationLabels from './modules/conversationLabels'; | import conversationLabels from './modules/conversationLabels'; | ||||||
| import conversationMetadata from './modules/conversationMetadata'; | import conversationMetadata from './modules/conversationMetadata'; | ||||||
| import conversationPage from './modules/conversationPage'; | import conversationPage from './modules/conversationPage'; | ||||||
| @@ -32,6 +33,7 @@ export default new Vuex.Store({ | |||||||
|     cannedResponse, |     cannedResponse, | ||||||
|     contactConversations, |     contactConversations, | ||||||
|     contacts, |     contacts, | ||||||
|  |     notifications, | ||||||
|     conversationLabels, |     conversationLabels, | ||||||
|     conversationMetadata, |     conversationMetadata, | ||||||
|     conversationPage, |     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', |   EDIT_CONTACT: 'EDIT_CONTACT', | ||||||
|   UPDATE_CONTACTS_PRESENCE: 'UPDATE_CONTACTS_PRESENCE', |   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 |   // Contact Conversation | ||||||
|   SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG', |   SET_CONTACT_CONVERSATIONS_UI_FLAG: 'SET_CONTACT_CONVERSATIONS_UI_FLAG', | ||||||
|   SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS', |   SET_CONTACT_CONVERSATIONS: 'SET_CONTACT_CONVERSATIONS', | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Muhsin Keloth
					Muhsin Keloth