Merge pull request #1141 from ruwany/master

Notification center enhancements
merge-requests/1/head
Kamidu Sachith Punchihewa 7 years ago committed by GitHub
commit 48e67df534
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -209,4 +209,32 @@ public interface NotificationManagementService {
defaultValue = "1")
@PathParam("id") @Max(45)
int id);
@PUT
@Path("/clear-all")
@ApiOperation(
produces = MediaType.APPLICATION_JSON,
httpMethod = "PUT",
value = "Clear all notifications",
notes = "When a user needs to mark all the notifications as checked/read this " +
"function can be used to clear all notifications",
tags = "Device Notification Management",
extensions = {
@Extension(properties = {
@ExtensionProperty(name = Constants.SCOPE, value = "perm:notifications:mark-checked")
})
}
)
@ApiResponses(
value = {
@ApiResponse(
code = 200,
message = "OK"),
@ApiResponse(
code = 500,
message = "Error occurred while clearing notifications.")
}
)
Response clearAllNotifications();
}

@ -20,6 +20,7 @@ package org.wso2.carbon.device.mgt.jaxrs.service.impl;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.context.CarbonContext;
import org.wso2.carbon.device.mgt.common.PaginationRequest;
import org.wso2.carbon.device.mgt.common.PaginationResult;
import org.wso2.carbon.device.mgt.common.notification.mgt.Notification;
@ -102,4 +103,18 @@ public class NotificationManagementServiceImpl implements NotificationManagement
}
}
@Override
public Response clearAllNotifications() {
Notification.Status status = Notification.Status.CHECKED;
try {
int loggedinUserTenantId = CarbonContext.getThreadLocalCarbonContext()
.getTenantId();
DeviceMgtAPIUtils.getNotificationManagementService().updateAllNotifications(status, loggedinUserTenantId);
return Response.status(Response.Status.OK).build();
} catch (NotificationManagementException e) {
log.error("Error encountered while trying to clear all notifications.", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
}
}

@ -63,6 +63,16 @@ public interface NotificationManagementService {
boolean updateNotificationStatus(int notificationId, Notification.Status status) throws
NotificationManagementException;
/**
* Method for updating status all notifications.
*
* @return boolean status of the operation.
* @throws NotificationManagementException
* if something goes wrong while updating the Notification.
*/
boolean updateAllNotifications(Notification.Status status, int tenantID) throws
NotificationManagementException;
/**
* Method to fetch all the notifications in the database.
*

@ -141,6 +141,28 @@ public class NotificationManagementServiceImpl implements NotificationManagement
return true;
}
@Override
public boolean updateAllNotifications(Notification.Status status, int tenantID) throws
NotificationManagementException {
if (log.isDebugEnabled()) {
log.debug("Attempting to clear all notifications");
}
try {
NotificationManagementDAOFactory.beginTransaction();
notificationDAO.updateAllNotifications(status, tenantID);
NotificationManagementDAOFactory.commitTransaction();
} catch (TransactionManagementException e) {
NotificationManagementDAOFactory.rollbackTransaction();
throw new NotificationManagementException("Error occurred while updating notification", e);
} finally {
NotificationManagementDAOFactory.closeConnection();
}
if (log.isDebugEnabled()) {
log.debug("All notifications updated successfully.");
}
return true;
}
@Override
public List<Notification> getAllNotifications() throws NotificationManagementException {
try {

@ -61,6 +61,15 @@ public interface NotificationDAO {
int updateNotificationStatus(int notificationId, Notification.Status status)
throws NotificationManagementException;
/**
* Update status of all notifications.
*
* @return returns the no of updated records.
* @throws NotificationManagementException
*/
int updateAllNotifications(Notification.Status status, int tenantID) throws
NotificationManagementException;
/**
* This method is used to get all notifications based on tenant-id.
*

@ -142,6 +142,28 @@ public abstract class AbstractNotificationDAOImpl implements NotificationDAO {
return rows;
}
@Override
public int updateAllNotifications(Notification.Status status, int tenantID)
throws NotificationManagementException {
Connection conn;
PreparedStatement stmt = null;
int rows;
try {
conn = NotificationManagementDAOFactory.getConnection();
String sql = "UPDATE DM_NOTIFICATION SET STATUS = ? WHERE TENANT_ID= ?";
stmt = conn.prepareStatement(sql);
stmt.setString(1, status.toString());
stmt.setInt(2, tenantID);
rows = stmt.executeUpdate();
} catch (Exception e) {
throw new NotificationManagementException("Error while trying to clear all " +
"notifications", e);
} finally {
NotificationDAOUtil.cleanupResources(stmt, null);
}
return rows;
}
@Override
public List<Notification> getAllNotifications(int tenantId) throws NotificationManagementException {
Connection conn;

@ -216,5 +216,12 @@ public class NotificationManagementServiceImplTests {
" as added no. of records.");
}
@Test(dependsOnMethods = "updateStatusofAllNotification", description = "this tries to " +
"update the status of all notifications")
public void updateStatusOfAllNotifications() throws NotificationManagementException {
Assert.assertTrue(notificationManagementService.updateAllNotifications(Notification
.Status.CHECKED, -1234));
}
}

@ -42,10 +42,8 @@
</a>
</div>
<div class="wr-hidden-operations-content col-lg-8" id="ast-container">
<div id="noNotificationtxt" class="panel-body">
No unread messages
</div>
<div class="wr-hidden-operations-content col-lg-8" style="padding: 5px"
id="ast-container">
</div>
</div>
</div>
@ -55,8 +53,13 @@
</div>
{{/zone}}
{{#zone "bottomJs"}}
<script id="notification-listing" data-current-user="{{@user.username}}"
data-image-resource="{{@unit.publicUri}}/images/" src="{{@unit.publicUri}}/templates/notification-listing.hbs"
<script id="notification-listing-new" data-current-user="{{@user.username}}"
data-image-resource="{{@unit.publicUri}}/images/" src="{{@unit
.publicUri}}/templates/notification-listing-new.hbs"
type="text/x-handlebars-template"></script>
<script id="notification-listing-all" data-current-user="{{@user.username}}"
data-image-resource="{{@unit.publicUri}}/images/" src="{{@unit
.publicUri}}/templates/notification-listing-all.hbs"
type="text/x-handlebars-template"></script>
{{js "js/notification-listing.js"}}
{{/zone}}

@ -17,6 +17,7 @@
*/
var deviceMgtAPIBaseURI = "/api/device-mgt/v1.0";
var notificationsAvailable = false;
/**
* Following function would execute
@ -28,29 +29,74 @@ function InitiateViewOption() {
}
function loadNotifications() {
var deviceListing = $("#notification-listing");
var deviceListingSrc = deviceListing.attr("src");
var currentUser = deviceListing.data("currentUser");
var deviceListingNew = $("#notification-listing-new");
var deviceListingNewSrc = deviceListingNew.attr("src");
var deviceListingAll = $("#notification-listing-all");
var deviceListingAllSrc = deviceListingAll.attr("src");
$.template(
"notification-listing",
deviceListingSrc,
"notification-listing-new",
deviceListingNewSrc,
function (template) {
invokerUtil.get(
deviceMgtAPIBaseURI + "/notifications",
deviceMgtAPIBaseURI + "/notifications?status=NEW",
// on success
function (data, textStatus, jqXHR) {
if (jqXHR.status == 200 && data) {
data = JSON.parse(data);
if (data["notifications"] && data["notifications"].length > 0) {
if (data["notifications"]) {
notificationsAvailable = true;
var viewModel = {};
viewModel["notifications"] = data["notifications"];
viewModel["appContext"] = context;
var content = template(viewModel);
$("#ast-container").html(content);
$("#ast-container").append(content);
var settings = {
"sorting" : false
};
$("#unread-notifications").datatables_extended(settings);
/**
* append advance operations to list table toolbar
*/
$('#unread-notifications_wrapper').find('.dataTablesTop' +
' .dataTables_toolbar').html(
"<a\ class=\"btn btn-primary\"" +
" data-click-event=\"clear-notification\">Clear All" +
" Notifications</a>"
);
}
}
},
// on error
function (jqXHR) {
console.log(jqXHR.status);
}
);
}
);
$.template(
"notification-listing-all",
deviceListingAllSrc,
function (template) {
invokerUtil.get(
deviceMgtAPIBaseURI + "/notifications",
// on success
function (data, textStatus, jqXHR) {
if (jqXHR.status == 200 && data) {
data = JSON.parse(data);
if (data["notifications"]) {
notificationsAvailable = true;
var viewModel = {};
viewModel["notifications"] = data["notifications"];
viewModel["appContext"] = context;
var content = template(viewModel);
$("#ast-container").append(content);
var settings = {
"sorting" : false
};
$("#all-notifications").datatables_extended(settings);
}
}
@ -68,16 +114,9 @@ function loadNotifications() {
function showAdvanceOperation(operation, button) {
$(button).addClass('selected');
$(button).siblings().removeClass('selected');
if ($(button).attr("id") == 'allNotifications') {
$("#noNotificationtxt").html('You do not have any notifications ');
} else if ($(button).attr("id") == 'unReadNotifications') {
$("#noNotificationtxt").html('You do not have any unread notifications ');
} else {
$("#noNotificationtxt").html('You do not have any unread notifications ');
}
var hiddenOperation = ".wr-hidden-operations-content > div";
$(hiddenOperation + '[data-operation="' + operation + '"]').show();
$(hiddenOperation + '[data-operation="' + operation + '"]').siblings().hide();
$(hiddenOperation + '[data-operation="' + operation + '"]').show();
}
$(document).ready(function () {
@ -119,4 +158,40 @@ $(document).ready(function () {
}
);
});
if(notificationsAvailable) {
$("#notification-clear-button").removeClass("hidden");
}
$("#ast-container").on("click", ".btn", function (e) {
var clickEvent = $(this).data('click-event');
if(clickEvent == "clear-notification"){
e.preventDefault();
var markAsReadNotificationsAPI = "/api/device-mgt/v1.0/notifications/clear-all";
var messageSideBar = ".sidebar-messages";
var clickEvent = $(this).data('click-event');
var eventHandler = $(this);
invokerUtil.put(
markAsReadNotificationsAPI,
null,
function (data) {
$('.message').remove();
$("#notification-bubble").html(0);
var undreadNotifications = $("#unread-notifications");
undreadNotifications.find("tbody").empty();
undreadNotifications.find("tbody").append("<tr><td colspan=''>No data" +
" available in table</td></tr>");
}, function () {
var content = "<li class='message message-danger'><h4><i class='icon fw fw-error'></i>Warning</h4>" +
"<p>Unexpected error occurred while loading notification. Please refresh the page and" +
" try again</p></li>";
$(messageSideBar).html(content);
}
);
}
});
});

@ -0,0 +1,42 @@
<!-- all -->
<div class="wr-hidden-operation" data-operation="all">
<div class="panel panel-default">
<div id="all-body" class="panel-collapse panel-body" role="tabpanel">
<div id="error-msg" class="alert alert-danger hidden" role="alert">
<i class="icon fw fw-error"></i><span></span>
</div>
<table class="table table-striped table-hover table-bordered display data-table" id="all-notifications">
<thead style="display: none;">
<tr class="sort-row">
<th>Description</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{{#if notifications}}
{{#each notifications}}
<tr data-type="selectable" data-id="{{id}}">
<td data-display="{{description}}" data-grid-label="Description">{{description}}</td>
<td style="text-align: center;">
<a href="device/{{deviceType}}?id={{deviceIdentifier}}" data-click-event="remove-form">
<span class="fw-stack">
<i class="fw fw-circle-outline fw-stack-2x"></i>
<i class="fw fw-view fw-stack-1x"></i>
</span>
</a>
</td>
</tr>
{{/each}}
{{else}}
<div>
You do not have any notifications
</div>
{{/if}}
<br class="c-both" />
</tbody>
</table>
</div>
</div>
</div>
<!-- /all -->

@ -0,0 +1,44 @@
<!-- unread -->
<div class="wr-hidden-operation" data-operation="unread" style="display: block">
<div class="panel panel-default">
<div id="unread-body" class="panel-collapse panel-body" role="tabpanel">
<div id="error-msg" class="alert alert-danger hidden" role="alert">
<i class="icon fw fw-error"></i><span></span>
</div>
<table class="table table-striped table-hover table-bordered display data-table" id="unread-notifications">
<thead style="display: none;">
<tr class="sort-row">
<th>Description</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{{#if notifications}}
{{#each notifications}}
{{#equal "NEW" status }}
<tr data-type="selectable" data-id="{{id}}">
<td data-display="{{description}}" data-grid-label="Description">{{description}}</td>
<td style="text-align: center;">
<a href="device/{{deviceType}}?id={{deviceIdentifier}}" data-id="{{id}}" data-url="device/{{deviceType}}?id={{deviceIdentifier}}" class="new-notification" data-click-event="remove-form">
<span class="fw-stack">
<i class="fw fw-circle-outline fw-stack-2x"></i>
<i class="fw fw-view fw-stack-1x"></i>
</span>
</a>
</td>
</tr>
{{/equal}}
{{/each}}
{{else}}
<div>
You do not have any unread notifications
</div>
{{/if}}
<br class="c-both" />
</tbody>
</table>
</div>
</div>
</div>
<!-- unread-->

@ -1,75 +0,0 @@
<!-- unread -->
<div class="wr-hidden-operation" data-operation="unread" style="display: block">
<div class="panel panel-default">
<div id="unread-body" class="panel-collapse panel-body" role="tabpanel">
<div id="error-msg" class="alert alert-danger hidden" role="alert">
<i class="icon fw fw-error"></i><span></span>
</div>
<table class="table table-striped table-hover table-bordered display data-table" id="unread-notifications">
<thead style="display: none;">
<tr class="sort-row">
<th>Description</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{{#each notifications}}
{{#equal "NEW" status }}
<tr data-type="selectable" data-id="{{id}}">
<td data-display="{{description}}" data-grid-label="Description">{{description}}</td>
<td style="text-align: center;">
<a href="device/{{deviceType}}?id={{deviceIdentifier}}" data-id="{{id}}" data-url="device/{{deviceType}}?id={{deviceIdentifier}}" class="new-notification" data-click-event="remove-form">
<span class="fw-stack">
<i class="fw fw-circle-outline fw-stack-2x"></i>
<i class="fw fw-view fw-stack-1x"></i>
</span>
</a>
</td>
</tr>
{{/equal}}
{{/each}}
<br class="c-both" />
</tbody>
</table>
</div>
</div>
</div>
<!-- unread-->
<!-- all -->
<div class="wr-hidden-operation" data-operation="all">
<div class="panel panel-default">
<div id="all-body" class="panel-collapse panel-body" role="tabpanel">
<div id="error-msg" class="alert alert-danger hidden" role="alert">
<i class="icon fw fw-error"></i><span></span>
</div>
<table class="table table-striped table-hover table-bordered display data-table" id="all-notifications">
<thead style="display: none;">
<tr class="sort-row">
<th>Description</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{{#each notifications}}
<tr data-type="selectable" data-id="{{id}}">
<td data-display="{{description}}" data-grid-label="Description">{{description}}</td>
<td style="text-align: center;">
<a href="device/{{deviceType}}?id={{deviceIdentifier}}" data-click-event="remove-form">
<span class="fw-stack">
<i class="fw fw-circle-outline fw-stack-2x"></i>
<i class="fw fw-view fw-stack-1x"></i>
</span>
</a>
</td>
</tr>
{{/each}}
<br class="c-both" />
</tbody>
</table>
</div>
</div>
</div>
<!-- /all -->

@ -39,8 +39,8 @@
</li>
{{/if}}
{{#unless isCloud}}
{{#if permissions.VIEW_DASHBOARD}}
<!--<li>
{{#if permissions.VIEW_DASHBOARD}}
<!--<li>
<a href="javascript:statisticLoad('/portal/t/{{currentUser.domain}}/dashboards/device-statistics-dashboard/')">
<i class="fw fw-bar-chart"></i>Device Statistics
</a>
@ -65,7 +65,7 @@
</div>
</div>
</li>-->
{{/if}}
{{/if}}
{{/unless}}
@ -126,12 +126,12 @@
{{/if}}
{{/if}}
{{#if permissions.IS_ADMIN}}
<li>
<a href="{{@app.context}}/device-locations">
<i class="fw fw-map-location"></i>
Device Locations
</a>
</li>
<li>
<a href="{{@app.context}}/device-locations">
<i class="fw fw-map-location"></i>
Device Locations
</a>
</li>
{{/if}}
@ -154,11 +154,14 @@
</ul>
{{/zone}}
{{#zone "sidePanes"}}
<div class="sidebar-wrapper sidebar-wrapper-animation-fix" id="right-sidebar" is-authorized="{{isAuthorizedForNotifications}}"
<div class="sidebar-wrapper sidebar-wrapper-animation-fix" id="right-sidebar" is-authorized="{{isAuthorizedForNotifications}}"
data-side="right" data-width="320" data-sidebar-fixed="true" data-top="90" data-fixed-offset="90" data-offset-top="50" data-spy="affix">
<ul class="sidebar-messages">
</ul>
<div class="text-center"><a href="{{@app.context}}/notification-listing" class="btn btn-primary">Show All Notifications</a></div>
<div class="text-center"><a href="{{@app.context}}/notification-listing" class="btn
btn-primary all-notifications">Show All Notifications</a></div>
<div id="right-side-pane-notifications-clear" class="text-center"><a class="btn
btn-primary" data-click-event="clear-notification">Clear All Notifications</a></div>
</div>
{{/zone}}
{{#zone "bottomJs"}}

@ -106,7 +106,7 @@ function loadNewNotifications() {
$(messageSideBar).html(
"<h4 class='text-center'>No New Notifications</h4>" +
"<h5 class='text-center text-muted'>" +
"Check this section for error notifications<br>related to device operations" +
"Check this section for error notifications<br>related to device operations" +
"</h5>"
);
}
@ -425,18 +425,24 @@ $.fn.collapse_nav_sub = function () {
$(document).ready(function () {
loadNotificationsPanel();
$("#right-sidebar").on("click", ".new-notification", function () {
var notificationId = $(this).data("id");
$("#right-sidebar").on("click", ".new-notification", function (e) {
e.preventDefault();
var notificationId = $(this).parents('li').find('h4 a').data('id');
var redirectUrl = $(this).data("url");
var markAsReadNotificationsAPI = "/api/device-mgt/v1.0/notifications/" + notificationId + "/mark-checked";
var messageSideBar = ".sidebar-messages";
var clickEvent = $(this).data('click-event');
var eventHandler = $(this);
invokerUtil.put(
markAsReadNotificationsAPI,
null,
function (data) {
data = JSON.parse(data);
if (data.statusCode == responseCodes["ACCEPTED"]) {
if(clickEvent && clickEvent === 'remove-notification'){
$(eventHandler).parents('li').slideUp();
$("#notification-bubble").html(parseInt($("#notification-bubble").text()) - 1);
}else {
location.href = redirectUrl;
}
}, function () {
@ -448,6 +454,34 @@ $(document).ready(function () {
);
});
$("#right-sidebar").on("click", ".btn", function (e) {
var clickEvent = $(this).data('click-event');
if(clickEvent == "clear-notification"){
e.preventDefault();
var markAsReadNotificationsAPI = "/api/device-mgt/v1.0/notifications/clear-all";
var messageSideBar = ".sidebar-messages";
var clickEvent = $(this).data('click-event');
var eventHandler = $(this);
invokerUtil.put(
markAsReadNotificationsAPI,
null,
function (data) {
$('.message').remove();
$("#notification-bubble").html(0);
}, function () {
var content = "<li class='message message-danger'><h4><i class='icon fw fw-error'></i>Warning</h4>" +
"<p>Unexpected error occurred while loading notification. Please refresh the page and" +
" try again</p></li>";
$(messageSideBar).html(content);
}
);
}
});
if (typeof $.fn.collapse == 'function') {
$('.navbar-collapse.tiles').on('shown.bs.collapse', function () {
$(this).collapse_nav_sub();
@ -456,59 +490,59 @@ $(document).ready(function () {
});
function statisticLoad(redirectUrl) {
var contentType = "application/json";
var defaultStatusClasses = "fw fw-stack-1x";
var content = $("#statistic-response-template").find(".content");
var title = content.find("#title");
var statusIcon = content.find("#status-icon");
$.ajax({
url: redirectUrl,
type: "GET",
success: function () {
window.location.href = redirectUrl;
},
error: function() {
var urix = backendEndBasePath + "/admin/publish-artifact/deploy/device_management";
var device = {};
invokerUtil.post(urix, device, function (data) {
title.html("Deploying statistic artifacts. Please wait...");
statusIcon.attr("class", defaultStatusClasses + " fw-check");
$(modalPopupContent).html(content.html());
showPopup();
poll(redirectUrl);
}, function (jqXHR) {
title.html("Failed to deploy artifacts, Please contact administrator.");
statusIcon.attr("class", defaultStatusClasses + " fw-error");
$(modalPopupContent).html(content.html());
showPopup();
}, contentType);
}
});
var contentType = "application/json";
var defaultStatusClasses = "fw fw-stack-1x";
var content = $("#statistic-response-template").find(".content");
var title = content.find("#title");
var statusIcon = content.find("#status-icon");
$.ajax({
url: redirectUrl,
type: "GET",
success: function () {
window.location.href = redirectUrl;
},
error: function() {
var urix = backendEndBasePath + "/admin/publish-artifact/deploy/device_management";
var device = {};
invokerUtil.post(urix, device, function (data) {
title.html("Deploying statistic artifacts. Please wait...");
statusIcon.attr("class", defaultStatusClasses + " fw-check");
$(modalPopupContent).html(content.html());
showPopup();
poll(redirectUrl);
}, function (jqXHR) {
title.html("Failed to deploy artifacts, Please contact administrator.");
statusIcon.attr("class", defaultStatusClasses + " fw-error");
$(modalPopupContent).html(content.html());
showPopup();
}, contentType);
}
});
}
var pollingCount = 15;
function poll(portalUrl) {
var content = $("#statistic-response-template").find(".content");
var title = content.find("#title");
var defaultStatusClasses = "fw fw-stack-1x";
var statusIcon = content.find("#status-icon");
$.ajax({
url: portalUrl,
type: "GET",
success: function (data) {
window.location.href = portalUrl;
},
dataType: "json",
error: setTimeout(function () {
pollingCount = pollingCount - 1;
if (pollingCount > 0) {
poll(portalUrl);
} else {
window.location.href = portalUrl;
}
}, 5000),
timeout: 5000
});
var content = $("#statistic-response-template").find(".content");
var title = content.find("#title");
var defaultStatusClasses = "fw fw-stack-1x";
var statusIcon = content.find("#status-icon");
$.ajax({
url: portalUrl,
type: "GET",
success: function (data) {
window.location.href = portalUrl;
},
dataType: "json",
error: setTimeout(function () {
pollingCount = pollingCount - 1;
if (pollingCount > 0) {
poll(portalUrl);
} else {
window.location.href = portalUrl;
}
}, 5000),
timeout: 5000
});
}

@ -1,14 +1,27 @@
{{#each notifications}}
<li class="message message-info" data-type="selectable" >
<h4>
<i class="icon fw fw-info"></i>
<a href="{{../context}}/device/{{deviceType}}?id={{deviceIdentifier}}"
data-id="{{id}}"
data-url="device?type={{deviceType}}&id={{deviceIdentifier}}"
class="new-notification" data-click-event="remove-form">
{{deviceType}} : {{deviceName}}
</a>
</h4>
<p>{{description}}</p>
<table>
<td>
<h4>
<i class="icon fw fw-info"></i>
<a href="{{../context}}/device/{{deviceType}}?id={{deviceIdentifier}}"
data-id="{{id}}"
data-url="{{../context}}/device/{{deviceType}}?id={{deviceIdentifier}}"
class="new-notification" data-click-event="remove-form">
{{deviceType}} : {{deviceName}}
</a>
</h4>
<p>{{description}}</p>
</td>
<td>
<a class="new-notification" data-click-event="remove-notification">
<span class="fw-stack">
<i class="fw fw-circle-outline fw-stack-2x"></i>
<i class="fw fw-delete fw-stack-1x"></i>
</span>
</a>
</a>
</td>
</table>
</li>
{{/each}}

@ -1178,8 +1178,12 @@ header .fw-user:before{
right: 0;
}
.all-notifications{
margin-bottom: 10px !important;
}
.sidebar-wrapper.toggled a.btn{
margin-bottom: 70px;
margin-bottom: 70px;
}
.sidebar-wrapper-animation-fix {

Loading…
Cancel
Save