summaryrefslogtreecommitdiff
path: root/addons/web/static
diff options
context:
space:
mode:
authorqsm-odoo <qsm@odoo.com>2016-11-02 12:36:16 +0100
committerqsm-odoo <qsm@odoo.com>2016-11-02 13:26:00 +0100
commitdd560377e28ac9a8e39f56241bfff490ca7bce3f (patch)
tree745e7459b006cdc66fa971b3ba5f4e6bf5116d3e /addons/web/static
parentc8d0fd4a50893535c7be0fb416e1f05a917e98d0 (diff)
[IMP] web: improve traceback reporting
* Do not assault the user with a full traceback on bug, but indicates an error occured and allow him to toggle the traceback * Automatically scroll the traceback to the end (better for python errors) * Propose the user to report the traceback by using a new "Copy" button which correctly copies the full error + traceback
Diffstat (limited to 'addons/web/static')
-rw-r--r--addons/web/static/lib/clipboard/clipboard.js753
-rw-r--r--addons/web/static/src/js/abstract_web_client.js2
-rw-r--r--addons/web/static/src/js/framework/crash_manager.js52
-rw-r--r--addons/web/static/src/less/modal.less19
-rw-r--r--addons/web/static/src/xml/base.xml31
5 files changed, 833 insertions, 24 deletions
diff --git a/addons/web/static/lib/clipboard/clipboard.js b/addons/web/static/lib/clipboard/clipboard.js
new file mode 100644
index 00000000000..f9b772b5036
--- /dev/null
+++ b/addons/web/static/lib/clipboard/clipboard.js
@@ -0,0 +1,753 @@
+/*!
+ * clipboard.js v1.5.15
+ * https://zenorocha.github.io/clipboard.js
+ *
+ * Licensed MIT © Zeno Rocha
+ */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/**
+ * A polyfill for Element.matches()
+ */
+if (Element && !Element.prototype.matches) {
+ var proto = Element.prototype;
+
+ proto.matches = proto.matchesSelector ||
+ proto.mozMatchesSelector ||
+ proto.msMatchesSelector ||
+ proto.oMatchesSelector ||
+ proto.webkitMatchesSelector;
+}
+
+/**
+ * Finds the closest parent that matches a selector.
+ *
+ * @param {Element} element
+ * @param {String} selector
+ * @return {Function}
+ */
+function closest (element, selector) {
+ while (element && element !== document) {
+ if (element.matches(selector)) return element;
+ element = element.parentNode;
+ }
+}
+
+module.exports = closest;
+
+},{}],2:[function(require,module,exports){
+var closest = require('./closest');
+
+/**
+ * Delegates event to a selector.
+ *
+ * @param {Element} element
+ * @param {String} selector
+ * @param {String} type
+ * @param {Function} callback
+ * @param {Boolean} useCapture
+ * @return {Object}
+ */
+function delegate(element, selector, type, callback, useCapture) {
+ var listenerFn = listener.apply(this, arguments);
+
+ element.addEventListener(type, listenerFn, useCapture);
+
+ return {
+ destroy: function() {
+ element.removeEventListener(type, listenerFn, useCapture);
+ }
+ }
+}
+
+/**
+ * Finds closest match and invokes callback.
+ *
+ * @param {Element} element
+ * @param {String} selector
+ * @param {String} type
+ * @param {Function} callback
+ * @return {Function}
+ */
+function listener(element, selector, type, callback) {
+ return function(e) {
+ e.delegateTarget = closest(e.target, selector);
+
+ if (e.delegateTarget) {
+ callback.call(element, e);
+ }
+ }
+}
+
+module.exports = delegate;
+
+},{"./closest":1}],3:[function(require,module,exports){
+/**
+ * Check if argument is a HTML element.
+ *
+ * @param {Object} value
+ * @return {Boolean}
+ */
+exports.node = function(value) {
+ return value !== undefined
+ && value instanceof HTMLElement
+ && value.nodeType === 1;
+};
+
+/**
+ * Check if argument is a list of HTML elements.
+ *
+ * @param {Object} value
+ * @return {Boolean}
+ */
+exports.nodeList = function(value) {
+ var type = Object.prototype.toString.call(value);
+
+ return value !== undefined
+ && (type === '[object NodeList]' || type === '[object HTMLCollection]')
+ && ('length' in value)
+ && (value.length === 0 || exports.node(value[0]));
+};
+
+/**
+ * Check if argument is a string.
+ *
+ * @param {Object} value
+ * @return {Boolean}
+ */
+exports.string = function(value) {
+ return typeof value === 'string'
+ || value instanceof String;
+};
+
+/**
+ * Check if argument is a function.
+ *
+ * @param {Object} value
+ * @return {Boolean}
+ */
+exports.fn = function(value) {
+ var type = Object.prototype.toString.call(value);
+
+ return type === '[object Function]';
+};
+
+},{}],4:[function(require,module,exports){
+var is = require('./is');
+var delegate = require('delegate');
+
+/**
+ * Validates all params and calls the right
+ * listener function based on its target type.
+ *
+ * @param {String|HTMLElement|HTMLCollection|NodeList} target
+ * @param {String} type
+ * @param {Function} callback
+ * @return {Object}
+ */
+function listen(target, type, callback) {
+ if (!target && !type && !callback) {
+ throw new Error('Missing required arguments');
+ }
+
+ if (!is.string(type)) {
+ throw new TypeError('Second argument must be a String');
+ }
+
+ if (!is.fn(callback)) {
+ throw new TypeError('Third argument must be a Function');
+ }
+
+ if (is.node(target)) {
+ return listenNode(target, type, callback);
+ }
+ else if (is.nodeList(target)) {
+ return listenNodeList(target, type, callback);
+ }
+ else if (is.string(target)) {
+ return listenSelector(target, type, callback);
+ }
+ else {
+ throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');
+ }
+}
+
+/**
+ * Adds an event listener to a HTML element
+ * and returns a remove listener function.
+ *
+ * @param {HTMLElement} node
+ * @param {String} type
+ * @param {Function} callback
+ * @return {Object}
+ */
+function listenNode(node, type, callback) {
+ node.addEventListener(type, callback);
+
+ return {
+ destroy: function() {
+ node.removeEventListener(type, callback);
+ }
+ }
+}
+
+/**
+ * Add an event listener to a list of HTML elements
+ * and returns a remove listener function.
+ *
+ * @param {NodeList|HTMLCollection} nodeList
+ * @param {String} type
+ * @param {Function} callback
+ * @return {Object}
+ */
+function listenNodeList(nodeList, type, callback) {
+ Array.prototype.forEach.call(nodeList, function(node) {
+ node.addEventListener(type, callback);
+ });
+
+ return {
+ destroy: function() {
+ Array.prototype.forEach.call(nodeList, function(node) {
+ node.removeEventListener(type, callback);
+ });
+ }
+ }
+}
+
+/**
+ * Add an event listener to a selector
+ * and returns a remove listener function.
+ *
+ * @param {String} selector
+ * @param {String} type
+ * @param {Function} callback
+ * @return {Object}
+ */
+function listenSelector(selector, type, callback) {
+ return delegate(document.body, selector, type, callback);
+}
+
+module.exports = listen;
+
+},{"./is":3,"delegate":2}],5:[function(require,module,exports){
+function select(element) {
+ var selectedText;
+
+ if (element.nodeName === 'SELECT') {
+ element.focus();
+
+ selectedText = element.value;
+ }
+ else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
+ element.focus();
+ element.setSelectionRange(0, element.value.length);
+
+ selectedText = element.value;
+ }
+ else {
+ if (element.hasAttribute('contenteditable')) {
+ element.focus();
+ }
+
+ var selection = window.getSelection();
+ var range = document.createRange();
+
+ range.selectNodeContents(element);
+ selection.removeAllRanges();
+ selection.addRange(range);
+
+ selectedText = selection.toString();
+ }
+
+ return selectedText;
+}
+
+module.exports = select;
+
+},{}],6:[function(require,module,exports){
+function E () {
+ // Keep this empty so it's easier to inherit from
+ // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
+}
+
+E.prototype = {
+ on: function (name, callback, ctx) {
+ var e = this.e || (this.e = {});
+
+ (e[name] || (e[name] = [])).push({
+ fn: callback,
+ ctx: ctx
+ });
+
+ return this;
+ },
+
+ once: function (name, callback, ctx) {
+ var self = this;
+ function listener () {
+ self.off(name, listener);
+ callback.apply(ctx, arguments);
+ };
+
+ listener._ = callback
+ return this.on(name, listener, ctx);
+ },
+
+ emit: function (name) {
+ var data = [].slice.call(arguments, 1);
+ var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
+ var i = 0;
+ var len = evtArr.length;
+
+ for (i; i < len; i++) {
+ evtArr[i].fn.apply(evtArr[i].ctx, data);
+ }
+
+ return this;
+ },
+
+ off: function (name, callback) {
+ var e = this.e || (this.e = {});
+ var evts = e[name];
+ var liveEvents = [];
+
+ if (evts && callback) {
+ for (var i = 0, len = evts.length; i < len; i++) {
+ if (evts[i].fn !== callback && evts[i].fn._ !== callback)
+ liveEvents.push(evts[i]);
+ }
+ }
+
+ // Remove event from queue to prevent memory leak
+ // Suggested by https://github.com/lazd
+ // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
+
+ (liveEvents.length)
+ ? e[name] = liveEvents
+ : delete e[name];
+
+ return this;
+ }
+};
+
+module.exports = E;
+
+},{}],7:[function(require,module,exports){
+(function (global, factory) {
+ if (typeof define === "function" && define.amd) {
+ define(['module', 'select'], factory);
+ } else if (typeof exports !== "undefined") {
+ factory(module, require('select'));
+ } else {
+ var mod = {
+ exports: {}
+ };
+ factory(mod, global.select);
+ global.clipboardAction = mod.exports;
+ }
+})(this, function (module, _select) {
+ 'use strict';
+
+ var _select2 = _interopRequireDefault(_select);
+
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+
+ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
+ return typeof obj;
+ } : function (obj) {
+ return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
+ };
+
+ function _classCallCheck(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ }
+
+ var _createClass = function () {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ return function (Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+ }();
+
+ var ClipboardAction = function () {
+ /**
+ * @param {Object} options
+ */
+ function ClipboardAction(options) {
+ _classCallCheck(this, ClipboardAction);
+
+ this.resolveOptions(options);
+ this.initSelection();
+ }
+
+ /**
+ * Defines base properties passed from constructor.
+ * @param {Object} options
+ */
+
+
+ _createClass(ClipboardAction, [{
+ key: 'resolveOptions',
+ value: function resolveOptions() {
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ this.action = options.action;
+ this.emitter = options.emitter;
+ this.target = options.target;
+ this.text = options.text;
+ this.trigger = options.trigger;
+
+ this.selectedText = '';
+ }
+ }, {
+ key: 'initSelection',
+ value: function initSelection() {
+ if (this.text) {
+ this.selectFake();
+ } else if (this.target) {
+ this.selectTarget();
+ }
+ }
+ }, {
+ key: 'selectFake',
+ value: function selectFake() {
+ var _this = this;
+
+ var isRTL = document.documentElement.getAttribute('dir') == 'rtl';
+
+ this.removeFake();
+
+ this.fakeHandlerCallback = function () {
+ return _this.removeFake();
+ };
+ this.fakeHandler = document.body.addEventListener('click', this.fakeHandlerCallback) || true;
+
+ this.fakeElem = document.createElement('textarea');
+ // Prevent zooming on iOS
+ this.fakeElem.style.fontSize = '12pt';
+ // Reset box model
+ this.fakeElem.style.border = '0';
+ this.fakeElem.style.padding = '0';
+ this.fakeElem.style.margin = '0';
+ // Move element out of screen horizontally
+ this.fakeElem.style.position = 'absolute';
+ this.fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px';
+ // Move element to the same position vertically
+ var yPosition = window.pageYOffset || document.documentElement.scrollTop;
+ this.fakeElem.addEventListener('focus', window.scrollTo(0, yPosition));
+ this.fakeElem.style.top = yPosition + 'px';
+
+ this.fakeElem.setAttribute('readonly', '');
+ this.fakeElem.value = this.text;
+
+ document.body.appendChild(this.fakeElem);
+
+ this.selectedText = (0, _select2.default)(this.fakeElem);
+ this.copyText();
+ }
+ }, {
+ key: 'removeFake',
+ value: function removeFake() {
+ if (this.fakeHandler) {
+ document.body.removeEventListener('click', this.fakeHandlerCallback);
+ this.fakeHandler = null;
+ this.fakeHandlerCallback = null;
+ }
+
+ if (this.fakeElem) {
+ document.body.removeChild(this.fakeElem);
+ this.fakeElem = null;
+ }
+ }
+ }, {
+ key: 'selectTarget',
+ value: function selectTarget() {
+ this.selectedText = (0, _select2.default)(this.target);
+ this.copyText();
+ }
+ }, {
+ key: 'copyText',
+ value: function copyText() {
+ var succeeded = void 0;
+
+ try {
+ succeeded = document.execCommand(this.action);
+ } catch (err) {
+ succeeded = false;
+ }
+
+ this.handleResult(succeeded);
+ }
+ }, {
+ key: 'handleResult',
+ value: function handleResult(succeeded) {
+ this.emitter.emit(succeeded ? 'success' : 'error', {
+ action: this.action,
+ text: this.selectedText,
+ trigger: this.trigger,
+ clearSelection: this.clearSelection.bind(this)
+ });
+ }
+ }, {
+ key: 'clearSelection',
+ value: function clearSelection() {
+ if (this.target) {
+ this.target.blur();
+ }
+
+ window.getSelection().removeAllRanges();
+ }
+ }, {
+ key: 'destroy',
+ value: function destroy() {
+ this.removeFake();
+ }
+ }, {
+ key: 'action',
+ set: function set() {
+ var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'copy';
+
+ this._action = action;
+
+ if (this._action !== 'copy' && this._action !== 'cut') {
+ throw new Error('Invalid "action" value, use either "copy" or "cut"');
+ }
+ },
+ get: function get() {
+ return this._action;
+ }
+ }, {
+ key: 'target',
+ set: function set(target) {
+ if (target !== undefined) {
+ if (target && (typeof target === 'undefined' ? 'undefined' : _typeof(target)) === 'object' && target.nodeType === 1) {
+ if (this.action === 'copy' && target.hasAttribute('disabled')) {
+ throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');
+ }
+
+ if (this.action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {
+ throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');
+ }
+
+ this._target = target;
+ } else {
+ throw new Error('Invalid "target" value, use a valid Element');
+ }
+ }
+ },
+ get: function get() {
+ return this._target;
+ }
+ }]);
+
+ return ClipboardAction;
+ }();
+
+ module.exports = ClipboardAction;
+});
+
+},{"select":5}],8:[function(require,module,exports){
+(function (global, factory) {
+ if (typeof define === "function" && define.amd) {
+ define(['module', './clipboard-action', 'tiny-emitter', 'good-listener'], factory);
+ } else if (typeof exports !== "undefined") {
+ factory(module, require('./clipboard-action'), require('tiny-emitter'), require('good-listener'));
+ } else {
+ var mod = {
+ exports: {}
+ };
+ factory(mod, global.clipboardAction, global.tinyEmitter, global.goodListener);
+ global.clipboard = mod.exports;
+ }
+})(this, function (module, _clipboardAction, _tinyEmitter, _goodListener) {
+ 'use strict';
+
+ var _clipboardAction2 = _interopRequireDefault(_clipboardAction);
+
+ var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter);
+
+ var _goodListener2 = _interopRequireDefault(_goodListener);
+
+ function _interopRequireDefault(obj) {
+ return obj && obj.__esModule ? obj : {
+ default: obj
+ };
+ }
+
+ function _classCallCheck(instance, Constructor) {
+ if (!(instance instanceof Constructor)) {
+ throw new TypeError("Cannot call a class as a function");
+ }
+ }
+
+ var _createClass = function () {
+ function defineProperties(target, props) {
+ for (var i = 0; i < props.length; i++) {
+ var descriptor = props[i];
+ descriptor.enumerable = descriptor.enumerable || false;
+ descriptor.configurable = true;
+ if ("value" in descriptor) descriptor.writable = true;
+ Object.defineProperty(target, descriptor.key, descriptor);
+ }
+ }
+
+ return function (Constructor, protoProps, staticProps) {
+ if (protoProps) defineProperties(Constructor.prototype, protoProps);
+ if (staticProps) defineProperties(Constructor, staticProps);
+ return Constructor;
+ };
+ }();
+
+ function _possibleConstructorReturn(self, call) {
+ if (!self) {
+ throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
+ }
+
+ return call && (typeof call === "object" || typeof call === "function") ? call : self;
+ }
+
+ function _inherits(subClass, superClass) {
+ if (typeof superClass !== "function" && superClass !== null) {
+ throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
+ }
+
+ subClass.prototype = Object.create(superClass && superClass.prototype, {
+ constructor: {
+ value: subClass,
+ enumerable: false,
+ writable: true,
+ configurable: true
+ }
+ });
+ if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
+ }
+
+ var Clipboard = function (_Emitter) {
+ _inherits(Clipboard, _Emitter);
+
+ /**
+ * @param {String|HTMLElement|HTMLCollection|NodeList} trigger
+ * @param {Object} options
+ */
+ function Clipboard(trigger, options) {
+ _classCallCheck(this, Clipboard);
+
+ var _this = _possibleConstructorReturn(this, (Clipboard.__proto__ || Object.getPrototypeOf(Clipboard)).call(this));
+
+ _this.resolveOptions(options);
+ _this.listenClick(trigger);
+ return _this;
+ }
+
+ /**
+ * Defines if attributes would be resolved using internal setter functions
+ * or custom functions that were passed in the constructor.
+ * @param {Object} options
+ */
+
+
+ _createClass(Clipboard, [{
+ key: 'resolveOptions',
+ value: function resolveOptions() {
+ var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
+
+ this.action = typeof options.action === 'function' ? options.action : this.defaultAction;
+ this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;
+ this.text = typeof options.text === 'function' ? options.text : this.defaultText;
+ }
+ }, {
+ key: 'listenClick',
+ value: function listenClick(trigger) {
+ var _this2 = this;
+
+ this.listener = (0, _goodListener2.default)(trigger, 'click', function (e) {
+ return _this2.onClick(e);
+ });
+ }
+ }, {
+ key: 'onClick',
+ value: function onClick(e) {
+ var trigger = e.delegateTarget || e.currentTarget;
+
+ if (this.clipboardAction) {
+ this.clipboardAction = null;
+ }
+
+ this.clipboardAction = new _clipboardAction2.default({
+ action: this.action(trigger),
+ target: this.target(trigger),
+ text: this.text(trigger),
+ trigger: trigger,
+ emitter: this
+ });
+ }
+ }, {
+ key: 'defaultAction',
+ value: function defaultAction(trigger) {
+ return getAttributeValue('action', trigger);
+ }
+ }, {
+ key: 'defaultTarget',
+ value: function defaultTarget(trigger) {
+ var selector = getAttributeValue('target', trigger);
+
+ if (selector) {
+ return document.querySelector(selector);
+ }
+ }
+ }, {
+ key: 'defaultText',
+ value: function defaultText(trigger) {
+ return getAttributeValue('text', trigger);
+ }
+ }, {
+ key: 'destroy',
+ value: function destroy() {
+ this.listener.destroy();
+
+ if (this.clipboardAction) {
+ this.clipboardAction.destroy();
+ this.clipboardAction = null;
+ }
+ }
+ }]);
+
+ return Clipboard;
+ }(_tinyEmitter2.default);
+
+ /**
+ * Helper function to retrieve attribute value.
+ * @param {String} suffix
+ * @param {Element} element
+ */
+ function getAttributeValue(suffix, element) {
+ var attribute = 'data-clipboard-' + suffix;
+
+ if (!element.hasAttribute(attribute)) {
+ return;
+ }
+
+ return element.getAttribute(attribute);
+ }
+
+ module.exports = Clipboard;
+});
+
+},{"./clipboard-action":7,"good-listener":4,"tiny-emitter":6}]},{},[8])(8)
+}); \ No newline at end of file
diff --git a/addons/web/static/src/js/abstract_web_client.js b/addons/web/static/src/js/abstract_web_client.js
index 115a58a654f..1d9c270b6c3 100644
--- a/addons/web/static/src/js/abstract_web_client.js
+++ b/addons/web/static/src/js/abstract_web_client.js
@@ -122,7 +122,7 @@ var WebClient = Widget.extend({
window.onerror = function (message, file, line, col, error) {
var traceback = error ? error.stack : '';
crash_manager.show_error({
- type: _t("Client Error"),
+ type: _t("Odoo Client Error"),
message: message,
data: {debug: file + ':' + line + "\n" + _t('Traceback:') + "\n" + traceback}
});
diff --git a/addons/web/static/src/js/framework/crash_manager.js b/addons/web/static/src/js/framework/crash_manager.js
index df21e87047c..af982ecd15d 100644
--- a/addons/web/static/src/js/framework/crash_manager.js
+++ b/addons/web/static/src/js/framework/crash_manager.js
@@ -37,7 +37,7 @@ var CrashManager = core.Class.extend({
if (this.connection_lost) {
return;
}
- if (error.code == -32098) {
+ if (error.code === -32098) {
core.bus.trigger('connection_lost');
this.connection_lost = true;
var timeinterval = setInterval(function() {
@@ -55,11 +55,11 @@ var CrashManager = core.Class.extend({
return;
}
if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
- this.show_warning({type: "Session Expired", data: { message: _t("Your Odoo session expired. Please refresh the current web page.") }});
+ this.show_warning({type: _t("Odoo Session Expired"), data: {message: _t("Your Odoo session expired. Please refresh the current web page.")}});
return;
}
if (_.has(map_title, error.data.exception_type)) {
- if(error.data.exception_type == 'except_orm'){
+ if(error.data.exception_type === 'except_orm'){
if(error.data.arguments[1]) {
error = _.extend({}, error,
{
@@ -93,7 +93,7 @@ var CrashManager = core.Class.extend({
}
this.show_warning(error);
- //InternalError
+ //InternalError
} else {
this.show_error(error);
@@ -105,23 +105,53 @@ var CrashManager = core.Class.extend({
}
new Dialog(this, {
size: 'medium',
- title: "Odoo " + (_.str.capitalize(error.type) || _t("Warning")),
+ title: _.str.capitalize(error.type || error.message) || _t("Odoo Warning"),
subtitle: error.data.title,
- $content: $('<div>').html(QWeb.render('CrashManager.warning', {error: error}))
+ $content: $(QWeb.render('CrashManager.warning', {error: error}))
}).open();
},
show_error: function(error) {
if (!this.active) {
return;
}
- new Dialog(this, {
- title: "Odoo " + _.str.capitalize(error.type),
- $content: QWeb.render('CrashManager.error', {error: error})
+ var dialog = new Dialog(this, {
+ title: _.str.capitalize(error.type || error.message) || _t("Odoo Error"),
+ $content: $(QWeb.render('CrashManager.error', {error: error}))
}).open();
+
+ // When the dialog opens, initialize the copy feature and destroy it when the dialog is closed
+ var $clipboardBtn;
+ var clipboard;
+ dialog.opened(function () {
+ $clipboardBtn = dialog.$(".o_clipboard_button");
+ $clipboardBtn.tooltip({title: _t("Copied !"), trigger: "manual", placement: "left"});
+ clipboard = new window.Clipboard($clipboardBtn[0], {
+ text: function () {
+ return (_t("Error") + ":\n" + error.message + "\n\n" + error.data.debug).trim();
+ }
+ });
+ clipboard.on("success", function (e) {
+ _.defer(function () {
+ $clipboardBtn.tooltip("show");
+ _.delay(function () {
+ $clipboardBtn.tooltip("hide");
+ }, 800);
+ });
+ });
+ });
+ dialog.on("closed", this, function () {
+ $clipboardBtn.tooltip("destroy");
+ clipboard.destroy();
+ });
+
+ // When the full traceback is shown, scroll it to the end (useful for better python error reporting)
+ dialog.$(".o_error_detail").on("shown.bs.collapse", function (e) {
+ e.target.scrollTop = e.target.scrollHeight;
+ });
},
show_message: function(exception) {
this.show_error({
- type: _t("Client Error"),
+ type: _t("Odoo Client Error"),
message: exception,
data: {debug: ""}
});
@@ -162,7 +192,7 @@ var RedirectWarningHandler = Dialog.extend(ExceptionHandler, {
new Dialog(this, {
size: 'medium',
- title: "Odoo " + (_.str.capitalize(error.type) || "Warning"),
+ title: _.str.capitalize(error.type) || _t("Odoo Warning"),
buttons: [
{text: error.data.arguments[2], classes : "btn-primary", click: function() {
window.location.href = '#action='+error.data.arguments[1];
diff --git a/addons/web/static/src/less/modal.less b/addons/web/static/src/less/modal.less
index bca950796fc..2a0fbb1fa36 100644
--- a/addons/web/static/src/less/modal.less
+++ b/addons/web/static/src/less/modal.less
@@ -33,7 +33,7 @@
text-align: left;
}
}
-
+
@media (min-width: @screen-sm-min) {
.modal-dialog {
@@ -52,6 +52,23 @@
.modal-body {
overflow: auto;
+ min-height: 0;
+
+ &.o_dialog_error {
+ overflow: visible;
+ .o-flex-display();
+ .o-flex-flow(column, nowrap);
+
+ > .alert, > button {
+ .o-flex(0, 0, auto);
+ }
+
+ > .o_error_detail {
+ .o-flex(1, 1, auto);
+ min-height: 0;
+ overflow: auto;
+ }
+ }
}
}
}
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index b0bc4e76c4a..b732944f052 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -19,21 +19,30 @@
<span class="o_icon fa fa-3x fa-exclamation"/>
</t>
</t>
-<t t-name="CrashManager.warning">
- <div class="o_dialog_warning">
- <t t-js="d">
- var message = d.message ? d.message : d.error.data.message;
- d.html_error = context.engine.tools.html_escape(message).replace(/\n/g, '<br/>');
- </t>
- <t t-raw="html_error"/>
+
+<div t-name="CrashManager.warning" class="o_dialog_warning">
+ <t t-js="d">
+ var message = d.message ? d.message : d.error.data.message;
+ d.html_error = context.engine.tools.html_escape(message).replace(/\n/g, '<br/>');
+ </t>
+ <t t-raw="html_error"/>
+</div>
+<div t-name="CrashManager.error" class="o_dialog_error">
+ <div class="alert alert-warning clearfix" role="alert">
+ <button class="btn btn-sm btn-primary pull-right ml8 o_clipboard_button">
+ <i class="fa fa-clipboard mr8"/>Copy the full error to clipboard
+ </button>
+ <p><b>An error occurred</b></p>
+ <p>Please use the copy button to report the error to your support service.</p>
</div>
-</t>
-<t t-name="CrashManager.error">
- <div class="o_error_detail">
+
+ <t t-set="errUID" t-value="_.uniqueId()"/>
+ <button class="btn btn-sm btn-link" t-att-data-target="'#o_error_detail' + errUID" data-toggle="collapse">See details</button>
+ <div t-att-id="'o_error_detail' + errUID" class="collapse alert alert-danger o_error_detail">
<pre><t t-esc="error.message"/></pre>
<pre><t t-esc="error.data.debug"/></pre>
</div>
-</t>
+</div>
<form t-name="ChangePassword" name="change_password_form" method="POST">
<div class="o_form_view">