[REF] web: move crash manager to frontend only
Previously, we removed the crash manager and moved its responsibilities to the error service. Because the crash manager was used in both the frontend and backend, and because the infrastructure code required to use the error service in the frontend hasn't been written yet, the crash manager has been moved to the frontend assets where it is still needed in the interim. Most of the logic of the crash manager has been moved to public_crash_manager (as it is now the only crash manager and it doesn't make sense to keep the inheritance hierarchy). References to the crash manager in the backend were adapted where needed or removed where they were no longer useful. closes odoo-dev/odoo#943 Related: odoo-dev/enterprise#174 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
committed by
Aaron Bohy
parent
c37e5efa48
commit
8a888bca8a
@@ -194,7 +194,6 @@
|
||||
'point_of_sale.qunit_suite_tests': [
|
||||
'web/static/tests/legacy/component_extension_tests.js',
|
||||
'point_of_sale/static/tests/unit/**/*',
|
||||
('remove', 'point_of_sale/static/tests/unit/helpers/test_main.js')
|
||||
],
|
||||
'point_of_sale.assets_backend_prod_only': [
|
||||
'point_of_sale/static/src/js/chrome_adapter.js',
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
odoo.define('web.web_client', function (require) {
|
||||
// this module is required by the test
|
||||
const { bus } = require('web.core');
|
||||
const WebClient = require('web.AbstractWebClient');
|
||||
|
||||
// listen to unhandled rejected promises, and when the rejection is not due
|
||||
// to a crash, prevent the browser from displaying an 'unhandledrejection'
|
||||
// error in the console, which would make tests crash on each Promise.reject()
|
||||
// something similar is done by the CrashManagerService, but by default, it
|
||||
// isn't deployed in tests
|
||||
bus.on('crash_manager_unhandledrejection', this, function (ev) {
|
||||
if (!ev.reason || !(ev.reason instanceof Error)) {
|
||||
ev.stopPropagation();
|
||||
ev.stopImmediatePropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
owl.config.mode = "dev";
|
||||
|
||||
const webClient = new WebClient();
|
||||
return webClient;
|
||||
});
|
||||
@@ -204,7 +204,6 @@ This module provides the core of the Odoo Web Client.
|
||||
|
||||
'web/static/src/legacy/js/services/session.js',
|
||||
'web/static/src/legacy/js/public/public_env.js',
|
||||
'web/static/src/legacy/js/public/public_crash_manager.js',
|
||||
'web/static/src/legacy/js/public/public_root.js',
|
||||
'web/static/src/legacy/js/public/public_root_instance.js',
|
||||
'web/static/src/legacy/js/public/public_widget.js',
|
||||
@@ -396,7 +395,6 @@ This module provides the core of the Odoo Web Client.
|
||||
'web/static/src/legacy/js/services/config.js',
|
||||
'web/static/src/legacy/js/services/core.js',
|
||||
'web/static/src/legacy/js/services/local_storage_service.js',
|
||||
'web/static/src/legacy/js/services/crash_manager.js',
|
||||
'web/static/src/legacy/js/core/error_utils.js',
|
||||
'web/static/src/legacy/js/services/session_storage_service.js',
|
||||
'web/static/src/legacy/js/common_env.js',
|
||||
@@ -408,7 +406,6 @@ This module provides the core of the Odoo Web Client.
|
||||
|
||||
# Used during the transition of the web architecture
|
||||
'web.frontend_legacy': [
|
||||
# Old rainbow man kept for website compatibility
|
||||
'web/static/src/legacy/frontend/**/*',
|
||||
],
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export class ConnectionAbortedError extends Error {}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Main RPC method
|
||||
// -----------------------------------------------------------------------------
|
||||
function makeErrorFromResponse(reponse) {
|
||||
export function makeErrorFromResponse(reponse) {
|
||||
// Odoo returns error like this, in a error field instead of properly
|
||||
// using http error codes...
|
||||
const { code, data: errorData, message, type: subType } = reponse;
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
odoo.define('web.CrashManager', function (require) {
|
||||
"use strict";
|
||||
|
||||
const AbstractService = require('web.AbstractService');
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var Widget = require('web.Widget');
|
||||
var config = require('web.config');
|
||||
var { formatTraceback, annotateTraceback } = require("@web/legacy/js/core/error_utils");
|
||||
|
||||
var _t = core._t;
|
||||
var _lt = core._lt;
|
||||
|
||||
// Register this eventlistener before qunit does.
|
||||
// Some errors needs to be negated by the crash_manager.
|
||||
window.addEventListener('unhandledrejection', ev =>
|
||||
core.bus.trigger('crash_manager_unhandledrejection', ev)
|
||||
);
|
||||
|
||||
let active = true;
|
||||
|
||||
/**
|
||||
* An extension of Dialog Widget to render the warnings and errors on the website.
|
||||
* Extend it with your template of choice like ErrorDialog/WarningDialog
|
||||
*/
|
||||
var CrashManagerDialog = Dialog.extend({
|
||||
xmlDependencies: (Dialog.prototype.xmlDependencies || []).concat(
|
||||
['/web/static/src/legacy/frontend/public_crash_manager/public_crash_manager.xml']
|
||||
),
|
||||
|
||||
/**
|
||||
* @param {Object} error
|
||||
* @param {string} error.message the message in Warning/Error Dialog
|
||||
* @param {string} error.traceback the traceback in ErrorDialog
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, options, error) {
|
||||
this._super.apply(this, [parent, options]);
|
||||
this.error = error;
|
||||
this.message = error.message;
|
||||
this.traceback = error.traceback;
|
||||
core.bus.off('close_dialogs', this);
|
||||
},
|
||||
|
||||
willStart: async function () {
|
||||
await this._super()
|
||||
const jsError = this.error.data && this.error.data.jsError;
|
||||
if (jsError) {
|
||||
if (config.isDebug('assets')) {
|
||||
this.traceback = await annotateTraceback(jsError);
|
||||
} else {
|
||||
this.traceback = formatTraceback(jsError);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
start: async function () {
|
||||
await this._super();
|
||||
const message = this.message;
|
||||
const traceback = this.traceback;
|
||||
|
||||
if (!traceback) {
|
||||
return;
|
||||
}
|
||||
this.$(".o_error_detail").on("shown.bs.collapse", function (e) {
|
||||
e.target.scrollTop = e.target.scrollHeight;
|
||||
});
|
||||
|
||||
const $clipboardBtn = this.$(".o_clipboard_button");
|
||||
$clipboardBtn.tooltip({title: _t("Copied !"), trigger: "manual", placement: "left"});
|
||||
this.clipboard = new window.ClipboardJS($clipboardBtn[0], {
|
||||
text: function () {
|
||||
return (_t("Error") + ":\n" + message + "\n\n" + traceback).trim();
|
||||
},
|
||||
// Container added because of Bootstrap modal that give the focus to another element.
|
||||
// We need to give to correct focus to ClipboardJS (see in ClipboardJS doc)
|
||||
// https://github.com/zenorocha/clipboard.js/issues/155
|
||||
container: this.el,
|
||||
});
|
||||
this.clipboard.on("success", function (e) {
|
||||
_.defer(function () {
|
||||
$clipboardBtn.tooltip("show");
|
||||
_.delay(function () {
|
||||
$clipboardBtn.tooltip("hide");
|
||||
}, 800);
|
||||
});
|
||||
});
|
||||
},
|
||||
destroy() {
|
||||
if (this.clipboard) {
|
||||
this.$(".o_clipboard_button").tooltip('dispose');
|
||||
this.clipboard.destroy();
|
||||
}
|
||||
this._super();
|
||||
}
|
||||
});
|
||||
|
||||
var ErrorDialog = CrashManagerDialog.extend({
|
||||
template: 'CrashManager.error',
|
||||
});
|
||||
|
||||
var WarningDialog = CrashManagerDialog.extend({
|
||||
template: 'CrashManager.warning',
|
||||
|
||||
/**
|
||||
* Sets size to medium by default.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
init: function (parent, options, error) {
|
||||
this._super(parent, _.extend({
|
||||
size: 'medium',
|
||||
}, options), error);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Focuses the ok button.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
open: function () {
|
||||
this._super({shouldFocusButtons: true});
|
||||
},
|
||||
});
|
||||
|
||||
var CrashManager = AbstractService.extend({
|
||||
init: function () {
|
||||
var self = this;
|
||||
active = true;
|
||||
this.isConnected = true;
|
||||
this.odooExceptionTitleMap = {
|
||||
'odoo.addons.base.models.ir_mail_server.MailDeliveryException': _lt("MailDeliveryException"),
|
||||
'odoo.exceptions.AccessDenied': _lt("Access Denied"),
|
||||
'odoo.exceptions.AccessError': _lt("Access Error"),
|
||||
'odoo.exceptions.MissingError': _lt("Missing Record"),
|
||||
'odoo.exceptions.UserError': _lt("User Error"),
|
||||
'odoo.exceptions.ValidationError': _lt("Validation Error"),
|
||||
'odoo.exceptions.Warning': _lt("Warning"),
|
||||
};
|
||||
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
// crash manager integration
|
||||
core.bus.on('rpc_error', this, this.rpc_error);
|
||||
window.onerror = function (message, file, line, col, error) {
|
||||
// Scripts injected in DOM (eg: google API's js files) won't return a clean error on window.onerror.
|
||||
// The browser will just give you a 'Script error.' as message and nothing else for security issue.
|
||||
// To enable onerror to work properly with CORS file, you should:
|
||||
// 1. add crossorigin="anonymous" to your <script> tag loading the file
|
||||
// 2. enabling 'Access-Control-Allow-Origin' on the server serving the file.
|
||||
// Since in some case it wont be possible to to this, this handle should have the possibility to be
|
||||
// handled by the script manipulating the injected file. For this, you will use window.onOriginError
|
||||
// If it is not handled, we should display something clearer than the common crash_manager error dialog
|
||||
// since it won't show anything except "Script error."
|
||||
// This link will probably explain it better: https://blog.sentry.io/2016/05/17/what-is-script-error.html
|
||||
if (!file && !line && !col) {
|
||||
// Chrome and Opera set "Script error." on the `message` and hide the `error`
|
||||
// Firefox handles the "Script error." directly. It sets the error thrown by the CORS file into `error`
|
||||
if (window.onOriginError) {
|
||||
window.onOriginError();
|
||||
delete window.onOriginError;
|
||||
} else {
|
||||
self.show_error({
|
||||
type: _t("Odoo Client Error"),
|
||||
message: _t("Unknown CORS error"),
|
||||
data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin. (Opening your browser console might give you a hint on the error.)")},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// ignore Chrome video internal error: https://crbug.com/809574
|
||||
if (!error && message === 'ResizeObserver loop limit exceeded') {
|
||||
return;
|
||||
}
|
||||
var traceback = error ? error.stack : '';
|
||||
self.show_error({
|
||||
type: _t("Odoo Client Error"),
|
||||
message: message,
|
||||
data: {debug: file + ':' + line + "\n" + _t('Traceback:') + "\n" + traceback, jsError: error},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// listen to unhandled rejected promises, and throw an error when the
|
||||
// promise has been rejected due to a crash
|
||||
core.bus.on('crash_manager_unhandledrejection', this, function (ev) {
|
||||
if (ev.reason && ev.reason instanceof Error) {
|
||||
let traceback = ev.reason.stack;
|
||||
self.show_error({
|
||||
type: _t("Odoo Client Error"),
|
||||
message: '',
|
||||
data: {debug: _t('Traceback:') + "\n" + traceback, jsError: ev.reason},
|
||||
});
|
||||
} else {
|
||||
// the rejection is not due to an Error, so prevent the browser
|
||||
// from displaying an 'unhandledrejection' error in the console
|
||||
ev.stopPropagation();
|
||||
ev.stopImmediatePropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
},
|
||||
enable: function () {
|
||||
active = true;
|
||||
},
|
||||
disable: function () {
|
||||
active = false;
|
||||
},
|
||||
handleLostConnection: function () {
|
||||
var self = this;
|
||||
if (!this.isConnected) {
|
||||
// already handled, nothing to do. This can happen when several
|
||||
// rpcs are done in parallel and fail because of a lost connection.
|
||||
return;
|
||||
}
|
||||
this.isConnected = false;
|
||||
var delay = 2000;
|
||||
core.bus.trigger('connection_lost');
|
||||
|
||||
setTimeout(function checkConnection() {
|
||||
ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () {
|
||||
core.bus.trigger('connection_restored');
|
||||
self.isConnected = true;
|
||||
}).guardedCatch(function () {
|
||||
// exponential backoff, with some jitter
|
||||
delay = (delay * 1.5) + 500*Math.random();
|
||||
setTimeout(checkConnection, delay);
|
||||
});
|
||||
}, delay);
|
||||
},
|
||||
rpc_error: function(error) {
|
||||
// Some qunit tests produces errors before the DOM is set.
|
||||
// This produces an error loop as the modal/toast has no DOM to attach to.
|
||||
if (!document.body || !active || this.connection_lost) return;
|
||||
|
||||
// Connection lost error
|
||||
if (error.code === -32098) {
|
||||
this.handleLostConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special exception handlers, see crash_registry bellow
|
||||
var handler = core.crash_registry.get(error.data.name, true);
|
||||
if (handler) {
|
||||
new (handler)(this, error).display();
|
||||
return;
|
||||
}
|
||||
|
||||
// Odoo custom exception: UserError, AccessError, ...
|
||||
if (_.has(this.odooExceptionTitleMap, error.data.name)) {
|
||||
error = _.extend({}, error, {
|
||||
data: _.extend({}, error.data, {
|
||||
message: error.data.arguments[0],
|
||||
title: this.odooExceptionTitleMap[error.data.name],
|
||||
}),
|
||||
});
|
||||
this.show_warning(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Any other Python exception
|
||||
this.show_error(error);
|
||||
},
|
||||
show_warning: function (error, options) {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
var message = error.data ? error.data.message : error.message;
|
||||
var title = _t("Something went wrong !");
|
||||
if (error.type) {
|
||||
title = _.str.capitalize(error.type);
|
||||
} else if (error.data && error.data.title) {
|
||||
title = _.str.capitalize(error.data.title);
|
||||
}
|
||||
|
||||
return this.displayNotification(Object.assign({}, options, {
|
||||
title,
|
||||
message,
|
||||
sticky: true,
|
||||
}));
|
||||
},
|
||||
show_error: function (error) {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
error.traceback = error.data.debug;
|
||||
var dialog = new ErrorDialog(this, {
|
||||
title: _.str.capitalize(error.type) || _t("Odoo Error"),
|
||||
}, error);
|
||||
|
||||
return dialog.open();
|
||||
},
|
||||
show_message: function(exception) {
|
||||
return this.show_error({
|
||||
type: _t("Odoo Client Error"),
|
||||
message: exception,
|
||||
data: {debug: ""}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
|
||||
*/
|
||||
var ExceptionHandler = {
|
||||
/**
|
||||
* @param parent The parent.
|
||||
* @param error The error object as returned by the JSON-RPC implementation.
|
||||
*/
|
||||
init: function(parent, error) {},
|
||||
/**
|
||||
* Called to inform to display the widget, if necessary. A typical way would be to implement
|
||||
* this interface in a class extending instance.web.Dialog and simply display the dialog in this
|
||||
* method.
|
||||
*/
|
||||
display: function() {},
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handle redirection warnings, which behave more or less like a regular
|
||||
* warning, with an additional redirection button.
|
||||
*/
|
||||
var RedirectWarningHandler = Widget.extend(ExceptionHandler, {
|
||||
init: function(parent, error) {
|
||||
this._super(parent);
|
||||
this.error = error;
|
||||
},
|
||||
display: function() {
|
||||
var self = this;
|
||||
var error = this.error;
|
||||
var additional_context = _.extend({}, this.context, error.data.arguments[3]);
|
||||
|
||||
new WarningDialog(this, {
|
||||
title: _.str.capitalize(error.type) || _t("Odoo Warning"),
|
||||
buttons: [
|
||||
{text: error.data.arguments[2], classes : "btn-primary", click: function() {
|
||||
self.do_action(
|
||||
error.data.arguments[1],
|
||||
{
|
||||
additional_context: additional_context,
|
||||
});
|
||||
self.destroy();
|
||||
}, close: true},
|
||||
{text: _t("Cancel"), click: function() { self.destroy(); }, close: true}
|
||||
]
|
||||
}, {
|
||||
message: error.data.arguments[0],
|
||||
}).open();
|
||||
}
|
||||
});
|
||||
|
||||
core.crash_registry.add('odoo.exceptions.RedirectWarning', RedirectWarningHandler);
|
||||
|
||||
function session_expired(cm) {
|
||||
return {
|
||||
display: function () {
|
||||
const notif = {
|
||||
type: _t("Odoo Session Expired"),
|
||||
message: _t("Your Odoo session expired. The current page is about to be refreshed."),
|
||||
};
|
||||
const options = {
|
||||
buttons: [{
|
||||
text: _t("Ok"),
|
||||
click: () => window.location.reload(true),
|
||||
close: true
|
||||
}],
|
||||
};
|
||||
cm.show_warning(notif, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
core.crash_registry.add('odoo.http.SessionExpiredException', session_expired);
|
||||
core.crash_registry.add('werkzeug.exceptions.Forbidden', session_expired);
|
||||
|
||||
core.crash_registry.add('504', function (cm) {
|
||||
return {
|
||||
display: function () {
|
||||
cm.show_warning({
|
||||
type: _t("Request timeout"),
|
||||
message: _t("The operation was interrupted. This usually means that the current operation is taking too much time.")});
|
||||
}
|
||||
};
|
||||
});
|
||||
core.serviceRegistry.add('crash_manager', CrashManager);
|
||||
|
||||
return {
|
||||
CrashManager: CrashManager,
|
||||
ErrorDialog: ErrorDialog,
|
||||
WarningDialog: WarningDialog,
|
||||
disable: () => active = false,
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
/** @odoo-module */
|
||||
import Dialog from "web.Dialog";
|
||||
import core from "web.core";
|
||||
|
||||
/**
|
||||
* An extension of Dialog Widget to render warnings.
|
||||
*/
|
||||
export const WarningDialog = Dialog.extend({
|
||||
template: 'web.LegacyWarningDialog',
|
||||
/**
|
||||
* @param {Object} error
|
||||
* @param {string} error.message the message in Warning Dialog
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, options, error) {
|
||||
options.size = options.size || "medium";
|
||||
this._super.apply(this, [parent, options]);
|
||||
this.message = error.message;
|
||||
core.bus.off('close_dialogs', this);
|
||||
},
|
||||
/**
|
||||
* Focuses the ok button.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
open: function () {
|
||||
this._super({shouldFocusButtons: true});
|
||||
},
|
||||
});
|
||||
@@ -4,7 +4,6 @@ odoo.define('web.framework', function (require) {
|
||||
var core = require('web.core');
|
||||
var ajax = require('web.ajax');
|
||||
var Widget = require('web.Widget');
|
||||
var disableCrashManager = require('web.CrashManager').disable;
|
||||
const {sprintf} = require('web.utils')
|
||||
|
||||
var _t = core._t;
|
||||
@@ -101,9 +100,6 @@ function unblockUI() {
|
||||
* If wait is true, sleep 1s and wait for the server i.e. after a restart.
|
||||
*/
|
||||
function redirect (url, wait) {
|
||||
// Dont display a dialog if some xmlhttprequest are in progress
|
||||
disableCrashManager();
|
||||
|
||||
var load = function() {
|
||||
var old = "" + window.location;
|
||||
var old_no_hash = old.split("#")[0];
|
||||
|
||||
@@ -1898,7 +1898,6 @@ var AbstractFieldBinary = AbstractField.extend({
|
||||
on_file_uploaded: function (size, name) {
|
||||
if (size === false) {
|
||||
this.displayNotification({ message: _t("There was a problem while uploading your file"), type: 'danger' });
|
||||
// TODO: use crashmanager
|
||||
console.warn("Error while uploading file : ", name);
|
||||
} else {
|
||||
this.on_file_uploaded_and_valid.apply(this, arguments);
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
odoo.define('web.PublicCrashManager', function (require) {
|
||||
"use strict";
|
||||
|
||||
const core = require('web.core');
|
||||
const CrashManager = require('web.CrashManager').CrashManager;
|
||||
|
||||
const PublicCrashManager = CrashManager.extend({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_displayWarning(message, title, options) {
|
||||
this.displayNotification(Object.assign({}, options, {
|
||||
title,
|
||||
message,
|
||||
sticky: true,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
core.serviceRegistry.add('crash_manager', PublicCrashManager);
|
||||
|
||||
return {
|
||||
CrashManager: PublicCrashManager,
|
||||
};
|
||||
|
||||
});
|
||||
@@ -1,421 +0,0 @@
|
||||
odoo.define('web.ErrorDialogRegistry', function (require) {
|
||||
"use strict";
|
||||
|
||||
var Registry = require('web.Registry');
|
||||
|
||||
return new Registry();
|
||||
});
|
||||
|
||||
odoo.define('web.CrashManager', function (require) {
|
||||
"use strict";
|
||||
|
||||
const AbstractService = require('web.AbstractService');
|
||||
var ajax = require('web.ajax');
|
||||
const BrowserDetection = require('web.BrowserDetection');
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var ErrorDialogRegistry = require('web.ErrorDialogRegistry');
|
||||
var Widget = require('web.Widget');
|
||||
var config = require('web.config');
|
||||
var { formatTraceback, annotateTraceback} = require("@web/legacy/js/core/error_utils");
|
||||
|
||||
var _t = core._t;
|
||||
var _lt = core._lt;
|
||||
|
||||
// Register this eventlistener before qunit does.
|
||||
// Some errors needs to be negated by the crash_manager.
|
||||
window.addEventListener('unhandledrejection', ev =>
|
||||
core.bus.trigger('crash_manager_unhandledrejection', ev)
|
||||
);
|
||||
|
||||
let active = true;
|
||||
|
||||
/**
|
||||
* An extension of Dialog Widget to render the warnings and errors on the website.
|
||||
* Extend it with your template of choice like ErrorDialog/WarningDialog
|
||||
*/
|
||||
var CrashManagerDialog = Dialog.extend({
|
||||
xmlDependencies: (Dialog.prototype.xmlDependencies || []).concat(
|
||||
['/web/static/src/legacy/xml/crash_manager.xml']
|
||||
),
|
||||
|
||||
/**
|
||||
* @param {Object} error
|
||||
* @param {string} error.message the message in Warning/Error Dialog
|
||||
* @param {string} error.traceback the traceback in ErrorDialog
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
init: function (parent, options, error) {
|
||||
this._super.apply(this, [parent, options]);
|
||||
this.error = error;
|
||||
this.message = error.message;
|
||||
this.traceback = error.traceback;
|
||||
core.bus.off('close_dialogs', this);
|
||||
},
|
||||
|
||||
willStart: async function () {
|
||||
await this._super()
|
||||
const jsError = this.error.data && this.error.data.jsError;
|
||||
if (jsError) {
|
||||
if (config.isDebug('assets')) {
|
||||
this.traceback = await annotateTraceback(jsError);
|
||||
} else {
|
||||
this.traceback = formatTraceback(jsError);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
start: async function () {
|
||||
await this._super();
|
||||
const message = this.message;
|
||||
const traceback = this.traceback;
|
||||
|
||||
if (!traceback) {
|
||||
return;
|
||||
}
|
||||
this.$(".o_error_detail").on("shown.bs.collapse", function (e) {
|
||||
e.target.scrollTop = e.target.scrollHeight;
|
||||
});
|
||||
|
||||
const $clipboardBtn = this.$(".o_clipboard_button");
|
||||
$clipboardBtn.tooltip({title: _t("Copied !"), trigger: "manual", placement: "left"});
|
||||
this.clipboard = new window.ClipboardJS($clipboardBtn[0], {
|
||||
text: function () {
|
||||
return (_t("Error") + ":\n" + message + "\n\n" + traceback).trim();
|
||||
},
|
||||
// Container added because of Bootstrap modal that give the focus to another element.
|
||||
// We need to give to correct focus to ClipboardJS (see in ClipboardJS doc)
|
||||
// https://github.com/zenorocha/clipboard.js/issues/155
|
||||
container: this.el,
|
||||
});
|
||||
this.clipboard.on("success", function (e) {
|
||||
_.defer(function () {
|
||||
$clipboardBtn.tooltip("show");
|
||||
_.delay(function () {
|
||||
$clipboardBtn.tooltip("hide");
|
||||
}, 800);
|
||||
});
|
||||
});
|
||||
},
|
||||
destroy() {
|
||||
if (this.clipboard) {
|
||||
this.$(".o_clipboard_button").tooltip('dispose');
|
||||
this.clipboard.destroy();
|
||||
}
|
||||
this._super();
|
||||
}
|
||||
});
|
||||
|
||||
var ErrorDialog = CrashManagerDialog.extend({
|
||||
template: 'CrashManager.error',
|
||||
});
|
||||
|
||||
var WarningDialog = CrashManagerDialog.extend({
|
||||
template: 'CrashManager.warning',
|
||||
|
||||
/**
|
||||
* Sets size to medium by default.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
init: function (parent, options, error) {
|
||||
this._super(parent, _.extend({
|
||||
size: 'medium',
|
||||
}, options), error);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Focuses the ok button.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
open: function () {
|
||||
this._super({shouldFocusButtons: true});
|
||||
},
|
||||
});
|
||||
|
||||
var CrashManager = AbstractService.extend({
|
||||
init: function () {
|
||||
var self = this;
|
||||
active = true;
|
||||
this.isConnected = true;
|
||||
this.odooExceptionTitleMap = {
|
||||
'odoo.addons.base.models.ir_mail_server.MailDeliveryException': _lt("MailDeliveryException"),
|
||||
'odoo.exceptions.AccessDenied': _lt("Access Denied"),
|
||||
'odoo.exceptions.AccessError': _lt("Access Error"),
|
||||
'odoo.exceptions.MissingError': _lt("Missing Record"),
|
||||
'odoo.exceptions.UserError': _lt("User Error"),
|
||||
'odoo.exceptions.ValidationError': _lt("Validation Error"),
|
||||
'odoo.exceptions.Warning': _lt("Warning"),
|
||||
};
|
||||
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
// crash manager integration
|
||||
core.bus.on('rpc_error', this, this.rpc_error);
|
||||
window.onerror = function (message, file, line, col, error) {
|
||||
// Scripts injected in DOM (eg: google API's js files) won't return a clean error on window.onerror.
|
||||
// The browser will just give you a 'Script error.' as message and nothing else for security issue.
|
||||
// To enable onerror to work properly with CORS file, you should:
|
||||
// 1. add crossorigin="anonymous" to your <script> tag loading the file
|
||||
// 2. enabling 'Access-Control-Allow-Origin' on the server serving the file.
|
||||
// Since in some case it wont be possible to to this, this handle should have the possibility to be
|
||||
// handled by the script manipulating the injected file. For this, you will use window.onOriginError
|
||||
// If it is not handled, we should display something clearer than the common crash_manager error dialog
|
||||
// since it won't show anything except "Script error."
|
||||
// This link will probably explain it better: https://blog.sentry.io/2016/05/17/what-is-script-error.html
|
||||
if (!file && !line && !col) {
|
||||
// Chrome and Opera set "Script error." on the `message` and hide the `error`
|
||||
// Firefox handles the "Script error." directly. It sets the error thrown by the CORS file into `error`
|
||||
if (window.onOriginError) {
|
||||
window.onOriginError();
|
||||
delete window.onOriginError;
|
||||
} else {
|
||||
self.show_error({
|
||||
type: _t("Odoo Client Error"),
|
||||
message: _t("Unknown CORS error"),
|
||||
data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin. (Opening your browser console might give you a hint on the error.)")},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// ignore Chrome video internal error: https://crbug.com/809574
|
||||
if (!error && message === 'ResizeObserver loop limit exceeded') {
|
||||
return;
|
||||
}
|
||||
var traceback = error ? error.stack : '';
|
||||
self.show_error({
|
||||
type: _t("Odoo Client Error"),
|
||||
message: message,
|
||||
data: {debug: file + ':' + line + "\n" + _t('Traceback:') + "\n" + traceback, jsError: error},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// listen to unhandled rejected promises, and throw an error when the
|
||||
// promise has been rejected due to a crash
|
||||
core.bus.on('crash_manager_unhandledrejection', this, function (ev) {
|
||||
if (ev.reason && ev.reason instanceof Error) {
|
||||
let traceback = ev.reason.stack;
|
||||
self.show_error({
|
||||
type: _t("Odoo Client Error"),
|
||||
message: '',
|
||||
data: {debug: _t('Traceback:') + "\n" + traceback, jsError: ev.reason},
|
||||
});
|
||||
} else {
|
||||
// the rejection is not due to an Error, so prevent the browser
|
||||
// from displaying an 'unhandledrejection' error in the console
|
||||
ev.stopPropagation();
|
||||
ev.stopImmediatePropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
},
|
||||
enable: function () {
|
||||
active = true;
|
||||
},
|
||||
disable: function () {
|
||||
active = false;
|
||||
},
|
||||
handleLostConnection: function () {
|
||||
var self = this;
|
||||
if (!this.isConnected) {
|
||||
// already handled, nothing to do. This can happen when several
|
||||
// rpcs are done in parallel and fail because of a lost connection.
|
||||
return;
|
||||
}
|
||||
this.isConnected = false;
|
||||
var delay = 2000;
|
||||
core.bus.trigger('connection_lost');
|
||||
|
||||
setTimeout(function checkConnection() {
|
||||
ajax.jsonRpc('/web/webclient/version_info', 'call', {}, {shadow:true}).then(function () {
|
||||
core.bus.trigger('connection_restored');
|
||||
self.isConnected = true;
|
||||
}).guardedCatch(function () {
|
||||
// exponential backoff, with some jitter
|
||||
delay = (delay * 1.5) + 500*Math.random();
|
||||
setTimeout(checkConnection, delay);
|
||||
});
|
||||
}, delay);
|
||||
},
|
||||
rpc_error: function(error) {
|
||||
// Some qunit tests produces errors before the DOM is set.
|
||||
// This produces an error loop as the modal/toast has no DOM to attach to.
|
||||
if (!document.body || !active || this.connection_lost) return;
|
||||
|
||||
// Connection lost error
|
||||
if (error.code === -32098) {
|
||||
this.handleLostConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special exception handlers, see crash_registry bellow
|
||||
var handler = core.crash_registry.get(error.data.name, true);
|
||||
if (handler) {
|
||||
new (handler)(this, error).display();
|
||||
return;
|
||||
}
|
||||
|
||||
// Odoo custom exception: UserError, AccessError, ...
|
||||
if (_.has(this.odooExceptionTitleMap, error.data.name)) {
|
||||
error = _.extend({}, error, {
|
||||
data: _.extend({}, error.data, {
|
||||
message: error.data.arguments[0],
|
||||
title: this.odooExceptionTitleMap[error.data.name],
|
||||
}),
|
||||
});
|
||||
this.show_warning(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Any other Python exception
|
||||
this.show_error(error);
|
||||
},
|
||||
show_warning: function (error, options) {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
var message = error.data ? error.data.message : error.message;
|
||||
var title = _t("Something went wrong !");
|
||||
if (error.type) {
|
||||
title = _.str.capitalize(error.type);
|
||||
} else if (error.data && error.data.title) {
|
||||
title = _.str.capitalize(error.data.title);
|
||||
}
|
||||
return this._displayWarning(message, title, options);
|
||||
},
|
||||
show_error: function (error) {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
error.traceback = error.data.debug;
|
||||
var dialogClass = error.data.context && ErrorDialogRegistry.get(error.data.context.exception_class) || ErrorDialog;
|
||||
var dialog = new dialogClass(this, {
|
||||
title: _.str.capitalize(error.type) || _t("Odoo Error"),
|
||||
}, error);
|
||||
|
||||
return dialog.open();
|
||||
},
|
||||
show_message: function(exception) {
|
||||
return this.show_error({
|
||||
type: _t("Odoo Client Error"),
|
||||
message: exception,
|
||||
data: {debug: ""}
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} message
|
||||
* @param {string} title
|
||||
* @param {Object} options
|
||||
*/
|
||||
_displayWarning: function (message, title, options) {
|
||||
return new WarningDialog(this, Object.assign({}, options, {
|
||||
title,
|
||||
}), {
|
||||
message,
|
||||
}).open();
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
|
||||
*/
|
||||
var ExceptionHandler = {
|
||||
/**
|
||||
* @param parent The parent.
|
||||
* @param error The error object as returned by the JSON-RPC implementation.
|
||||
*/
|
||||
init: function(parent, error) {},
|
||||
/**
|
||||
* Called to inform to display the widget, if necessary. A typical way would be to implement
|
||||
* this interface in a class extending instance.web.Dialog and simply display the dialog in this
|
||||
* method.
|
||||
*/
|
||||
display: function() {},
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handle redirection warnings, which behave more or less like a regular
|
||||
* warning, with an additional redirection button.
|
||||
*/
|
||||
var RedirectWarningHandler = Widget.extend(ExceptionHandler, {
|
||||
init: function(parent, error) {
|
||||
this._super(parent);
|
||||
this.error = error;
|
||||
},
|
||||
display: function() {
|
||||
var self = this;
|
||||
var error = this.error;
|
||||
var additional_context = _.extend({}, this.context, error.data.arguments[3]);
|
||||
|
||||
new WarningDialog(this, {
|
||||
title: _.str.capitalize(error.type) || _t("Odoo Warning"),
|
||||
buttons: [
|
||||
{text: error.data.arguments[2], classes : "btn-primary", click: function() {
|
||||
self.do_action(
|
||||
error.data.arguments[1],
|
||||
{
|
||||
additional_context: additional_context,
|
||||
});
|
||||
self.destroy();
|
||||
}, close: true},
|
||||
{text: _t("Cancel"), click: function() { self.destroy(); }, close: true}
|
||||
]
|
||||
}, {
|
||||
message: error.data.arguments[0],
|
||||
}).open();
|
||||
}
|
||||
});
|
||||
|
||||
core.crash_registry.add('odoo.exceptions.RedirectWarning', RedirectWarningHandler);
|
||||
|
||||
function session_expired(cm) {
|
||||
return {
|
||||
display: function () {
|
||||
const notif = {
|
||||
type: _t("Odoo Session Expired"),
|
||||
message: _t("Your Odoo session expired. The current page is about to be refreshed."),
|
||||
};
|
||||
const options = {
|
||||
buttons: [{
|
||||
text: _t("Ok"),
|
||||
click: () => window.location.reload(true),
|
||||
close: true
|
||||
}],
|
||||
};
|
||||
cm.show_warning(notif, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
core.crash_registry.add('odoo.http.SessionExpiredException', session_expired);
|
||||
core.crash_registry.add('werkzeug.exceptions.Forbidden', session_expired);
|
||||
|
||||
core.crash_registry.add('504', function (cm) {
|
||||
return {
|
||||
display: function () {
|
||||
cm.show_warning({
|
||||
type: _t("Request timeout"),
|
||||
message: _t("The operation was interrupted. This usually means that the current operation is taking too much time.")});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
CrashManager: CrashManager,
|
||||
ErrorDialog: ErrorDialog,
|
||||
WarningDialog: WarningDialog,
|
||||
disable: () => active = false,
|
||||
};
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
makeLegacyRpcService,
|
||||
makeLegacySessionService,
|
||||
makeLegacyDialogMappingService,
|
||||
makeLegacyCrashManagerService,
|
||||
} from "./utils";
|
||||
import * as AbstractService from "web.AbstractService";
|
||||
import * as legacyEnv from "web.env";
|
||||
@@ -38,8 +39,9 @@ export const legacySetupProm = new Promise((resolve) => {
|
||||
serviceRegistry.add("legacy_session", legacySessionService);
|
||||
const legacyWebClientService = makeLegacyWebClientService(legacyEnv);
|
||||
serviceRegistry.add("legacy_web_client", legacyWebClientService);
|
||||
const legacyDialogMappingService = makeLegacyDialogMappingService(legacyEnv);
|
||||
serviceRegistry.add("legacy_notification", makeLegacyNotificationService(legacyEnv));
|
||||
serviceRegistry.add("legacy_crash_manager", makeLegacyCrashManagerService(legacyEnv));
|
||||
const legacyDialogMappingService = makeLegacyDialogMappingService(legacyEnv);
|
||||
serviceRegistry.add("legacy_dialog_mapping", legacyDialogMappingService);
|
||||
await Promise.all([whenReady(), session.is_bound]);
|
||||
legacyEnv.qweb.addTemplates(session.owlTemplates);
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
import { browser } from "../core/browser/browser";
|
||||
import AbstractStorageService from "web.AbstractStorageService";
|
||||
import { ConnectionAbortedError, RPCError } from "../core/network/rpc_service";
|
||||
import {
|
||||
ConnectionAbortedError,
|
||||
RPCError,
|
||||
makeErrorFromResponse,
|
||||
} from "../core/network/rpc_service";
|
||||
import { ErrorDialog } from "../core/errors/error_dialogs";
|
||||
|
||||
export function mapDoActionOptionAPI(legacyOptions) {
|
||||
legacyOptions = legacyOptions || {};
|
||||
@@ -257,3 +262,20 @@ export function makeLegacyNotificationService(legacyEnv) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function makeLegacyCrashManagerService(legacyEnv) {
|
||||
return {
|
||||
dependencies: ["dialog"],
|
||||
start(env) {
|
||||
legacyEnv.services.crash_manager = {
|
||||
show_message(message) {
|
||||
env.services.dialog.open(ErrorDialog, { traceback: message });
|
||||
},
|
||||
rpc_error(errorResponse) {
|
||||
// Will be handled by error_service
|
||||
Promise.reject(makeErrorFromResponse(errorResponse));
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,5 +23,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<div t-name="web.LegacyWarningDialog" class="o_dialog_warning" role="alert">
|
||||
<p t-esc="widget.message" style="white-space: pre-wrap;"/>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
|
||||
@@ -26,8 +26,6 @@ odoo.define("web.test_legacy", async (require) => {
|
||||
const session = require("web.session");
|
||||
await session.is_bound; // await for templates from server
|
||||
require("web.test_utils");
|
||||
const LegacyCrashManager = require("web.CrashManager");
|
||||
LegacyCrashManager.disable();
|
||||
resolve();
|
||||
});
|
||||
|
||||
|
||||
@@ -123,7 +123,6 @@ odoo.define('web.test_utils', async function (require) {
|
||||
// to load xml files that are normally lazy loaded by specific widgets).
|
||||
await Promise.all([
|
||||
session.is_bound,
|
||||
ajax.loadXML('/web/static/src/legacy/xml/crash_manager.xml', core.qweb),
|
||||
ajax.loadXML('/web/static/src/legacy/frontend/debug_manager/debug.xml', core.qweb),
|
||||
ajax.loadXML('/web/static/src/legacy/xml/dialog.xml', core.qweb),
|
||||
ajax.loadXML('/web/static/src/legacy/xml/translation_dialog.xml', core.qweb),
|
||||
|
||||
Reference in New Issue
Block a user