ui: Dismiss notifications without page reload

- Delete only the <li> of the notification using HTMX.
- Notifications list stays open. User can dismiss another notification.
- Decrement notification counter using JavaScript after removing
  notification from the list.
- Added HTMX to every kind of notification.
- Tested dismissing notifications from the top, middle and bottom of the
  list.

Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net>
[sunil: Update comment format in .js file]
Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
Joseph Nuthalapati 2026-02-04 00:15:16 +05:30 committed by Sunil Mohan Adapa
parent 4e668c8a98
commit 13a575017c
No known key found for this signature in database
GPG Key ID: 43EA1CFF0AA7C5F2
6 changed files with 43 additions and 1 deletions

View File

@ -42,6 +42,9 @@
{% trans "Go to Distribution Update" %}
</a>
<a href="{% url 'notification_dismiss' id=id %}?next={{ request.path|iriencode }}"
hx-get="{% url 'notification_dismiss' id=id %}"
hx-target="#notification-{{ id }}"
hx-swap="delete swap:300ms"
role="button" class="btn btn-default">
{% trans "Dismiss" %}
</a>

View File

@ -18,6 +18,9 @@
<p>
<a href="{% url 'notification_dismiss' id=id %}?next={{ request.path|iriencode }}"
hx-get="{% url 'notification_dismiss' id=id %}"
hx-target="#notification-{{ id }}"
hx-swap="delete swap:300ms"
role="button" class="btn btn-default">
{% trans "Dismiss" %}
</a>

View File

@ -11,7 +11,8 @@
<div id="notifications" class="notifications collapse no-no-js" hx-swap-oob="true">
<ul>
{% for note in notifications %}
<li class="notification notification-{{ note.severity }}">
<li id="notification-{{ note.id }}"
class="notification notification-{{ note.severity }}">
<div class="notification-header">
<span class="notification-time"
title="{{ note.last_update_time|date:'DATETIME_FORMAT' }}">
@ -54,6 +55,9 @@
{% for action in note.actions %}
{% if action.type == "dismiss" %}
<a href="{% url 'notification_dismiss' id=note.id %}?next={{ request.path|iriencode }}"
hx-get="{% url 'notification_dismiss' id=note.id %}"
hx-target="#notification-{{ note.id }}"
hx-swap="delete swap:300ms"
role="button" class="btn btn-default">
{% trans "Dismiss" %}
</a>

View File

@ -19,6 +19,9 @@
{% if data.state == "completed" %}
<div class="btn-toolbar">
<a href="{% url 'notification_dismiss' id=id %}?next={{ request.path|iriencode }}"
hx-get="{% url 'notification_dismiss' id=id %}"
hx-target="#notification-{{ id }}"
hx-swap="delete swap:300ms"
role="button" class="btn btn-default">
{% trans "Dismiss" %}
</a>

View File

@ -682,10 +682,18 @@ class AppLogsView(TemplateView):
def notification_dismiss(request, id):
"""Dismiss a notification."""
from django.http import HttpResponse
from .notification import Notification
notes = Notification.list(key=id, user=request.user)
if notes:
# If a notification is not found, no need to dismiss it.
notes[0].dismiss()
# Don't redirect if the request is from HTMX.
if request.headers.get('HX-Request'):
response = HttpResponse(status=200)
# Trigger the notification-dismissed event to update the UI.
response['HX-Trigger'] = 'notification-dismissed'
return response
return HttpResponseRedirect(_get_redirect_url_from_param(request))

View File

@ -298,3 +298,24 @@ document.addEventListener('htmx:afterSwap', function (event) {
window.location.reload();
}
});
/*
* Decrement notification counter badge when a notification is dismissed via
* HTMX.
*/
document.addEventListener('notification-dismissed', function (evt) {
// There are 2 badges on the page. One for mobile navbar and one for desktop
// navbar.
const badges = document.querySelectorAll('.notifications-dropdown .badge');
badges.forEach(badge => {
const count = parseInt(badge.textContent.trim());
if (count > 1) {
badge.textContent = count - 1;
} else {
const dropdowns = document.querySelectorAll('.notifications-dropdown');
dropdowns.forEach(dropdown => dropdown.remove());
}
});
});