Skip to content
Permalink
Browse files

[IMP] web, *: share and improve notification system

* barcodes, calendar, partner_autocomplete, web_settings_dashboard

Make the notification system better, using bootstrap toast component,
and make it available in the frontend.

The toast component allowed to remove some JS code and made the
component customizable by themes but it had to be reviewed/fixed to make
it functional (maybe bootstrap will review its work in next versions).

This work should still be improved because there are still too many
ways (deprecated and not deprecated) to instantiate toasts in the
backend. The goal here was however to make the toasts more modern and
make them available in the frontend.

This work is needed for some tasks. @kig-odoo and @fja-odoo made a
pre-work to instantiate toasts in the frontend for their respective
tasks. This commit unifies the system.

Part of odoo#32793
task-1970731
  • Loading branch information...
qsm-odoo committed Apr 18, 2019
1 parent 6abfd26 commit 92a4891e6fb53e04aacf70d503da2f2d0bf7f0c4
Showing with 233 additions and 196 deletions.
  1. +1 −1 addons/barcodes/static/tests/barcode_tests.js
  2. +1 −1 addons/calendar/__manifest__.py
  3. +6 −4 addons/calendar/static/src/js/base_calendar.js
  4. +0 −15 addons/calendar/static/src/xml/base_calendar.xml
  5. +19 −0 addons/calendar/static/src/xml/notification_calendar.xml
  6. +1 −6 addons/partner_autocomplete/static/tests/partner_autocomplete_tests.js
  7. +28 −2 addons/web/static/src/js/core/service_mixins.js
  8. +4 −7 addons/web/static/src/js/services/notification_service.js
  9. +62 −46 addons/web/static/src/js/widgets/notification.js
  10. +9 −0 addons/web/static/src/scss/bootstrap_overridden.scss
  11. +8 −0 addons/web/static/src/scss/bootstrap_review.scss
  12. +1 −1 addons/web/static/src/scss/name_and_signature.scss
  13. +12 −0 addons/web/static/src/scss/notification.scss
  14. +0 −3 addons/web/static/src/scss/primary_variables.scss
  15. +0 −49 addons/web/static/src/scss/webclient.scss
  16. +0 −20 addons/web/static/src/xml/base.xml
  17. +36 −0 addons/web/static/src/xml/notification.xml
  18. +2 −2 addons/web/static/tests/chrome/action_manager_tests.js
  19. +1 −1 addons/web/static/tests/fields/relational_fields/field_one2many_tests.js
  20. +2 −1 addons/web/static/tests/helpers/test_utils.js
  21. +19 −18 addons/web/static/tests/services/notification_service_tests.js
  22. +13 −13 addons/web/static/tests/views/form_tests.js
  23. +1 −1 addons/web/static/tests/views/list_tests.js
  24. +4 −2 addons/web/views/webclient_templates.xml
  25. +3 −3 addons/web_settings_dashboard/static/tests/dashboard_tests.js
