2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 13:05:42 +00:00

Merge branch 'inventree:master' into matmair/issue2694

This commit is contained in:
Matthias Mair
2022-03-20 22:15:27 +01:00
committed by GitHub
35 changed files with 1442 additions and 182 deletions

View File

@ -0,0 +1,25 @@
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block label %}history{% endblock %}
{% block heading %}
{% trans "Notification History" %}
{% endblock %}
{% block actions %}
<div class='btn btn-secondary' type='button' id='history-refresh' title='{% trans "Refresh Notification History" %}'>
<span class='fa fa-sync'></span> {% trans "Refresh Notification History" %}
</div>
{% endblock %}
{% block content %}
<div class='row'>
<table class='table table-striped table-condensed' id='history-table'>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,28 @@
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block label %}inbox{% endblock %}
{% block heading %}
{% trans "Pending Notifications" %}
{% endblock %}
{% block actions %}
<div class='btn btn-secondary' type='button' id='mark-all' title='{% trans "Mark all as read" %}'>
<span class='fa fa-bookmark'></span> {% trans "Mark all as read" %}
</div>
<div class='btn btn-secondary' type='button' id='inbox-refresh' title='{% trans "Refresh Pending Notifications" %}'>
<span class='fa fa-sync'></span> {% trans "Refresh Pending Notifications" %}
</div>
{% endblock %}
{% block content %}
<div class='row'>
<table class='table table-striped table-condensed' id='inbox-table'>
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,154 @@
{% extends "base.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block breadcrumb_list %}
{% endblock %}
{% block page_title %}
{% inventree_title %} | {% trans "Notifications" %}
{% endblock %}
{% block sidebar %}
{% include "InvenTree/notifications/sidebar.html" %}
{% endblock %}
{% block content %}
{% include "InvenTree/notifications/inbox.html" %}
{% include "InvenTree/notifications/history.html" %}
{% endblock %}
{% block js_ready %}
{{ block.super }}
function updateNotificationTables() {
$("#inbox-table").bootstrapTable('refresh');
$("#history-table").bootstrapTable('refresh');
}
// this allows the global notification panel to update the tables
window.updateNotifications = updateNotificationTables
function loadNotificationTable(table, options={}, enableDelete=false) {
var params = options.params || {};
var read = typeof(params.read) === 'undefined' ? true : params.read;
$(table).inventreeTable({
url: options.url,
name: options.name,
groupBy: false,
search: true,
queryParams: {
ordering: 'age',
read: read,
},
paginationVAlign: 'bottom',
formatNoMatches: options.no_matches,
columns: [
{
field: 'pk',
title: '{% trans "ID" %}',
visible: false,
switchable: false,
},
{
field: 'age',
title: '{% trans "Age" %}',
sortable: 'true',
formatter: function(value, row) {
return row.age_human
}
},
{
field: 'category',
title: '{% trans "Category" %}',
sortable: 'true',
},
{
field: 'target',
title: '{% trans "Item" %}',
sortable: 'true',
formatter: function(value, row, index, field) {
if (value == null) {
return '';
}
var html = `${value.model}: ${value.name}`;
if (value.link ) {html = `<a href='${value.link}'>${html}</a>`;}
return html;
}
},
{
field: 'name',
title: '{% trans "Name" %}',
},
{
field: 'message',
title: '{% trans "Message" %}',
},
{
formatter: function(value, row, index, field) {
var bRead = getReadEditButton(row.pk, row.read)
if (enableDelete) {
var bDel = "<button title='{% trans "Delete Notification" %}' class='notification-delete btn btn-outline-secondary' type='button' pk='" + row.pk + "'><span class='fas fa-trash-alt icon-red'></span></button>";
} else {
var bDel = '';
}
var html = "<div class='btn-group float-right' role='group'>" + bRead + bDel + "</div>";
return html;
}
}
]
});
$(table).on('click', '.notification-read', function() {
updateNotificationReadState($(this));
});
}
loadNotificationTable("#inbox-table", {
name: 'inbox',
url: '{% url 'api-notifications-list' %}',
params: {
read: false,
},
no_matches: function() { return '{% trans "No unread notifications found" %}'; },
});
$("#inbox-refresh").on('click', function() {
$("#inbox-table").bootstrapTable('refresh');
});
$("#mark-all").on('click', function() {
inventreeGet(
'{% url "api-notifications-readall" %}',
{
read: false,
},
);
updateNotificationTables();
});
loadNotificationTable("#history-table", {
name: 'history',
url: '{% url 'api-notifications-list' %}',
no_matches: function() { return '{% trans "No notification history found" %}'; },
}, true);
$("#history-refresh").on('click', function() {
$("#history-table").bootstrapTable('refresh');
});
$("#history-table").on('click', '.notification-delete', function() {
constructForm(`/api/notifications/${$(this).attr('pk')}/`, {
method: 'DELETE',
title: '{% trans "Delete Notification" %}',
onSuccess: function(data) {
updateNotificationTables();
}
});
});
enableSidebar('notifications');
{% endblock %}

View File

@ -0,0 +1,11 @@
{% load i18n %}
{% load static %}
{% load inventree_extras %}
{% trans "Notifications" as text %}
{% include "sidebar_header.html" with text=text icon='fa-bell' %}
{% trans "Inbox" as text %}
{% include "sidebar_item.html" with label='inbox' text=text icon="fa-envelope" %}
{% trans "History" as text %}
{% include "sidebar_item.html" with label='history' text=text icon="fa-clock" %}

View File

@ -91,7 +91,7 @@
<!-- general InvenTree -->
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'notification.js' %}"></script>
<!-- fontawesome -->
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>

