Update notifications flyout [SCI-10735]

This commit is contained in:
Anton 2024-10-14 10:20:43 +02:00
parent 100c84732d
commit c71366acc5
9 changed files with 132 additions and 29 deletions

View file

@ -30,7 +30,7 @@
flex-direction: column;
height: calc(100vh - 8rem);
padding: 1.5rem;
width: 400px;
width: 600px;
.sci--navigation--notificaitons-flyout-title {
@include font-h2;

View file

@ -5,11 +5,18 @@ class UserNotificationsController < ApplicationController
def index
page = (params.dig(:page, :number) || 1).to_i
notifications = load_notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT)
notifications = load_notifications
case params[:tab]
when 'all'
notifications = notifications.where.not(read_at: nil)
when 'unread'
notifications = notifications.where(read_at: nil)
end
notifications = notifications.page(page).per(Constants::INFINITE_SCROLL_LIMIT)
render json: notifications, each_serializer: NotificationSerializer
notifications.mark_as_read!
end
def unseen_counter
@ -18,6 +25,17 @@ class UserNotificationsController < ApplicationController
}
end
def mark_all_read
load_notifications.mark_as_read!
render json: { success: true }
end
def toggle_read
notification = current_user.notifications.find(params[:id])
notification.update(read_at: (params[:mark_as_read] ? DateTime.now : nil))
render json: notification, serializer: NotificationSerializer
end
private
def load_notifications
@ -25,5 +43,4 @@ class UserNotificationsController < ApplicationController
.in_app
.order(created_at: :desc)
end
end

View file