@@ -562,7 +562,7 @@ QUnit.test('barcode_scanned only trigger error for active view', async function
}
_.each(['O','-','B','T','N','.','c','a','n','c','e','l','Enter'], modalTriggerKeypressEvent);
await testUtils.nextTick();
assert.verifySteps(['warning'], "only one event should be triggered");
assert.verifySteps(['danger'], "only one event should be triggered");
form.destroy();
});
});
@@ -33,7 +33,7 @@
'views/calendar_views.xml',
'data/mail_activity_data.xml',
],
'qweb': ['static/src/xml/*.xml'],
'qweb': ['static/src/xml/base_calendar.xml'],
'installable': True,
'application': True,
'auto_install': False,
@@ -2,17 +2,19 @@ odoo.define('base_calendar.base_calendar', function (require) {
"use strict";

var BasicModel = require('web.BasicModel');
var field_registry = require('web.field_registry');
var fieldRegistry = require('web.field_registry');
var Notification = require('web.Notification');
var relational_fields = require('web.relational_fields');
var relationalFields = require('web.relational_fields');
var session = require('web.session');
var WebClient = require('web.WebClient');

var FieldMany2ManyTags = relational_fields.FieldMany2ManyTags;
var FieldMany2ManyTags = relationalFields.FieldMany2ManyTags;


var CalendarNotification = Notification.extend({
template: "CalendarNotification",
xmlDependencies: (Notification.prototype.xmlDependencies || [])
.concat(['/calendar/static/src/xml/notification_calendar.xml']),

init: function(parent, params) {
this._super(parent, params);
@@ -155,6 +157,6 @@ var Many2ManyAttendee = FieldMany2ManyTags.extend({
},
});

field_registry.add('many2manyattendee', Many2ManyAttendee);
fieldRegistry.add('many2manyattendee', Many2ManyAttendee);

});
@@ -6,20 +6,6 @@
</t>
</t>

<t t-name="CalendarNotification" t-extend="Notification">
<t t-jquery=".o_notification_title > t" t-operation="replace">
<span t-attf-class="link2event eid_{{widget.eid}}">
<t t-esc="widget.title"/>
</span>
</t>
<t t-jquery=".o_notification_content" t-operation="append">
<br/><br/>
<button type="button" class="btn btn-primary link2showed oe_highlight oe_form oe_button"><span>OK</span></button>
<button type="button" class="btn btn-link link2event">Details</button>
<button type="button" class="btn btn-link link2recall">Snooze</button>
</t>
</t>

<t t-extend="mail.systray.ActivityMenu.Previews">
<t t-jquery="div.o_preview_title" t-operation="after">
<div t-if="activity and activity.type == 'meeting'">
@@ -44,5 +30,4 @@
</div>
</t>
</t>

</template>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<template>

<t t-name="CalendarNotification" t-extend="Notification">
<t t-jquery=".o_notification_title > t" t-operation="replace">
<span t-attf-class="link2event eid_{{widget.eid}}">
<t t-esc="widget.title"/>
</span>
</t>
<t t-jquery=".o_notification_content" t-operation="after">
<div class="mt-2">
<button type="button" class="btn btn-primary link2showed oe_highlight oe_form oe_button"><span>OK</span></button>
<button type="button" class="btn btn-link link2event">Details</button>
<button type="button" class="btn btn-link link2recall">Snooze</button>
</div>
</t>
</t>

</template>
@@ -81,12 +81,7 @@ odoo.define('partner_autocomplete.tests', function (require) {
return this._getOdooSuggestions(value);
},
do_notify: function (title, message, sticky, className) {
return this.call('notification', 'notify', {
title: title,
message: message,
sticky: true,
className: 'o_partner_autocomplete_test_notify'
});
return this.displayNotification(title, message, 'warning', true, 'o_partner_autocomplete_test_notify');
},
});
});
@@ -222,11 +222,37 @@ var ServicesMixin = {
});
});
},
/**
* Displays a notification.
*
* @param {string} title
* @param {string} [message]
* @param {string} [type='warning'] 'info', 'warning', 'danger' or ''
* @param {boolean} [sticky=false]
* @param {string} [className]
*/
displayNotification: function (title, message, type, sticky, className) {
return this.call('notification', 'notify', {
type: type,
title: title,
message: message,
sticky: sticky,
className: className,
});
},
/**
* @deprecated will be removed as soon as the notification system is reviewed
* @see displayNotification
*/
do_notify: function (title, message, sticky, className) {
return this.call('notification', 'notify', {title: title, message: message, sticky: sticky, className: className});
return this.displayNotification(title, message, 'warning', sticky, className);
},
/**
* @deprecated will be removed as soon as the notification system is reviewed
* @see displayNotification
*/
do_warn: function (title, message, sticky, className) {
return this.call('notification', 'notify', {type: 'warning', title: title, message: message, sticky: sticky, className: className});
return this.displayNotification(title, message, 'danger', sticky, className);
},
};