View File

@ -128,6 +128,7 @@
</div>
{% include 'modals.html' %}
{% include 'about.html' %}
{% include "notifications.html" %}
</div>
<!-- Scripts -->
@ -161,7 +162,6 @@
<script type='text/javascript' src="{% static 'script/randomColor.min.js' %}"></script>
<!-- general InvenTree -->
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
<!-- dynamic javascript templates -->
@ -189,8 +189,10 @@
<script type='text/javascript' src="{% i18n_static 'plugin.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'tables.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'table_filters.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'notification.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/solid.min.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/regular.min.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/brands.min.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.min.js' %}"></script>

View File

@ -4,7 +4,7 @@
{% load inventree_extras %}
{% block title %}
{% blocktrans with part=part.name %} The available stock for {{ part }} has fallen below the configured minimum level{% endblocktrans %}
{{ message }}
{% if link %}
<p>{% trans "Click on the following link to view this part" %}: <a href="{{ link }}">{{ link }}</a></p>
{% endif %}

View File

@ -0,0 +1,313 @@
{% load i18n %}
/* exported
showAlertOrCache,
showCachedAlerts,
startNotificationWatcher,
stopNotificationWatcher,
openNotificationPanel,
closeNotificationPanel,
*/
/*
* Add a cached alert message to sesion storage
*/
function addCachedAlert(message, options={}) {
var alerts = sessionStorage.getItem('inventree-alerts');
if (alerts) {
alerts = JSON.parse(alerts);
} else {
alerts = [];
}
alerts.push({
message: message,
style: options.style || 'success',
icon: options.icon,
});
sessionStorage.setItem('inventree-alerts', JSON.stringify(alerts));
}
/*
* Remove all cached alert messages
*/
function clearCachedAlerts() {
sessionStorage.removeItem('inventree-alerts');
}
/*
* Display an alert, or cache to display on reload
*/
function showAlertOrCache(message, cache, options={}) {
if (cache) {
addCachedAlert(message, options);
} else {
showMessage(message, options);
}
}
/*
* Display cached alert messages when loading a page
*/
function showCachedAlerts() {
var alerts = JSON.parse(sessionStorage.getItem('inventree-alerts')) || [];
alerts.forEach(function(alert) {
showMessage(
alert.message,
{
style: alert.style || 'success',
icon: alert.icon,
}
);
});
clearCachedAlerts();
}
/*
* Display an alert message at the top of the screen.
* The message will contain a "close" button,
* and also dismiss automatically after a certain amount of time.
*
* arguments:
* - message: Text / HTML content to display
*
* options:
* - style: alert style e.g. 'success' / 'warning'
* - timeout: Time (in milliseconds) after which the message will be dismissed
*/
function showMessage(message, options={}) {
var style = options.style || 'info';
var timeout = options.timeout || 5000;
var target = options.target || $('#alerts');
var details = '';
if (options.details) {
details = `<p><small>${options.details}</p></small>`;
}
// Hacky function to get the next available ID
var id = 1;
while ($(`#alert-${id}`).exists()) {
id++;
}
var icon = '';
if (options.icon) {
icon = `<span class='${options.icon}'></span>`;
}
// Construct the alert
var html = `
<div id='alert-${id}' class='alert alert-${style} alert-dismissible fade show' role='alert'>
${icon}
<b>${message}</b>
${details}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
target.append(html);
// Remove the alert automatically after a specified period of time
$(`#alert-${id}`).delay(timeout).slideUp(200, function() {
$(this).alert(close);
});
}
var notificationWatcher = null; // reference for the notificationWatcher
/**
* start the regular notification checks
**/
function startNotificationWatcher() {
notificationCheck(force=true);
notificationWatcher = setInterval(notificationCheck, 1000);
}
/**
* stop the regular notification checks
**/
function stopNotificationWatcher() {
clearInterval(notificationWatcher);
}
var notificationUpdateTic = 0;
/**
* The notification checker is initiated when the document is loaded. It checks if there are unread notifications
* if unread messages exist the notification indicator is updated
*
* options:
* - force: set true to force an update now (if you got in focus for example)
**/
function notificationCheck(force = false) {
notificationUpdateTic = notificationUpdateTic + 1;
// refresh if forced or
// if in focus and was not refreshed in the last 5 seconds
if (force || (document.hasFocus() && notificationUpdateTic >= 5)) {
notificationUpdateTic = 0;
inventreeGet(
'/api/notifications/',
{
read: false,
},
{
success: function(response) {
updateNotificationIndicator(response.length);
}
}
);
}
}
/**
* handles read / unread buttons and UI rebuilding
*
* arguments:
* - btn: element that got clicked / fired the event -> must contain pk and target as attributes
*
* options:
* - panel_caller: this button was clicked in the notification panel
**/
function updateNotificationReadState(btn, panel_caller=false) {
var url = `/api/notifications/${btn.attr('pk')}/${btn.attr('target')}/`;
inventreePut(url, {}, {
method: 'POST',
success: function() {
// update the notification tables if they were declared
if (window.updateNotifications) {
window.updateNotifications();
}
// update current notification count
var count = parseInt($('#notification-counter').html());
if (btn.attr('target') == 'read') {
count = count - 1;
} else {
count = count + 1;
}
// update notification indicator now
updateNotificationIndicator(count);
// remove notification if called from notification panel
if (panel_caller) {
btn.parent().parent().remove();
}
}
});
};
/**
* Returns the html for a read / unread button
*
* arguments:
* - pk: primary key of the notification
* - state: current state of the notification (read / unread) -> just pass what you were handed by the api
* - small: should the button be small
**/
function getReadEditButton(pk, state, small=false) {
if (state) {
var bReadText = '{% trans "Mark as unread" %}';
var bReadIcon = 'fas fa-bookmark icon-red';
var bReadTarget = 'unread';
} else {
var bReadText = '{% trans "Mark as read" %}';
var bReadIcon = 'far fa-bookmark icon-green';
var bReadTarget = 'read';
}
var style = (small) ? 'btn-sm ' : '';
return `<button title='${bReadText}' class='notification-read btn ${style}btn-outline-secondary' type='button' pk='${pk}' target='${bReadTarget}'><span class='${bReadIcon}'></span></button>`;
}
/**
* fills the notification panel when opened
**/
function openNotificationPanel() {
var html = '';
var center_ref = '#notification-center';
inventreeGet(
'/api/notifications/',
{
read: false,
},
{
success: function(response) {
if (response.length == 0) {
html = `<p class='text-muted'>{% trans "No unread notifications" %}</p>`;
} else {
// build up items
response.forEach(function(item, index) {
html += '<li class="list-group-item">';
// d-flex justify-content-between align-items-start
html += '<div>';
html += `<span class="badge rounded-pill bg-primary">${item.category}</span><span class="ms-2">${item.name}</span>`;
html += '</div>';
if (item.target) {
var link_text = `${item.target.model}: ${item.target.name}`;
if (item.target.link) {
link_text = `<a href='${item.target.link}'>${link_text}</a>`;
}
html += link_text;
}
html += '<div>';
html += `<span class="text-muted">${item.age_human}</span>`;
html += getReadEditButton(item.pk, item.read, true);
html += '</div></li>';
});
// package up
html = `<ul class="list-group">${html}</ul>`;
}
// set html
$(center_ref).html(html);
}
}
);
$(center_ref).on('click', '.notification-read', function() {
updateNotificationReadState($(this), true);
});
}
/**
* clears the notification panel when closed
**/
function closeNotificationPanel() {
$('#notification-center').html(`<p class='text-muted'>{% trans "Notifications will load here" %}</p>`);
}
/**
* updates the notification counter
**/
function updateNotificationIndicator(count) {
// reset update Ticker -> safe some API bandwidth
notificationUpdateTic = 0;
if (count == 0) {
$('#notification-alert').addClass('d-none');
} else {
$('#notification-alert').removeClass('d-none');
}
$('#notification-counter').html(count);
}