@ -1,12 +1,20 @@
<template>
<div class="sci-navigation--notificaitons-flyout-notification">
<div class="sci-navigation--notificaitons-flyout-notification-date">
{{ notification.attributes.created_at }}
<div class="flex item-center">
<div class="sci-navigation--notificaitons-flyout-notification-date">
{{ notification.attributes.created_at }}
</div>
<div class="ml-auto cursor-pointer" @click="toggleRead()">
<div v-if="!notification.attributes.checked" class="w-2.5 h-2.5 bg-sn-coral rounded-full cursor-pointer"></div>
<div v-else class="w-2.5 h-2.5 border-2 border-sn-grey rounded-full border-solid cursor-pointer"></div>
</div>
</div>
<div class="sci-navigation--notificaitons-flyout-notification-title"
v-html="notification.attributes.title"
:data-seen="notification.attributes.checked"></div>
<div v-html="notification.attributes.message" class="sci-navigation--notificaitons-flyout-notification-message"></div>
<a :href="lastBreadcrumbUrl" @click="toggleRead(true)" class="hover:no-underline text-black hover:text-black">
<div class="sci-navigation--notificaitons-flyout-notification-title"
v-html="notification.attributes.title"
:data-seen="notification.attributes.checked"></div>
<div v-html="notification.attributes.message" class="sci-navigation--notificaitons-flyout-notification-message"></div>
</a>
<div v-if="notification.attributes.breadcrumbs" class="flex items-center flex-wrap gap-0.5">
<template v-for="(breadcrumb, index) in notification.attributes.breadcrumbs" :key="index">
<div class="flex items-center gap-0.5">
@ -19,23 +27,33 @@
</template>
<script>
import axios from '../../../packs/custom_axios.js';
export default {
name: 'NotificationItem',
props: {
notification: Object
},
computed: {
icon() {
switch (this.notification.attributes.type_of) {
case 'deliver':
return 'fas fa-truck';
case 'assignment':
return 'fas fa-list-alt';
case 'recent_changes':
return 'fas fa-list-alt';
case 'deliver_error':
return 'sn-icon sn-icon-alert-warning';
lastBreadcrumbUrl() {
if (!this.notification.attributes.breadcrumbs) {
return null;
}
return this.notification.attributes.breadcrumbs[this.notification.attributes.breadcrumbs.length - 1]?.url;
}
},
methods: {
toggleRead(check = false) {
const params = {};
if (!this.notification.attributes.checked || check) {
params.mark_as_read = true;
}
axios.post(this.notification.attributes.toggle_read_url, params)
.then((response) => {
const notification = response.data.data;
this.$emit('updateNotification', notification);
});
}
}
};

View file

@ -6,17 +6,45 @@
{{ i18n.t('nav.settings') }}
</a>
</div>
<hr>
<div class="flex items-center">
<div
@click="changeTab('all')"
class="px-4 py-2 text-sn-grey cursor-pointer border-solid border-0 border-b-4 border-transparent"
:class="{'!text-sn-black border-sn-blue': activeTab === 'all'}"
>
{{ i18n.t('nav.notifications.all') }}
</div>
<div
@click="changeTab('unread')"
class="px-4 py-2 text-sn-grey cursor-pointer border-solid border-0 border-b-4 border-transparent"
:class="{'!text-sn-black border-sn-blue': activeTab === 'unread'}"
>
{{ i18n.t('nav.notifications.unread') }}
</div>
<div
@click="changeTab('read')"
class="px-4 py-2 text-sn-grey cursor-pointer border-solid border-0 border-b-4 border-transparent"
:class="{'!text-sn-black border-sn-blue': activeTab === 'read'}"
>
{{ i18n.t('nav.notifications.read') }}
</div>
<div class="py-4 ml-auto cursor-pointer" @click="markAllNotificationsAsRead">
{{ i18n.t('nav.notifications.read_all') }}
</div>
</div>
<hr class="!mt-0.5">
<perfect-scrollbar @wheel="preventPropagation" ref="scrollContainer" class="sci--navigation--notificaitons-flyout-notifications">
<div class="sci-navigation--notificaitons-flyout-subtitle" v-if="todayNotifications.length">
{{ i18n.t('nav.notifications.today') }}
</div>
<NotificationItem v-for="notification in todayNotifications" :key="notification.type_of + '-' + notification.id"
@updateNotification="updateNotification"
:notification="notification" />
<div class="sci-navigation--notificaitons-flyout-subtitle" v-if="olderNotifications.length">
{{ i18n.t('nav.notifications.older') }}
</div>
<NotificationItem v-for="notification in olderNotifications" :key="notification.type_of + '-' + notification.id"
@updateNotification="updateNotification"
:notification="notification" />
<div class="next-page-loader">
<img src="/images/medium/loading.svg" v-if="loadingPage" />
@ -37,6 +65,7 @@ export default {
},
props: {
notificationsUrl: String,
markAllNotificationsUrl: String,
unseenNotificationsCount: Number,
preferencesUrl: String
},
@ -45,11 +74,14 @@ export default {
notifications: [],
nextPageUrl: null,
scrollBar: null,
loadingPage: false
activeTab: 'all',
loadingPage: false,
firstPageUrl: null
};
},
created() {
this.nextPageUrl = this.notificationsUrl;
this.firstPageUrl = this.notificationsUrl;
this.loadNotifications();
},
mounted() {
@ -76,6 +108,27 @@ export default {
}
},
methods: {
changeTab(tab) {
this.activeTab = tab;
this.notifications = [];
this.nextPageUrl = this.firstPageUrl;
this.loadNotifications();
},
markAllNotificationsAsRead() {
axios.post(this.markAllNotificationsUrl)
.then(() => {
this.notifications = this.notifications.map((n) => {
n.attributes.checked = true;
return n;
});
this.$emit('update:unseenNotificationsCount');
});
},
updateNotification(notification) {
const index = this.notifications.findIndex((n) => n.id === notification.id);
this.notifications.splice(index, 1, notification);
this.$emit('update:unseenNotificationsCount');
},
preventPropagation(e) {
e.stopPropagation();
e.preventDefault();
@ -85,7 +138,7 @@ export default {
this.loadingPage = true;
axios.get(this.nextPageUrl)
axios.get(this.nextPageUrl, { params: { tab: this.activeTab } })
.then((response) => {
this.notifications = this.notifications.concat(response.data.data);
this.nextPageUrl = response.data.links.next;

View file

@ -45,8 +45,9 @@
<NotificationsFlyout
:preferencesUrl="user.preferences_url"
:notificationsUrl="notificationsUrl"
:markAllNotificationsUrl="markAllNotificationsUrl"
:unseenNotificationsCount="unseenNotificationsCount"
@update:unseenNotificationsCount="checkUnseenNotifications()"
@update:unseenNotificationsCount="checkUnseenNotifications(false)"
@close="$refs.notificationDropdown.$refs.field.click();"/>
</template>
</GeneralDropdown>
@ -92,6 +93,7 @@ export default {
props: {
url: String,
notificationsUrl: String,
markAllNotificationsUrl: String,
unseenNotificationsUrl: String,
quickSearchUrl: String,
teamsUrl: String,
@ -119,7 +121,7 @@ export default {
$(document).on('turbolinks:load', () => {
this.notificationsOpened = false;
this.checkUnseenNotifications();
this.checkUnseenNotifications(false);
this.refreshCurrentTeam();
this.hideSearch = !!document.getElementById('GlobalSearch');
this.globalSearchKey += 1;
@ -177,11 +179,13 @@ export default {
searchValue(e) {
window.open(`${this.searchUrl}?q=${e.target.value}`, '_self');
},
checkUnseenNotifications() {
checkUnseenNotifications(repeat = true) {
clearTimeout(this.unseenNotificationsTimeout);
$.get(this.unseenNotificationsUrl, (result) => {
this.unseenNotificationsCount = result.unseen;
this.unseenNotificationsTimeout = setTimeout(this.checkUnseenNotifications, 30000);
if (repeat) {
this.unseenNotificationsTimeout = setTimeout(this.checkUnseenNotifications, 30000);
}
});
},
refreshCurrentTeam() {

View file

@ -4,7 +4,7 @@ class NotificationSerializer < ActiveModel::Serializer
include Rails.application.routes.url_helpers
include BreadcrumbsHelper
attributes :id, :title, :message, :created_at, :read_at, :type, :breadcrumbs, :checked, :today
attributes :id, :title, :message, :created_at, :read_at, :type, :breadcrumbs, :checked, :today, :toggle_read_url
def title
object.to_notification.title
@ -31,4 +31,7 @@ class NotificationSerializer < ActiveModel::Serializer
object.read_at.present?
end
def toggle_read_url
toggle_read_user_notification_path(object)
end
end

View file

@ -3,6 +3,7 @@
<top-menu-container
url="<%= top_menu_navigations_path %>"
notifications-url="<%= user_notifications_path %>"
mark-all-notifications-url="<%= mark_all_read_user_notifications_path %>"
quick-search-url="<%= quick_search_path %>"
unseen-notifications-url="<%= unseen_counter_user_notifications_path %>"
teams-url="<%= visible_teams_teams_path %>"

View file

@ -310,6 +310,9 @@ en:
academy_tooltip: "SciNote Academy"
notifications:
title: "Notifications"
read: "Read"
unread: "Unread"
read_all: "Read all"
all: "All"
message: "Messages"
system: "System"

View file

@ -168,6 +168,10 @@ Rails.application.routes.draw do
collection do
get :filter_groups
get :unseen_counter
post :mark_all_read
end
member do
post :toggle_read
end
end