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:
aignatov-bio 2019-02-19 11:08:59 +01:00 committed by GitHub
parent c598541e09
commit 8e905b67b2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 343 additions and 28 deletions

View file

@ -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;
});
})();

View file

@ -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') {

View file

@ -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;
}
}
}

View file

@ -87,6 +87,7 @@ table {
}
.notifications-dropdown-header,
.system-notifications-dropdown-header,
.dropdown-header {
background-color: $brand-primary;
color: $color-concrete;

View file

@ -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

View file

@ -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)

View file

@ -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? %>

View file

@ -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") %>

View file

@ -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" %>

View file

@ -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"

View file

@ -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'

View 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

View file

@ -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

View file

@ -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

View file

@ -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