View File

@ -96,7 +96,18 @@
</button>
</li>
{% endif %}
<li class='nav-item' id='navbar-barcode-li'>
<li class='nav-item me-2'>
<button data-bs-toggle="offcanvas" data-bs-target="#offcanvasRight" class='btn position-relative' title='{% trans "Show Notifications" %}'>
<span class='fas fa-bell'></span>
<span class="position-absolute top-100 start-100 translate-middle badge rounded-pill bg-danger d-none" id="notification-alert">
<span class="visually-hidden">{% trans "New Notifications" %}</span>
<span id="notification-counter">0</span>
</span>
</button>
</li>
<li class='nav-item me-2'>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-objects" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>

View File

@ -0,0 +1,14 @@
{% load i18n %}
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" data-bs-scroll="true" aria-labelledby="offcanvasRightLabel">
<div class="offcanvas-header">
<h5 id="offcanvasRightLabel">{% trans "Notifications" %}</h5>
<button type="button" class="btn-close text-reset" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body">
<div id="notification-center">
<p class='text-muted'>{% trans "Notifications will load here" %}</p>
</div>
<hr>
<a href="{% url 'notifications' %}">{% trans "Show all notifications and history" %}</a>
</div>
</div>

View File

@ -77,7 +77,7 @@
<!-- general InvenTree -->
<script type='text/javascript' src="{% static 'script/inventree/inventree.js' %}"></script>
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<script type='text/javascript' src="{% i18n_static 'notification.js' %}"></script>
{% block body_scripts_inventree %}
{% endblock %}