mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 14:45:56 +08:00
System notification navbar menu drop down [SCI 2955] (#1498)
* Add dropdown menu and icon for system notifications * Finish system notification dropdown menu and add tests
This commit is contained in:
parent
c598541e09
commit
8e905b67b2
|
@ -51,8 +51,8 @@
|
|||
});
|
||||
}
|
||||
|
||||
function loadUnseenNotificationsNumber() {
|
||||
var notificationCount = $('#count-notifications');
|
||||
function loadUnseenNotificationsNumber(element = 'notifications', icon = '.fa-bell') {
|
||||
var notificationCount = $('#count-' + element);
|
||||
$.ajax({
|
||||
url: notificationCount.attr('data-href'),
|
||||
type: 'GET',
|
||||
|
@ -62,7 +62,7 @@
|
|||
if (data.notificationNmber > 0) {
|
||||
notificationCount.html(data.notificationNmber);
|
||||
notificationCount.show();
|
||||
toggleNotificationBellPosition();
|
||||
toggleNotificationBellPosition(element, icon);
|
||||
} else {
|
||||
notificationCount.hide();
|
||||
}
|
||||
|
@ -70,17 +70,17 @@
|
|||
});
|
||||
}
|
||||
|
||||
function toggleNotificationBellPosition() {
|
||||
var notificationCount = $('#count-notifications');
|
||||
var button = $('#notifications-dropdown');
|
||||
function toggleNotificationBellPosition(element = 'notifications', icon = '.fa-bell') {
|
||||
var notificationCount = $('#count-' + element);
|
||||
var button = $('#' + element + '-dropdown');
|
||||
|
||||
if (notificationCount.is(':hidden')) {
|
||||
button
|
||||
.find('.fa-bell')
|
||||
.find(icon)
|
||||
.css('position', 'relative');
|
||||
} else {
|
||||
button
|
||||
.find('.fa-bell')
|
||||
.find(icon)
|
||||
.css('position', 'absolute');
|
||||
}
|
||||
}
|
||||
|
@ -104,4 +104,57 @@
|
|||
loadUnseenNotificationsNumber();
|
||||
toggleNotificationBellPosition();
|
||||
initGlobalSwitchForm();
|
||||
}());
|
||||
|
||||
// System notifications
|
||||
|
||||
function loadDropdownSystemNotifications() {
|
||||
var button = $('#system-notifications-dropdown');
|
||||
var noRecentText = $('.dropdown-system-notifications .system-notifications-no-recent');
|
||||
button
|
||||
.on('click', function() {
|
||||
noRecentText.hide();
|
||||
$.ajax({
|
||||
url: button.attr('data-href'),
|
||||
type: 'GET',
|
||||
dataType: 'json',
|
||||
beforeSend: animateSpinner($('.system-notifications-dropdown-header'), true),
|
||||
success: function(data) {
|
||||
var ul = $('.dropdown-menu.dropdown-system-notifications');
|
||||
// After closing system notification modal release system notifications dropdown
|
||||
$('#manage-module-system-notification-modal').on('hidden.bs.modal', function() {
|
||||
setTimeout(function() {
|
||||
$('.dropdown.system-notifications')[0].dataset.closable = true;
|
||||
}, 100);
|
||||
});
|
||||
$('.system-notifications-dropdown-header')
|
||||
.nextAll('.system-notification')
|
||||
.remove();
|
||||
$('.system-notifications-dropdown-header')
|
||||
.after(data.html);
|
||||
animateSpinner($('.system-notifications-dropdown-header'), false);
|
||||
if (ul.children('.system-notification').length === 0) {
|
||||
noRecentText.show();
|
||||
}
|
||||
bindSystemNotificationAjax();
|
||||
SystemNotificationsMarkAsSeen('.dropdown-system-notifications');
|
||||
}
|
||||
});
|
||||
$('#count-system-notifications').hide();
|
||||
toggleNotificationBellPosition('system-notifications', '.fa-gift');
|
||||
});
|
||||
}
|
||||
|
||||
// init
|
||||
loadDropdownSystemNotifications();
|
||||
$('.dropdown-system-notifications').scroll(function() {
|
||||
SystemNotificationsMarkAsSeen('.dropdown-system-notifications');
|
||||
});
|
||||
loadUnseenNotificationsNumber('system-notifications', '.fa-gift');
|
||||
// Override dropdown menu closing action while system notification modal open
|
||||
$('.dropdown.system-notifications').on('hide.bs.dropdown', function() {
|
||||
if (this.dataset.closable === 'false') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
// update selected notiifcations
|
||||
function SystemNotificationsMarkAsSeen() {
|
||||
var WindowSize = $(window).height();
|
||||
var NotificaitonSize = 75;
|
||||
function SystemNotificationsMarkAsSeen(container = window) {
|
||||
var WindowSize = $(container).height();
|
||||
var NotificationsToUpdate = [];
|
||||
|
||||
_.each($('.system-notification[data-new="1"]'), function(el) {
|
||||
var NotificationTopPosition = el.getBoundingClientRect().top;
|
||||
if (NotificationTopPosition > 0 && NotificationTopPosition < (WindowSize - NotificaitonSize)) {
|
||||
if (NotificationTopPosition > 0 && NotificationTopPosition < WindowSize) {
|
||||
NotificationsToUpdate.push(el.dataset.systemNotificationId);
|
||||
el.dataset.new = 0;
|
||||
}
|
||||
|
@ -32,6 +30,7 @@ function bindSystemNotificationAjax() {
|
|||
var SystemNotification = $('.system-notification[data-system-notification-id=' + data.id + ']')[0];
|
||||
SystemNotificationModalBody.html(data.modal_body);
|
||||
SystemNotificationModalTitle.text(data.modal_title);
|
||||
$('.dropdown.system-notifications')[0].dataset.closable = false;
|
||||
// Open modal
|
||||
SystemNotificationModal.modal('show');
|
||||
if (SystemNotification.dataset.unread === '1') {
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
#notifications-dropdown {
|
||||
.fa-bell {
|
||||
font-size: $font-size-large;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#count-notifications {
|
||||
|
@ -310,7 +309,7 @@
|
|||
}
|
||||
|
||||
#nav-team-switch {
|
||||
margin-left: 30px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.custom-nav-dropdown {
|
||||
|
@ -362,3 +361,54 @@
|
|||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// System notiifcations dropdown
|
||||
|
||||
#system-notifications-dropdown {
|
||||
|
||||
.fa-gift {
|
||||
font-size: $font-size-large;
|
||||
}
|
||||
|
||||
#count-system-notifications {
|
||||
background-color: $brand-primary;
|
||||
border-radius: 5px;
|
||||
color: $color-concrete;
|
||||
display: none;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
margin-left: 12px;
|
||||
padding: 1px 6px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-system-notifications {
|
||||
margin-bottom: 10px;
|
||||
max-height: 500px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
padding-bottom: 0;
|
||||
padding-top: 0;
|
||||
width: 450px;
|
||||
word-wrap: break-word;
|
||||
|
||||
.system-notifications-no-recent {
|
||||
padding: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
.system-notifications-dropdown-header {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.show-all {
|
||||
margin-left: 20px;
|
||||
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: $font-size-small;
|
||||
font-weight: $headings-font-weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,6 +87,7 @@ table {
|
|||
}
|
||||
|
||||
.notifications-dropdown-header,
|
||||
.system-notifications-dropdown-header,
|
||||
.dropdown-header {
|
||||
background-color: $brand-primary;
|
||||
color: $color-concrete;
|
||||
|
|
|
@ -31,6 +31,7 @@ class SystemNotificationsController < ApplicationController
|
|||
render json: { result: 'failed' }
|
||||
end
|
||||
|
||||
# Update read_at parameter for system notifications
|
||||
def mark_as_read
|
||||
current_user.user_system_notifications.mark_as_read(params[:id])
|
||||
render json: { result: 'ok' }
|
||||
|
@ -38,6 +39,12 @@ class SystemNotificationsController < ApplicationController
|
|||
render json: { result: 'failed' }
|
||||
end
|
||||
|
||||
def unseen_counter
|
||||
render json: {
|
||||
notificationNmber: current_user.user_system_notifications.unseen.count
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prepare_notifications
|
||||
|
|
|
@ -4,6 +4,8 @@ class UserSystemNotification < ApplicationRecord
|
|||
belongs_to :user
|
||||
belongs_to :system_notification
|
||||
|
||||
scope :unseen, -> { where(seen_at: nil) }
|
||||
|
||||
def self.mark_as_seen(notifications_id)
|
||||
where(system_notification_id: notifications_id)
|
||||
.update_all(seen_at: Time.now)
|
||||
|
|
|
@ -40,8 +40,11 @@
|
|||
<%= render "shared/about_modal" %>
|
||||
<%= render "shared/file_preview_modal.html.erb" %>
|
||||
<%= render "shared/navigation" %>
|
||||
|
||||
<% if user_signed_in? && flash[:system_notification_modal] && current_user.show_login_system_notification? %>
|
||||
<%= render partial: "/system_notifications/system_notification_modal", locals: { notification: current_user.user_system_notifications.show_on_login(true) } %>
|
||||
<% else %>
|
||||
<%= render partial: "/system_notifications/system_notification_modal", locals: { notification: nil} %>
|
||||
<% end %>
|
||||
|
||||
<% unless user_signed_in? %>
|
||||
|
|
|
@ -17,6 +17,38 @@
|
|||
<% if user_signed_in? %>
|
||||
<div class="collapse navbar-collapse" id="main-menu">
|
||||
<ul class="nav navbar-nav navbar-left" id="nav-team-switch">
|
||||
<!-- System notification dropdown -->
|
||||
<li class="dropdown system-notifications">
|
||||
<a href="#"
|
||||
id="system-notifications-dropdown"
|
||||
class="dropdown-toggle"
|
||||
title="System notifications"
|
||||
data-toggle="dropdown"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
data-href="<%= system_notifications_url(format: :json) %>">
|
||||
<i class="fas fa-gift"></i>
|
||||
<span class="visible-xs-inline visible-sm-inline"><%= t("system_notifications.index.whats_new") %></span>
|
||||
<span id="count-system-notifications"
|
||||
data-href="<%= unseen_counter_system_notifications_url %>">
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-system-notifications">
|
||||
<li class="system-notifications-dropdown-header">
|
||||
<span class="pull-left"><%= t("system_notifications.index.whats_new") %></span>
|
||||
<span class="show-all">
|
||||
<%= link_to t('system_notifications.index.see_all'), system_notifications_path %>
|
||||
</span>
|
||||
<span class="pull-right">
|
||||
<%= link_to t('nav.user.settings'), preferences_path %>
|
||||
</span>
|
||||
</li>
|
||||
<li class="system-notifications-no-recent">
|
||||
<em><%= t("system_notifications.index.no_notifications") %></em>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<!-- Global team switch -->
|
||||
<% if current_user.teams.length > 0 %>
|
||||
<li id="team-switch">
|
||||
|
@ -83,7 +115,7 @@
|
|||
name="q"
|
||||
placeholder="<%= t('nav.search') %>" />
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default" type="submit"><%=t 'nav.search_button' %></button>
|
||||
<button class="btn btn-default" type="submit"><%= t 'nav.search_button' %></button>
|
||||
</span>
|
||||
</div>
|
||||
<% end %>
|
||||
|
@ -136,7 +168,7 @@
|
|||
data-href="<%= unseen_notification_url(current_user) %>">
|
||||
</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-notifications">
|
||||
<ul class="dropdown-menu dropdown-notifications system-notifications">
|
||||
<li class="notifications-dropdown-header">
|
||||
<span><%= t('notifications.title') %></span>
|
||||
<span class="pull-right">
|
||||
|
@ -159,4 +191,5 @@
|
|||
</nav>
|
||||
<div id="loading-animation"></div>
|
||||
|
||||
<%= javascript_include_tag("system_notifications/index") %>
|
||||
<%= javascript_include_tag("navigation") %>
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
<% provide(:head_title, t("system_notifications.index.whats_new")) %>
|
||||
|
||||
<%= render partial: "system_notification_modal", locals: { notification: nil} %>
|
||||
|
||||
<div class="content-pane" id="system-notifications-index">
|
||||
<h3 class="title-container">
|
||||
<strong><%= t("system_notifications.index.whats_new") %></strong>
|
||||
<%= link_to t("system_notifications.index.settings"), nil %>
|
||||
<%= link_to t("system_notifications.index.settings"), preferences_path %>
|
||||
</h3>
|
||||
<%= form_tag system_notifications_path, method: :get, id: "search-bar-notifications", class: "navbar-form navbar-left", role: "search" do %>
|
||||
<div class="form-group">
|
||||
|
@ -34,4 +32,3 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<%= javascript_include_tag "system_notifications/index.js" %>
|
||||
|
|
|
@ -1293,10 +1293,11 @@ en:
|
|||
success_flash: "Successfully added sample group <strong>%{sample_group}</strong> to team <strong>%{team}</strong>."
|
||||
system_notifications:
|
||||
index:
|
||||
whats_new: "What's new"
|
||||
whats_new: "What's New"
|
||||
more_notifications: "More system notifications"
|
||||
no_notifications: "No system notifications"
|
||||
no_notifications: "No more system notifications"
|
||||
settings: "Settings"
|
||||
see_all: "See all"
|
||||
activities:
|
||||
index:
|
||||
today: "Today"
|
||||
|
|
|
@ -429,6 +429,7 @@ Rails.application.routes.draw do
|
|||
resources :system_notifications, only: [:index,:show] do
|
||||
collection do
|
||||
post 'mark_as_seen'
|
||||
get 'unseen_counter'
|
||||
end
|
||||
member do
|
||||
post 'mark_as_read'
|
||||
|
|
83
spec/controllers/system_notifications_controller_spec.rb
Normal file
83
spec/controllers/system_notifications_controller_spec.rb
Normal file
|
@ -0,0 +1,83 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe SystemNotificationsController, type: :controller do
|
||||
login_user
|
||||
render_views
|
||||
let(:user) { User.first }
|
||||
|
||||
describe 'Methods' do
|
||||
let(:notifcation_one) { create :system_notification }
|
||||
let(:notifcation_two) { create :system_notification, title: 'Special one' }
|
||||
before do
|
||||
create :user_system_notification,
|
||||
user: user,
|
||||
system_notification: notifcation_one
|
||||
create :user_system_notification,
|
||||
user: user,
|
||||
system_notification: notifcation_two
|
||||
end
|
||||
|
||||
it '#show return right result format' do
|
||||
params = {
|
||||
id: user.user_system_notifications.first.system_notification_id
|
||||
}
|
||||
get :show, format: :json, params: params
|
||||
expect(response).to have_http_status(:ok)
|
||||
body = JSON.parse(response.body)
|
||||
expect(body).to include('id', 'modal_title', 'modal_body')
|
||||
end
|
||||
|
||||
it '#mark_as_seen correctly set seen_at' do
|
||||
params = {
|
||||
notifications: user.user_system_notifications
|
||||
.map(&:system_notification_id).to_s
|
||||
}
|
||||
get :mark_as_seen, format: :json, params: params
|
||||
expect(response).to have_http_status(:ok)
|
||||
body = JSON.parse(response.body)
|
||||
expect(body['result']).to eq 'ok'
|
||||
expect(user.user_system_notifications.where(seen_at: nil).count).to eq 0
|
||||
end
|
||||
|
||||
it '#mark_as_seen response failed on wrong id\'s format' do
|
||||
params = { notifications: 'wrong format' }
|
||||
get :mark_as_seen, format: :json, params: params
|
||||
expect(response).to have_http_status(:ok)
|
||||
body = JSON.parse(response.body)
|
||||
expect(body['result']).to eq 'failed'
|
||||
end
|
||||
|
||||
it '#mark_as_read correctly set read_at' do
|
||||
params = {
|
||||
id: user.user_system_notifications.first.system_notification_id
|
||||
}
|
||||
get :mark_as_read, format: :json, params: params
|
||||
expect(response).to have_http_status(:ok)
|
||||
body = JSON.parse(response.body)
|
||||
expect(body['result']).to eq 'ok'
|
||||
expect(user.user_system_notifications.where(read_at: nil).count).to eq 1
|
||||
end
|
||||
|
||||
it '#unseen_counter return right result' do
|
||||
get :unseen_counter, format: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
body = JSON.parse(response.body)
|
||||
expect(body['notificationNmber']).to eq 2
|
||||
end
|
||||
|
||||
it '#index check next page link' do
|
||||
notifications = create_list :system_notification, 50
|
||||
notifications.each do |i|
|
||||
create :user_system_notification,
|
||||
user: user,
|
||||
system_notification: i
|
||||
end
|
||||
get :index, format: :json
|
||||
expect(response).to have_http_status(:ok)
|
||||
body = JSON.parse(response.body)
|
||||
expect(body['more_url']).to include('system_notifications.json?page=2')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,14 +5,14 @@ FactoryBot.define do
|
|||
user
|
||||
system_notification
|
||||
trait :seen do
|
||||
seen { Faker::Time.between(3.days.ago, Date.today) }
|
||||
seen_at { Faker::Time.between(3.days.ago, Date.today) }
|
||||
end
|
||||
trait :read do
|
||||
read { Faker::Time.between(3.days.ago, Date.today) }
|
||||
read_at { Faker::Time.between(3.days.ago, Date.today) }
|
||||
end
|
||||
trait :seen_and_read do
|
||||
seen { Faker::Time.between(3.days.ago, Date.today) }
|
||||
read { Faker::Time.between(seen, Date.today) }
|
||||
seen_at { Faker::Time.between(3.days.ago, Date.today) }
|
||||
read_at { Faker::Time.between(seen_at, Date.today) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe SystemNotification do
|
||||
let(:user) { create :user }
|
||||
subject(:system_notification) { build :system_notification }
|
||||
|
||||
it 'is valid' do
|
||||
|
@ -69,4 +70,44 @@ describe SystemNotification do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Methods' do
|
||||
let(:notifcation_one) { create :system_notification }
|
||||
let(:notifcation_two) { create :system_notification, title: 'Special one' }
|
||||
before do
|
||||
create :user_system_notification,
|
||||
user: user,
|
||||
system_notification: notifcation_one
|
||||
create :user_system_notification,
|
||||
user: user,
|
||||
system_notification: notifcation_two
|
||||
end
|
||||
|
||||
it 'get last notifications without search' do
|
||||
result = SystemNotification.last_notifications(user)
|
||||
expect(result.length).to eq 2
|
||||
expect(result.first).to respond_to(
|
||||
:id,
|
||||
:title,
|
||||
:description,
|
||||
:last_time_changed_at,
|
||||
:seen_at,
|
||||
:read_at
|
||||
)
|
||||
end
|
||||
|
||||
it 'get last notifications with search' do
|
||||
result = SystemNotification.last_notifications(user, 'Special one')
|
||||
expect(result.length).to eq 1
|
||||
expect(result.first).to respond_to(
|
||||
:id,
|
||||
:title,
|
||||
:description,
|
||||
:last_time_changed_at,
|
||||
:seen_at,
|
||||
:read_at
|
||||
)
|
||||
expect(result.first.title).to eq 'Special one'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe UserSystemNotification do
|
||||
let(:user) { create :user }
|
||||
subject(:user_system_notification) { build :user_system_notification }
|
||||
|
||||
it 'is valid' do
|
||||
|
@ -13,4 +14,47 @@ describe UserSystemNotification do
|
|||
it { is_expected.to belong_to(:user) }
|
||||
it { is_expected.to belong_to(:system_notification) }
|
||||
end
|
||||
|
||||
describe 'Methods' do
|
||||
let(:notifcation_one) { create :system_notification }
|
||||
let(:notifcation_two) { create :system_notification }
|
||||
let(:notifcation_three) { create :system_notification, :show_on_login }
|
||||
|
||||
it 'make_as_seen update seen_at' do
|
||||
usn = create :user_system_notification,
|
||||
user: user,
|
||||
system_notification: notifcation_one
|
||||
notifications_to_update = [usn.system_notification_id]
|
||||
user.user_system_notifications.mark_as_seen(notifications_to_update)
|
||||
expect(UserSystemNotification.find(usn.id).seen_at).not_to be_nil
|
||||
end
|
||||
|
||||
it 'make_as_read update read_at' do
|
||||
usn = create :user_system_notification,
|
||||
user: user,
|
||||
system_notification: notifcation_one
|
||||
user.user_system_notifications.mark_as_read(usn.system_notification_id)
|
||||
expect(UserSystemNotification.find(usn.id).read_at).not_to be_nil
|
||||
end
|
||||
|
||||
it 'show_on_login method only check any notifications' do
|
||||
usn = create :user_system_notification,
|
||||
user: user,
|
||||
system_notification: notifcation_three
|
||||
result = user.user_system_notifications.show_on_login
|
||||
expect(result).not_to be_nil
|
||||
expect(UserSystemNotification.find(usn.id).seen_at).to be_nil
|
||||
expect(UserSystemNotification.find(usn.id).read_at).to be_nil
|
||||
end
|
||||
|
||||
it 'show_on_login method update notification read and seen time' do
|
||||
usn = create :user_system_notification,
|
||||
user: user,
|
||||
system_notification: notifcation_three
|
||||
result = user.user_system_notifications.show_on_login(true)
|
||||
expect(result).not_to be_nil
|
||||
expect(UserSystemNotification.find(usn.id).seen_at).not_to be_nil
|
||||
expect(UserSystemNotification.find(usn.id).read_at).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue