mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 04:25:42 +00:00
* Catch DoesNotExist error * Move notificationtable function to js file * Fix for custom metadata class - Previously only worked if a POST or PUT action was available on the endpoint - So, a ListAPIView endpoint would not actually work! - Adding in a BulkDelete mixin to a ListAPIView caused failure * Add unit test to ensure new OPTIONS metadata updates are checked * Expand functionality of the existing BulkDelete mixin - Allow deletion by custom filters - Allow each implementing class to implement custom filters - Adds more unit testing for BulkDelete mixin class * Add bulk delete operation for Notification API - Ensure users can only delete their *own* notifications * Improve notification tables / buttons / etc * Adds unit testing for bulk delete of notifications - Fixed API permissions for notifications list endpoint * Update BulkDelete operations for the StockItemTestResult table * Use filters parameter in attachments table to ensure that only correct attachments are deleted * JS linting * Fixes for unit tests
410 lines
11 KiB
JavaScript
410 lines
11 KiB
JavaScript
{% load i18n %}
|
|
|
|
/* exported
|
|
loadNotificationTable,
|
|
showAlertOrCache,
|
|
showCachedAlerts,
|
|
startNotificationWatcher,
|
|
stopNotificationWatcher,
|
|
openNotificationPanel,
|
|
closeNotificationPanel,
|
|
*/
|
|
|
|
|
|
/*
|
|
* Load notification table
|
|
*/
|
|
function loadNotificationTable(table, options={}, enableDelete=false) {
|
|
|
|
var params = options.params || {};
|
|
var read = typeof(params.read) === 'undefined' ? true : params.read;
|
|
|
|
setupFilterList(`notifications-${options.name}`, table);
|
|
|
|
$(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));
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* 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);
|
|
},
|
|
error: function(xhr) {
|
|
console.warn('Could not access server: /api/notifications');
|
|
}
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 float-right' 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,
|
|
ordering: '-creation',
|
|
},
|
|
{
|
|
success: function(response) {
|
|
if (response.length == 0) {
|
|
html = `<p class='text-muted'><em>{% trans "No unread notifications" %}</em><span class='fas fa-check-circle icon-green float-right'></span></p>`;
|
|
} else {
|
|
// build up items
|
|
response.forEach(function(item, index) {
|
|
html += '<li class="list-group-item">';
|
|
html += `<div>`;
|
|
html += `<span class="badge bg-secondary rounded-pill">${item.name}</span>`;
|
|
html += getReadEditButton(item.pk, item.read, true);
|
|
html += `</div>`;
|
|
|
|
if (item.target) {
|
|
var link_text = `${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"><small>${item.age_human}</small></span>`;
|
|
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);
|
|
}
|