details: https://code.tryton.org/tryton/commit/80fddd54da71
branch: default
user: Cédric Krier <[email protected]>
date: Thu Feb 05 16:12:46 2026 +0100
description:
Fill blank state of empty notification menu
Closes #14575
diffstat:
sao/src/notification.js | 132 ++++++++++++++++++++-----------------
tryton/tryton/gui/notification.py | 74 +++++++++++---------
2 files changed, 110 insertions(+), 96 deletions(-)
diffs (240 lines):
diff -r 8d0759bc0e9a -r 80fddd54da71 sao/src/notification.js
--- a/sao/src/notification.js Sat Feb 14 09:49:11 2026 +0100
+++ b/sao/src/notification.js Thu Feb 05 16:12:46 2026 +0100
@@ -40,74 +40,82 @@
let Notification = new Sao.Model('res.notification');
Notification.execute('get', []).done((notifications) => {
this.el.empty();
- for (let notification of notifications) {
- let notification_item = jQuery('<div/>', {
- }).append(jQuery('<span/>', {
- 'class': 'notification-label',
- 'text': notification.label,
- 'title': notification.label,
- })).append(jQuery('<span/>', {
- 'class': 'notification-description',
- 'text': notification.description,
- 'title': notification.description,
- }));
- let link = jQuery('<a/>', {
- 'role': 'menuitem',
- 'href': '#',
- }).click((evt) => {
- evt.preventDefault();
- this.open(notification)
- }).append(notification_item);
- let li = jQuery('<li/>', {
- 'class': 'notification-item',
+ if (!notifications.length) {
+ jQuery('<li/>', {
+ 'class': 'dropdown-header',
'role': 'presentation',
- });
- let img = jQuery('<img/>', {
- 'class': 'icon',
- });
- link.prepend(img);
- Sao.common.ICONFACTORY.get_icon_url(
- notification.icon || 'tryton-notification')
- .then(url => {
- img.attr('src', url);
- // Append only when the url is known to prevent
- // layout shifts
- li.append(link);
+ }).text(Sao.i18n.gettext("No notifications at this time"))
+ .appendTo(this.el);
+ } else {
+ for (let notification of notifications) {
+ let notification_item = jQuery('<div/>', {
+ }).append(jQuery('<span/>', {
+ 'class': 'notification-label',
+ 'text': notification.label,
+ 'title': notification.label,
+ })).append(jQuery('<span/>', {
+ 'class': 'notification-description',
+ 'text': notification.description,
+ 'title': notification.description,
+ }));
+ let link = jQuery('<a/>', {
+ 'role': 'menuitem',
+ 'href': '#',
+ }).click((evt) => {
+ evt.preventDefault();
+ this.open(notification)
+ }).append(notification_item);
+ let li = jQuery('<li/>', {
+ 'class': 'notification-item',
+ 'role': 'presentation',
+ });
+ let img = jQuery('<img/>', {
+ 'class': 'icon',
});
- if (notification.unread) {
- li.addClass('notification-unread');
+ link.prepend(img);
+ Sao.common.ICONFACTORY.get_icon_url(
+ notification.icon || 'tryton-notification')
+ .then(url => {
+ img.attr('src', url);
+ // Append only when the url is known to prevent
+ // layout shifts
+ li.append(link);
+ });
+ if (notification.unread) {
+ li.addClass('notification-unread');
+ }
+ this.el.append(li)
}
- this.el.append(li)
+ this.el.append(
+ jQuery('<li/>', {
+ 'role': 'presentation',
+ 'class': 'notification-item notification-action',
+ }).append(jQuery('<a/>', {
+ 'role': 'menuitem',
+ 'href': '#',
+ 'title': Sao.i18n.gettext("All Notifications..."),
+ }).append(jQuery('<span/>', {
+ 'class': 'caret',
+ })).click((evt) => {
+ evt.preventDefault();
+ let params = {
+ context: jQuery.extend(
+ {}, Sao.Session.current_session.context),
+ domain: [
+ ['user', '=',
Sao.Session.current_session.user_id]],
+ };
+ params.model = 'res.notification';
+ Sao.Tab.create(params).done(() => {
+ this.indicator.hide();
+ });
+ }))
+ );
}
this.el.append(
jQuery('<li/>', {
- 'role': 'presentation',
- 'class': 'notification-item notification-action',
- }).append(jQuery('<a/>', {
- 'role': 'menuitem',
- 'href': '#',
- 'title': Sao.i18n.gettext("All Notifications..."),
- }).append(jQuery('<span/>', {
- 'class': 'caret',
- })).click((evt) => {
- evt.preventDefault();
- let params = {
- context: jQuery.extend({},
Sao.Session.current_session.context),
- domain: [['user', '=',
Sao.Session.current_session.user_id]],
- };
- params.model = 'res.notification';
- Sao.Tab.create(params).done(() => {
- this.indicator.hide();
- });
- }))
- );
- if (notifications.length > 0) {
- this.el.append(
- jQuery('<li/>', {
- 'role': 'separator',
- 'class': 'divider',
- }));
- }
+ 'role': 'separator',
+ 'class': 'divider',
+ }));
let preferences_img = jQuery('<img/>', {
'class': 'icon',
});
diff -r 8d0759bc0e9a -r 80fddd54da71 tryton/tryton/gui/notification.py
--- a/tryton/tryton/gui/notification.py Sat Feb 14 09:49:11 2026 +0100
+++ b/tryton/tryton/gui/notification.py Thu Feb 05 16:12:46 2026 +0100
@@ -33,48 +33,54 @@
notifications = rpc.execute(
'model', 'res.notification', 'get', rpc.CONTEXT)
- for notification in notifications:
- item = Gtk.MenuItem()
-
- hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
- hbox.set_margin_top(4)
- hbox.set_margin_bottom(4)
- hbox.set_margin_start(6)
- hbox.set_margin_end(6)
+ if not notifications:
+ item = Gtk.MenuItem(
+ label=_("No notifications at this time"))
+ item.set_sensitive(False)
+ menu.append(item)
+ else:
+ for notification in notifications:
+ item = Gtk.MenuItem()
- img = common.IconFactory.get_image(
- notification['icon'] or 'tryton-notification',
- Gtk.IconSize.MENU)
- hbox.pack_start(img, False, False, 0)
+ hbox = Gtk.Box(
+ orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
+ hbox.set_margin_top(4)
+ hbox.set_margin_bottom(4)
+ hbox.set_margin_start(6)
+ hbox.set_margin_end(6)
- vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
+ img = common.IconFactory.get_image(
+ notification['icon'] or 'tryton-notification',
+ Gtk.IconSize.MENU)
+ hbox.pack_start(img, False, False, 0)
- label = Gtk.Label(notification['label'])
- label.set_xalign(0)
- vbox.pack_start(label, False, False, 0)
+ vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
- description = Gtk.Label(notification['description'])
- description.set_xalign(0)
- description.get_style_context().add_class("dim-label")
- description.set_max_width_chars(30)
- description.set_line_wrap(True)
- description.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
- description.set_ellipsize(Pango.EllipsizeMode.END)
- description.set_lines(2)
- vbox.pack_start(description, False, False, 0)
+ label = Gtk.Label(notification['label'])
+ label.set_xalign(0)
+ vbox.pack_start(label, False, False, 0)
- hbox.pack_start(vbox, True, True, 0)
- item.add(hbox)
-
- item.connect('activate', self.open, notification)
+ description = Gtk.Label(notification['description'])
+ description.set_xalign(0)
+ description.get_style_context().add_class("dim-label")
+ description.set_max_width_chars(30)
+ description.set_line_wrap(True)
+ description.set_line_wrap_mode(Pango.WrapMode.WORD_CHAR)
+ description.set_ellipsize(Pango.EllipsizeMode.END)
+ description.set_lines(2)
+ vbox.pack_start(description, False, False, 0)
- if notification['unread']:
- style = item.get_style_context()
- style.add_class('unread-notification')
+ hbox.pack_start(vbox, True, True, 0)
+ item.add(hbox)
+
+ item.connect('activate', self.open, notification)
- menu.append(item)
+ if notification['unread']:
+ style = item.get_style_context()
+ style.add_class('unread-notification')
- if notifications:
+ menu.append(item)
+
menu.append(Gtk.SeparatorMenuItem())
def open_all_notifications(item):