@@ -1,9 +1,10 @@
odoo.define('web.NotificationService', function (require) {
"use strict";
'use strict';

var AbstractService = require('web.AbstractService');
var Notification = require('web.Notification');
var core = require('web.core');

var id = 0;

/**
@@ -16,9 +17,7 @@ var id = 0;
* by using this file. The proper way is to use the do_warn or do_notify
* methods on the Widget class.
*/

var NotificationService = AbstractService.extend({

custom_events: {
close: '_onCloseNotification',
},
@@ -37,8 +36,8 @@ var NotificationService = AbstractService.extend({

/**
* It may sometimes be useful to close programmatically a notification. For
* example, when there is a sticky notification that warns the user about
* some condition (connection lost), but the condition do not apply anymore.
* example, when there is a sticky notification warning the user about some
* condition (connection lost), but the condition does not apply anymore.
*
* @param {number} notificationId
* @param {boolean} [silent=false] if true, the notification does not call
@@ -105,9 +104,7 @@ var NotificationService = AbstractService.extend({
},
});


core.serviceRegistry.add('notification', NotificationService);


return NotificationService;
});
@@ -1,39 +1,38 @@
odoo.define('web.Notification', function (require) {
"use strict";
'use strict';

var Widget = require('web.Widget');

/**
* Notification
*
* This file contains the widget which is used to display a warning/information
* message in the top right of the screen.
* Widget which is used to display a warning/information message on the top
* right of the screen.
*
* If you want to display such a notification, you probably do not want to do it
* by importing this file. The proper way is to use the do_warn or do_notify
* methods on the Widget class.
*/

var Widget = require('web.Widget');

var Notification = Widget.extend({
template: 'Notification',
xmlDependencies: ['/web/static/src/xml/notification.xml'],
events: {
'click > .o_close': '_onClose',
'click .o_buttons button': '_onClickButton'
'hidden.bs.toast': '_onClose',
'click .o_notification_buttons button': '_onClickButton'
},
_autoCloseDelay: 2500,
_animationDelay: 400,
_animation: true,

/**
* @override
* @param {Widget} parent
* @param {Object} params
* @param {string} params.title notification title
* @param {string} params.message notification main message
* @param {string} params.type 'notification' or 'warning'
* @param {boolean} [params.sticky=false] if true, the notification will stay
* visible until the user clicks on it.
* @param {string} [params.className] className to add on the dom
* @param {string} params.title
* @param {string} [params.message]
* @param {string} [params.type='warning'] 'danger', 'warning', 'info' or ''
* @param {boolean} [params.sticky=false] if true, the notification will
* stay visible until the user clicks on it.
* @param {string} [params.className]
* @param {function} [params.onClose] callback when the user click on the x
* or when the notification is auto close (no sticky)
* or when the notification is auto close (no sticky)
* @param {Object[]} params.buttons
* @param {function} params.buttons[0].click callback on click
* @param {boolean} [params.buttons[0].primary] display the button as primary
@@ -46,55 +45,68 @@ var Notification = Widget.extend({
this.message = params.message;
this.buttons = params.buttons || [];
this.sticky = !!this.buttons.length || !!params.sticky;
this.type = params.type || 'notification';
this.type = params.type === undefined ? 'warning' : params.type;
this.className = params.className || '';
this._closeCallback = params.onClose;
this.icon = 'fa-lightbulb-o';

if (this.type === 'danger') {
this.icon = 'fa-exclamation';
this.className += ' bg-danger';
} else if (this.type === 'warning') {
this.icon = 'fa-lightbulb-o';
this.className += ' bg-warning';
} else if (this.type === 'info') {
this.icon = 'fa-info';
this.className += ' bg-info';
}

if (this.buttons && this.buttons.length) {
this.icon = 'fa-question-circle-o';
}
if (this.type === 'warning') {
this.icon = 'fa-exclamation';
this.className += ' o_error';
}
},
/**
* @override
*/
start: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
self.$el.animate({opacity: 1.0}, self._animationDelay, "swing", function () {
if(!self.sticky) {
setTimeout(function () {
self.close();
}, self._autoCloseDelay);
}
});
this.$el.toast({
animation: this._animation,
autohide: !this.sticky,
delay: this._autoCloseDelay,
});
void this.$el[0].offsetWidth; // Force a paint refresh before showing the toast
this.$el.toast('show');
return this._super.apply(this, arguments);
},
/**
* @override
*/
destroy: function () {
this.$el.toast('dispose');
this._super.apply(this, arguments);
},

//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------

/**
* This method is used to destroy the widget with a nice animation. We
* first perform an animation, then call the real destroy method.
* Destroys the widget with a nice animation.
*
* @private
* @param {boolean} [silent=false] if true, the notification does not call
* _closeCallback method
*/
close: function (silent) {
this.silent = silent;
this.$el.toast('hide');

// Make 'close' work if the notification is not shown yet but will be.
// Should not be needed but the calendar notification system is an
// example of feature that does not work without this yet.
var self = this;
this.trigger_up('close');
if (!silent && !this._buttonClicked) {
if (this._closeCallback) {
this._closeCallback();
}
}
this.$el.animate({opacity: 0.0, height: 0}, this._animationDelay, "swing", self.destroy.bind(self));
this.$el.one('shown.bs.toast', function () {
self.$el.toast('hide');
});
},

//--------------------------------------------------------------------------
@@ -120,14 +132,18 @@ var Notification = Widget.extend({
},
/**
* @private
* @param {MouseEvent} ev
* @param {Event} ev
*/
_onClose: function (ev) {
ev.preventDefault();
this.close();
this.trigger_up('close');
if (!this.silent && !this._buttonClicked) {
if (this._closeCallback) {
this._closeCallback();
}
}
this.destroy();
},
});

return Notification;

});
@@ -117,6 +117,15 @@ $nav-pills-border-radius: 0 !default;
$nav-pills-link-active-color: $white !default;
$nav-pills-link-active-bg: $o-brand-primary !default;

// Toasts

$toast-max-width: 300px !default;
$toast-padding-x: 1.5rem !default;
$toast-padding-y: 0.5rem !default;
$toast-font-size: $font-size-base !default;
$toast-background-color: rgba($white, .7) !default;
$toast-header-background-color: $toast-background-color !default;

// Badges

$badge-font-weight: normal !default;
@@ -41,6 +41,14 @@
}
}

.toast-header {
background-clip: border-box;
}
.toast-body {
// Same as card-body, see explanation above
@include o-bg-color(opacify($toast-background-color, 0.08));
}

// Modify modals so that their scrollable element is the modal-body (except in
// mobile).
@include media-breakpoint-up(sm) {

0 comments on commit 92a4891

Please sign in to comment.
You can’t perform that action at this time.