1 /*---------------------------------------------------------
3 *---------------------------------------------------------*/
6 var instance = openerp;
7 openerp.web.chrome = {};
9 var QWeb = instance.web.qweb,
12 instance.web.Notification = instance.web.Widget.extend({
13 template: 'Notification',
15 this._super.apply(this, arguments);
16 instance.web.notification = this;
19 this._super.apply(this, arguments);
25 notify: function(title, text, sticky) {
31 return this.$el.notify('create', {
36 warn: function(title, text, sticky) {
42 return this.$el.notify('create', 'oe_notification_alert', {
49 instance.web.action_notify = function(element, action) {
50 element.do_notify(action.params.title, action.params.text, action.params.sticky);
52 instance.web.client_actions.add("action_notify", "instance.web.action_notify");
54 instance.web.action_warn = function(element, action) {
55 element.do_warn(action.params.title, action.params.text, action.params.sticky);
57 instance.web.client_actions.add("action_warn", "instance.web.action_warn");
60 * The very minimal function everything should call to create a dialog
61 * in OpenERP Web Client.
63 instance.web.dialog = function(element) {
64 var result = element.dialog.apply(element, _.rest(_.toArray(arguments)));
65 result.dialog("widget").openerpClass();
70 A useful class to handle dialogs.
73 - $buttons: A jQuery element targeting a dom part where buttons can be added. It always exists
74 during the lifecycle of the dialog.
76 instance.web.Dialog = instance.web.Widget.extend({
81 @param {Widget} parent
82 @param {dictionary} options A dictionary that will be forwarded to jQueryUI Dialog. Additionaly, that
83 dictionary can contain the following keys:
84 - buttons: Deprecated. The buttons key is not propagated to jQueryUI Dialog. It must be a dictionary (key = button
85 label, value = click handler) or a list of dictionaries (each element in the dictionary is send to the
86 corresponding method of a jQuery element targeting the <button> tag). It is deprecated because all dialogs
87 in OpenERP must be personalized in some way (button in red, link instead of button, ...) and this
88 feature does not allow that kind of personalization.
89 - destroy_on_close: Default true. If true and the dialog is closed, it is automatically destroyed.
90 @param {jQuery object} content Some content to replace this.$el .
92 init: function (parent, options, content) {
95 this.content_to_set = content;
96 this.dialog_options = {
98 destroy_on_close: true,
104 max_height: $(window.top).height() - 200,
106 position: [false, 40],
108 beforeClose: function () {
109 self.trigger("closing");
111 resizeStop: function() {
112 self.trigger("resized");
116 _.extend(this.dialog_options, options);
118 this.on("closing", this, this._closing);
119 this.$buttons = $('<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"><span class="oe_dialog_custom_buttons"/></div>');
121 _get_options: function() {
123 var o = _.extend({}, this.dialog_options);
125 width: $(window.top).width(),
126 height: $(window.top).height(),
128 _.each(sizes, function(available_size, unit) {
129 o[unit] = self._get_size(o[unit], available_size);
130 o['min_' + unit] = self._get_size(o['min_' + unit] || 0, available_size);
131 o['max_' + unit] = self._get_size(o['max_' + unit] || 0, available_size);
132 if (o[unit] !== 'auto' && o['min_' + unit] && o[unit] < o['min_' + unit]) {
133 o[unit] = o['min_' + unit];
135 if (o[unit] !== 'auto' && o['max_' + unit] && o[unit] > o['max_' + unit]) {
136 o[unit] = o['max_' + unit];
139 o.title = o.title || this.dialog_title;
142 _get_size: function(val, available_size) {
143 val = val.toString();
144 if (val === 'auto') {
146 } else if (val.slice(-1) === "%") {
147 return Math.round(available_size / 100 * parseInt(val.slice(0, -1), 10));
149 return parseInt(val, 10);
152 renderElement: function() {
153 if (this.content_to_set) {
154 this.setElement(this.content_to_set);
155 } else if (this.template) {
160 Opens the popup. Inits the dialog if it is not already inited.
165 if (!this.dialog_inited) {
168 this.$el.dialog('open');
169 this.$el.dialog("widget").append(this.$buttons);
172 _add_buttons: function(buttons) {
174 var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
175 _.each(buttons, function(fn, text) {
176 // buttons can be object or array
177 if (!_.isFunction(fn)) {
181 var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {} }}}));
182 $customButons.append($but);
183 $but.on('click', function(ev) {
184 fn.call(self.$el, ev);
189 Initializes the popup.
191 @return The result returned by start().
193 init_dialog: function() {
194 var options = this._get_options();
195 if (options.buttons) {
196 this._add_buttons(options.buttons);
197 delete(options.buttons);
199 this.renderElement();
200 instance.web.dialog(this.$el, options);
201 if (options.height === 'auto' && options.max_height) {
202 this.$el.css({ 'max-height': options.max_height, 'overflow-y': 'auto' });
204 this.dialog_inited = true;
205 var res = this.start();
209 Closes the popup, if destroy_on_close was passed to the constructor, it is also destroyed.
212 if (this.dialog_inited && this.$el.is(":data(dialog)")) {
213 this.$el.dialog('close');
216 _closing: function() {
217 if (this.__tmp_dialog_destroying)
219 if (this.dialog_options.destroy_on_close) {
220 this.__tmp_dialog_closing = true;
222 this.__tmp_dialog_closing = undefined;
226 Destroys the popup, also closes it.
228 destroy: function () {
229 this.$buttons.remove();
230 _.each(this.getChildren(), function(el) {
233 if (! this.__tmp_dialog_closing) {
234 this.__tmp_dialog_destroying = true;
236 this.__tmp_dialog_destroying = undefined;
238 if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(dialog)")) {
239 this.$el.dialog('destroy');
245 instance.web.CrashManager = instance.web.Class.extend({
250 rpc_error: function(error) {
254 var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
256 new (handler)(this, error).display();
259 if (error.data.name === "openerp.addons.web.session SessionExpiredException") {
260 this.show_warning({type: "Session Expired", data: { message: _t("Your OpenERP session expired. Please refresh the current web page.") }});
263 if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning"
264 || error.data.exception_type === "access_error") {
265 this.show_warning(error);
267 this.show_error(error);
270 show_warning: function(error) {
274 if (error.data.exception_type === "except_osv") {
275 error = _.extend({}, error, {data: _.extend({}, error.data, {message: error.data.arguments[0] + "\n\n" + error.data.arguments[1]})});
277 instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), {
278 title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
280 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
284 show_error: function(error) {
289 buttons[_t("Ok")] = function() {
290 $(this).dialog("close");
292 var dialog = new instance.web.Dialog(this, {
293 title: "OpenERP " + _.str.capitalize(error.type),
300 dialog.$el.html(QWeb.render('CrashManager.error', {session: instance.session, error: error}));
302 show_message: function(exception) {
304 type: _t("Client Error"),
312 An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
314 instance.web.ExceptionHandler = {
316 @param parent The parent.
317 @param error The error object as returned by the JSON-RPC implementation.
319 init: function(parent, error) {},
321 Called to inform to display the widget, if necessary. A typical way would be to implement
322 this interface in a class extending instance.web.Dialog and simply display the dialog in this
325 display: function() {},
329 The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
330 instance.web.ExceptionHandler.
332 instance.web.crash_manager_registry = new instance.web.Registry();
335 * Handle redirection warnings, which behave more or less like a regular
336 * warning, with an additional redirection button.
338 instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
339 init: function(parent, error) {
343 display: function() {
345 error.data.message = error.data.arguments[0];
347 instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), {
348 title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
350 {text: _t("Ok"), click: function() { $(this).dialog("close"); }},
351 {text: error.data.arguments[2], click: function() {
352 window.location.href='#action='+error.data.arguments[1];
353 $(this).dialog("close");
360 instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
362 instance.web.Loading = instance.web.Widget.extend({
363 template: _t("Loading"),
364 init: function(parent) {
367 this.blocked_ui = false;
368 this.session.on("request", this, this.request_call);
369 this.session.on("response", this, this.response_call);
370 this.session.on("response_failed", this, this.response_call);
372 destroy: function() {
373 this.on_rpc_event(-this.count);
376 request_call: function() {
377 this.on_rpc_event(1);
379 response_call: function() {
380 this.on_rpc_event(-1);
382 on_rpc_event : function(increment) {
384 if (!this.count && increment === 1) {
386 this.long_running_timer = setTimeout(function () {
387 self.blocked_ui = true;
388 instance.web.blockUI();
392 this.count += increment;
393 if (this.count > 0) {
394 if (instance.session.debug) {
395 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
397 this.$el.text(_t("Loading"));
400 this.getParent().$el.addClass('oe_wait');
403 clearTimeout(this.long_running_timer);
404 // Don't unblock if blocked by somebody else
405 if (self.blocked_ui) {
406 this.blocked_ui = false;
407 instance.web.unblockUI();
410 this.getParent().$el.removeClass('oe_wait');
415 instance.web.DatabaseManager = instance.web.Widget.extend({
416 init: function(parent) {
418 this.unblockUIFunction = instance.web.unblockUI;
419 $.validator.addMethod('matches', function (s, _, re) {
420 return new RegExp(re).test(s);
421 }, _t("Invalid database name"));
425 $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
426 var fetch_db = this.rpc("/web/database/get_list", {}).then(
428 self.db_list = result;
434 var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
435 self.lang_list = result;
437 return $.when(fetch_db, fetch_langs).always(self.do_render);
439 do_render: function() {
441 instance.webclient.toggle_bars(true);
442 self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
443 $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
444 $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
445 $('ul.oe_secondary_submenu > li:first').addClass('oe_active');
446 $('ul.oe_secondary_submenu > li').bind('click', function (event) {
447 var menuitem = $(this);
448 menuitem.addClass('oe_active').siblings().removeClass('oe_active');
449 var form_id =menuitem.find('a').attr('href');
450 $(form_id).show().siblings().hide();
451 event.preventDefault();
453 $('#back-to-login').click(self.do_exit);
454 self.$el.find("td").addClass("oe_form_group_cell");
455 self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
456 self.$el.find("label").addClass("oe_form_label");
457 self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
458 self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
459 self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
460 self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
461 self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
462 self.$el.find("form[name=change_pwd_form]").validate({
464 old_pwd: _t("Please enter your previous password"),
465 new_pwd: _t("Please enter your new password"),
467 required: _t("Please confirm your new password"),
468 equalTo: _t("The confirmation does not match the password")
471 submitHandler: self.do_change_password
474 destroy: function () {
475 this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
479 * Converts a .serializeArray() result into a dict. Does not bother folding
480 * multiple identical keys into an array, last key wins.
482 * @param {Array} array
484 to_object: function (array) {
486 _(array).each(function (record) {
487 result[record.name] = record.value;
492 * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
493 * from unblocking the UI
495 blockUI: function () {
496 instance.web.blockUI();
497 instance.web.unblockUI = function () {};
500 * Reinstates $.unblockUI so third parties can play with blockUI, and
503 unblockUI: function () {
504 instance.web.unblockUI = this.unblockUIFunction;
505 instance.web.unblockUI();
508 * Displays an error dialog resulting from the various RPC communications
509 * failing over themselves
511 * @param {Object} error error description
512 * @param {String} error.title title of the error dialog
513 * @param {String} error.error message of the error dialog
515 display_error: function (error) {
516 return instance.web.dialog($('<div>'), {
520 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
522 }).html(error.error);
524 do_create: function(form) {
526 var fields = $(form).serializeArray();
527 self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
528 var form_obj = self.to_object(fields);
529 var client_action = {
530 type: 'ir.actions.client',
533 'db': form_obj['db_name'],
535 'password': form_obj['create_admin_pwd'],
536 'login_successful': function() {
537 var url = '/?db=' + form_obj['db_name'];
538 if (self.session.debug) {
541 instance.web.redirect(url);
546 self.do_action(client_action);
549 do_duplicate: function(form) {
551 var fields = $(form).serializeArray();
552 self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
554 self.display_error(result);
557 self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
561 do_drop: function(form) {
564 fields = $form.serializeArray(),
565 $db_list = $form.find('[name=drop_db]'),
567 if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
570 self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
572 self.display_error(result);
575 self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
579 do_backup: function(form) {
582 self.session.get_file({
584 success: function () {
585 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
587 error: function(error){
590 title: _t("Backup Database"),
591 error: 'AccessDenied'
595 complete: function() {
600 do_restore: function(form) {
604 url: '/web/database/restore',
607 success: function (body) {
608 // If empty body, everything went fine
609 if (!body) { return; }
611 if (body.indexOf('403 Forbidden') !== -1) {
613 title: _t("Access Denied"),
614 error: _t("Incorrect super-administrator password")
618 title: _t("Restore Database"),
619 error: _t("Could not restore the database")
623 complete: function() {
625 self.do_notify(_t("Restored"), _t("Database restored successfully"));
629 do_change_password: function(form) {
631 self.rpc("/web/database/change_password", {
632 'fields': $(form).serializeArray()
633 }).done(function(result) {
635 self.display_error(result);
639 self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
642 do_exit: function () {
644 instance.webclient.show_login();
647 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
649 instance.web.Login = instance.web.Widget.extend({
651 remember_credentials: true,
653 'change input[name=db],select[name=db]': function(ev) {
654 this.set('database_selector', $(ev.currentTarget).val());
658 init: function(parent, action) {
660 this.has_local_storage = typeof(localStorage) != 'undefined';
662 this.selected_db = null;
663 this.selected_login = null;
664 this.params = action.params || {};
665 if (_.isEmpty(this.params)) {
666 this.params = $.bbq.getState(true);
668 if (action && action.params && action.params.db) {
669 this.params.db = action.params.db;
670 } else if ($.deparam.querystring().db) {
671 this.params.db = $.deparam.querystring().db;
673 if (this.params.db) {
674 this.selected_db = this.params.db;
677 if (this.params.login_successful) {
678 this.on('login_successful', this, this.params.login_successful);
683 self.$el.find("form").submit(self.on_submit);
684 self.$el.find('.oe_login_manage_db').click(function() {
685 self.do_action("database_manager");
687 self.on('change:database_selector', this, function() {
688 this.database_selected(this.get('database_selector'));
691 if ($.param.fragment().token) {
692 self.params.token = $.param.fragment().token;
694 // used by dbmanager.do_create via internal client action
695 if (self.params.db && self.params.login && self.params.password) {
696 d = self.do_login(self.params.db, self.params.login, self.params.password);
698 d = self.rpc("/web/database/get_list", {})
699 .done(self.on_db_loaded)
700 .fail(self.on_db_failed)
702 if (self.selected_db && self.has_local_storage && self.remember_credentials) {
703 self.$("[name=login]").val(localStorage.getItem(self.selected_db + '|last_login') || '');
704 if (self.session.debug) {
705 self.$("[name=password]").val(localStorage.getItem(self.selected_db + '|last_password') || '');
712 database_selected: function(db) {
713 var params = $.deparam.querystring();
715 this.$('.oe_login_dbpane').empty().text(_t('Loading...'));
716 this.$('[name=login], [name=password]').prop('readonly', true);
717 instance.web.redirect('/?' + $.param(params));
719 on_db_loaded: function (result) {
721 this.db_list = result;
722 if (!this.selected_db) {
723 this.selected_db = result[0];
725 this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db}));
726 if(this.db_list.length === 0) {
727 this.do_action("database_manager");
728 } else if(this.db_list.length === 1) {
729 this.$('div.oe_login_dbpane').hide();
731 this.$('div.oe_login_dbpane').show();
734 on_db_failed: function (error, event) {
735 if (error.data.name === 'openerp.exceptions.AccessDenied') {
736 event.preventDefault();
739 on_submit: function(ev) {
743 var db = this.$("form [name=db]").val();
745 this.do_warn(_t("Login"), _t("No database selected !"));
748 var login = this.$("form input[name=login]").val();
749 var password = this.$("form input[name=password]").val();
751 this.do_login(db, login, password);
754 * Performs actual login operation, and UI-related stuff
756 * @param {String} db database to log in
757 * @param {String} login user login
758 * @param {String} password user password
760 do_login: function (db, login, password) {
763 self.$(".oe_login_pane").fadeOut("slow");
764 return this.session.session_authenticate(db, login, password).then(function() {
765 if (self.has_local_storage && self.remember_credentials) {
766 localStorage.setItem(db + '|last_login', login);
767 if (self.session.debug) {
768 localStorage.setItem(db + '|last_password', password);
771 self.trigger('login_successful');
773 self.$(".oe_login_pane").fadeIn("fast", function() {
774 self.show_error(_t("Invalid username or password"));
778 show_error: function(message) {
779 this.$el.addClass("oe_login_invalid");
780 this.$(".oe_login_error_message").text(message);
782 hide_error: function() {
783 this.$el.removeClass('oe_login_invalid');
786 instance.web.client_actions.add("login", "instance.web.Login");
790 * Redirect to url by replacing window.location
791 * If wait is true, sleep 1s and wait for the server i.e. after a restart.
793 instance.web.redirect = function(url, wait) {
794 // Dont display a dialog if some xmlhttprequest are in progress
795 if (instance.client && instance.client.crashmanager) {
796 instance.client.crashmanager.active = false;
799 var load = function() {
800 var old = "" + window.location;
802 window.location.reload();
804 window.location = url;
808 var wait_server = function() {
809 instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
810 setTimeout(wait_server, 250);
815 setTimeout(wait_server, 1000);
822 * Client action to reload the whole interface.
823 * If params.menu_id, it opens the given menu entry.
824 * If params.wait, reload will wait the openerp server to be reachable before reloading
826 instance.web.Reload = function(parent, action) {
827 var params = action.params || {};
828 var menu_id = params.menu_id || false;
829 var l = window.location;
831 var sobj = $.deparam(l.search.substr(1));
832 if (params.url_search) {
833 sobj = _.extend(sobj, params.url_search);
835 var search = '?' + $.param(sobj);
839 hash = "#menu_id=" + menu_id;
841 var url = l.protocol + "//" + l.host + l.pathname + search + hash;
843 instance.web.redirect(url, params.wait);
845 instance.web.client_actions.add("reload", "instance.web.Reload");
848 * Client action to go back in breadcrumb history.
849 * If can't go back in history stack, will go back to home.
851 instance.web.HistoryBack = function(parent) {
852 if (!parent.history_back()) {
853 instance.web.Home(parent);
856 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
859 * Client action to go back home.
861 instance.web.Home = function(parent, action) {
862 var url = '/' + (window.location.search || '');
863 instance.web.redirect(url, action.params && action.params.wait);
865 instance.web.client_actions.add("home", "instance.web.Home");
867 instance.web.ChangePassword = instance.web.Widget.extend({
868 template: "ChangePassword",
871 this.getParent().dialog_title = _t("Change Password");
872 var $button = self.$el.find('.oe_form_button');
873 $button.appendTo(this.getParent().$buttons);
874 $button.eq(2).click(function(){
875 self.getParent().close();
877 $button.eq(0).click(function(){
878 self.rpc("/web/session/change_password",{
879 'fields': $("form[name=change_password_form]").serializeArray()
880 }).done(function(result) {
882 self.display_error(result);
885 instance.webclient.on_logout();
890 display_error: function (error) {
891 return instance.web.dialog($('<div>'), {
895 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
897 }).html(error.error);
900 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
902 instance.web.Menu = instance.web.Widget.extend({
906 this._super.apply(this, arguments);
907 this.has_been_loaded = $.Deferred();
908 this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
909 this.data = {data:{children:[]}};
910 this.on("menu_loaded", this, function (menu_data) {
912 // launch the fetch of needaction counters, asynchronous
913 if (!_.isEmpty(menu_data.all_menu_ids)) {
914 this.do_load_needaction(menu_data.all_menu_ids);
917 var lazyreflow = _.debounce(this.reflow.bind(this), 200);
918 instance.web.bus.on('resize', this, function() {
924 this._super.apply(this, arguments);
925 this.$secondary_menus = this.getParent().$el.find('.oe_secondary_menus_container');
926 this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
927 return this.do_reload();
929 do_reload: function() {
931 return this.rpc("/web/menu/load", {}).done(function(r) {
935 menu_loaded: function(data) {
937 this.data = {data: data};
938 this.renderElement();
939 this.$secondary_menus.html(QWeb.render("Menu.secondary", { widget : this }));
940 this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
941 // Hide second level submenus
942 this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
943 if (self.current_menu) {
944 self.open_menu(self.current_menu);
946 this.trigger('menu_loaded', data);
947 this.has_been_loaded.resolve();
949 do_load_needaction: function (menu_ids) {
951 menu_ids = _.compact(menu_ids);
952 if (_.isEmpty(menu_ids)) {
955 return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
956 self.on_needaction_loaded(r);
959 on_needaction_loaded: function(data) {
961 this.needaction_data = data;
962 _.each(this.needaction_data, function (item, menu_id) {
963 var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
964 $item.find('.oe_menu_counter').remove();
965 if (item.needaction_counter && item.needaction_counter > 0) {
966 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
971 * Reflow the menu items and dock overflowing items into a "More" menu item.
972 * Automatically called when 'menu_loaded' event is triggered and on window resizing.
976 this.$el.height('auto').show();
977 var $more_container = this.$('.oe_menu_more_container').hide();
978 var $more = this.$('.oe_menu_more');
979 $more.children('li').insertBefore($more_container);
980 var $toplevel_items = this.$el.children('li').not($more_container).hide();
981 $toplevel_items.each(function() {
982 var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
983 self.$el.parent().children(':visible').each(function() {
984 remaining_space -= $(this).outerWidth();
986 if ($(this).width() > remaining_space) {
991 $more.append($toplevel_items.filter(':hidden').show());
992 $more_container.toggle(!!$more.children().length);
993 // Hide toplevel item if there is only one
994 var $toplevel = this.$el.children("li:visible");
995 if ($toplevel.length === 1) {
1000 * Opens a given menu by id, as if a user had browsed to that menu by hand
1001 * except does not trigger any event on the way
1003 * @param {Number} id database id of the terminal menu to select
1005 open_menu: function (id) {
1006 this.current_menu = id;
1007 this.session.active_id = id;
1008 var $clicked_menu, $sub_menu, $main_menu;
1009 $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
1010 this.trigger('open_menu', id, $clicked_menu);
1012 if (this.$secondary_menus.has($clicked_menu).length) {
1013 $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
1014 $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
1016 $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
1017 $main_menu = $clicked_menu;
1020 // Activate current main menu
1021 this.$el.find('.oe_active').removeClass('oe_active');
1022 $main_menu.addClass('oe_active');
1024 // Show current sub menu
1025 this.$secondary_menus.find('.oe_secondary_menu').hide();
1028 // Hide/Show the leftbar menu depending of the presence of sub-items
1029 this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
1031 // Activate current menu item and show parents
1032 this.$secondary_menus.find('.oe_active').removeClass('oe_active');
1033 if ($main_menu !== $clicked_menu) {
1034 $clicked_menu.parents().show();
1035 if ($clicked_menu.is('.oe_menu_toggler')) {
1036 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
1038 $clicked_menu.parent().addClass('oe_active');
1043 * Call open_menu with the first menu_item matching an action_id
1045 * @param {Number} id the action_id to match
1047 open_action: function (id) {
1048 var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
1049 var menu_id = $menu.data('menu');
1051 this.open_menu(menu_id);
1055 * Process a click on a menu item
1057 * @param {Number} id the menu_id
1058 * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
1060 menu_click: function(id, needaction) {
1061 if (!id) { return; }
1063 // find back the menuitem in dom to get the action
1064 var $item = this.$el.find('a[data-menu=' + id + ']');
1065 if (!$item.length) {
1066 $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
1068 var action_id = $item.data('action-id');
1069 // If first level menu doesnt have action trigger first leaf
1071 if(this.$el.has($item).length) {
1072 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
1073 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
1075 action_id = $items.data('action-id');
1076 id = $items.data('menu');
1081 this.trigger('menu_click', {
1082 action_id: action_id,
1083 needaction: needaction,
1085 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
1090 do_reload_needaction: function () {
1092 if (self.current_menu) {
1093 self.do_load_needaction([self.current_menu]).then(function () {
1094 self.trigger("need_action_reloaded");
1099 * Jquery event handler for menu click
1101 * @param {Event} ev the jquery event
1103 on_top_menu_click: function(ev) {
1105 var id = $(ev.currentTarget).data('menu');
1106 var menu_ids = [id];
1107 var menu = _.filter(this.data.data.children, function (menu) {return menu.id == id;})[0];
1108 function add_menu_ids (menu) {
1109 if (menu.children) {
1110 _.each(menu.children, function (menu) {
1111 menu_ids.push(menu.id);
1117 self.do_load_needaction(menu_ids).then(function () {
1118 self.trigger("need_action_reloaded");
1120 this.on_menu_click(ev);
1122 on_menu_click: function(ev) {
1123 ev.preventDefault();
1124 var needaction = $(ev.target).is('div.oe_menu_counter');
1125 this.menu_click($(ev.currentTarget).data('menu'), needaction);
1129 instance.web.UserMenu = instance.web.Widget.extend({
1130 template: "UserMenu",
1131 init: function(parent) {
1132 this._super(parent);
1133 this.update_promise = $.Deferred().resolve();
1137 this._super.apply(this, arguments);
1138 this.$el.on('click', '.oe_dropdown_menu li a[data-menu]', function(ev) {
1139 ev.preventDefault();
1140 var f = self['on_menu_' + $(this).data('menu')];
1146 do_update: function () {
1148 var fct = function() {
1149 var $avatar = self.$el.find('.oe_topbar_avatar');
1150 $avatar.attr('src', $avatar.data('default-src'));
1151 if (!self.session.uid)
1153 var func = new instance.web.Model("res.users").get_func("read");
1154 return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
1155 var topbar_name = res.name;
1156 if(instance.session.debug)
1157 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1158 if(res.company_id[0] > 1)
1159 topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
1160 self.$el.find('.oe_topbar_name').text(topbar_name);
1161 if (!instance.session.debug) {
1162 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1164 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
1165 $avatar.attr('src', avatar_src);
1168 this.update_promise = this.update_promise.then(fct, fct);
1170 on_menu_help: function() {
1171 window.open('http://help.openerp.com', '_blank');
1173 on_menu_logout: function() {
1174 this.trigger('user_logout');
1176 on_menu_settings: function() {
1178 if (!this.getParent().has_uncommitted_changes()) {
1179 self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
1180 result.res_id = instance.session.uid;
1181 self.getParent().action_manager.do_action(result);
1185 on_menu_account: function() {
1187 if (!this.getParent().has_uncommitted_changes()) {
1188 var P = new instance.web.Model('ir.config_parameter');
1189 P.call('get_param', ['database.uuid']).then(function(dbuuid) {
1191 'd': instance.session.db,
1192 'u': window.location.protocol + '//' + window.location.host,
1195 response_type: 'token',
1196 client_id: dbuuid || '',
1197 state: JSON.stringify(state),
1200 instance.web.redirect('https://accounts.openerp.com/oauth2/auth?'+$.param(params));
1204 on_menu_about: function() {
1206 self.rpc("/web/webclient/version_info", {}).done(function(res) {
1207 var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1208 $help.find('a.oe_activate_debug_mode').click(function (e) {
1210 window.location = $.param.querystring( window.location.href, 'debug');
1212 instance.web.dialog($help, {autoOpen: true,
1213 modal: true, width: 507, height: 290, resizable: false, title: _t("About")});
1218 instance.web.Client = instance.web.Widget.extend({
1219 init: function(parent, origin) {
1220 instance.client = instance.webclient = this;
1221 this._super(parent);
1222 this.origin = origin;
1226 return instance.session.session_bind(this.origin).then(function() {
1227 var $e = $(QWeb.render(self._template, {widget: self}));
1228 self.replaceElement($e);
1231 return self.show_common();
1234 bind_events: function() {
1236 this.$el.on('mouseenter', '.oe_systray > div:not([data-tipsy=true])', function() {
1237 $(this).attr('data-tipsy', 'true').tipsy().trigger('mouseenter');
1239 this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1240 ev.preventDefault();
1241 var $toggle = $(this);
1242 var $menu = $toggle.siblings('.oe_dropdown_menu');
1243 $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1244 var state = $menu.is('.oe_opened');
1245 setTimeout(function() {
1246 // Do not alter propagation
1247 $toggle.add($menu).toggleClass('oe_opened', !state);
1249 // Move $menu if outside window's edge
1250 var doc_width = $(document).width();
1251 var offset = $menu.offset();
1252 var menu_width = $menu.width();
1253 var x = doc_width - offset.left - menu_width - 2;
1255 $menu.offset({ left: offset.left + x }).width(menu_width);
1260 instance.web.bus.on('click', this, function(ev) {
1262 if (!$(ev.target).is('input[type=file]')) {
1263 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1267 show_common: function() {
1269 this.crashmanager = new instance.web.CrashManager();
1270 instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1271 self.notification = new instance.web.Notification(this);
1272 self.notification.appendTo(self.$el);
1273 self.loading = new instance.web.Loading(self);
1274 self.loading.appendTo(self.$el);
1275 self.action_manager = new instance.web.ActionManager(self);
1276 self.action_manager.appendTo(self.$('.oe_application'));
1278 toggle_bars: function(value) {
1279 this.$('tr:has(td.oe_topbar),.oe_leftbar').toggle(value);
1281 has_uncommitted_changes: function() {
1286 instance.web.WebClient = instance.web.Client.extend({
1287 _template: 'WebClient',
1289 'click .oe_logo_edit_admin': 'logo_edit'
1291 init: function(parent) {
1292 this._super(parent);
1293 this._current_state = null;
1294 this.menu_dm = new instance.web.DropMisordered();
1295 this.action_mutex = new $.Mutex();
1296 this.set('title_part', {"zopenerp": "OpenERP"});
1300 this.on("change:title_part", this, this._title_changed);
1301 this._title_changed();
1302 return $.when(this._super()).then(function() {
1303 if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1304 $("body").addClass("kitten-mode-activated");
1305 $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1307 var imgkit = Math.floor(Math.random() * 2 + 1);
1308 $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">';
1311 if (!self.session.session_is_valid()) {
1314 self.show_application();
1319 Sets the first part of the title of the window, dedicated to the current action.
1321 set_title: function(title) {
1322 this.set_title_part("action", title);
1325 Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
1326 a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
1327 title of the window separated by '-'.
1329 set_title_part: function(part, title) {
1330 var tmp = _.clone(this.get("title_part"));
1332 this.set("title_part", tmp);
1334 _title_changed: function() {
1335 var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
1337 _.each(parts, function(part) {
1338 var str = this.get("title_part")[part];
1340 tmp = tmp ? tmp + " - " + str : str;
1343 document.title = tmp;
1345 show_common: function() {
1348 window.onerror = function (message, file, line) {
1349 self.crashmanager.show_error({
1350 type: _t("Client Error"),
1352 data: {debug: file + ':' + line}
1356 show_login: function() {
1357 this.toggle_bars(false);
1359 var state = $.bbq.getState(true);
1361 type: 'ir.actions.client',
1366 this.action_manager.do_action(action);
1367 this.action_manager.inner_widget.on('login_successful', this, function() {
1368 this.show_application(); // will load the state we just pushed
1371 show_application: function() {
1373 self.toggle_bars(true);
1375 self.menu = new instance.web.Menu(self);
1376 self.menu.replace(this.$el.find('.oe_menu_placeholder'));
1377 self.menu.on('menu_click', this, this.on_menu_action);
1378 self.user_menu = new instance.web.UserMenu(self);
1379 self.user_menu.replace(this.$el.find('.oe_user_menu_placeholder'));
1380 self.user_menu.on('user_logout', self, self.on_logout);
1381 self.user_menu.do_update();
1382 self.bind_hashchange();
1384 self.check_timezone();
1386 update_logo: function() {
1387 var img = this.session.url('/web/binary/company_logo');
1388 this.$('.oe_logo img').attr('src', '').attr('src', img);
1389 this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1391 logo_edit: function(ev) {
1393 self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1394 self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1395 result.res_id = res['company_id'][0];
1396 result.target = "new";
1397 result.views = [[false, 'form']];
1399 action_buttons: true,
1401 self.action_manager.do_action(result);
1402 var form = self.action_manager.dialog_widget.views.form.controller;
1403 form.on("on_button_cancel", self.action_manager.dialog, self.action_manager.dialog.close);
1404 form.on('record_saved', self, function() {
1405 self.action_manager.dialog.close();
1412 check_timezone: function() {
1414 return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1415 var user_offset = result[0]['tz_offset'];
1416 var offset = -(new Date().getTimezoneOffset());
1417 // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1418 var browser_offset = (offset < 0) ? "-" : "+";
1419 browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1420 browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1421 if (browser_offset !== user_offset) {
1422 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1423 $icon.on('click', function() {
1424 var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1425 user_timezone: instance.session.user_context.tz || 'UTC',
1426 user_offset: user_offset,
1427 browser_offset: browser_offset,
1429 notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1430 notification.close();
1431 }).find('a').on('click', function() {
1432 notification.close();
1433 self.user_menu.on_menu_settings();
1437 $icon.appendTo(self.$('.oe_systray'));
1441 destroy_content: function() {
1442 _.each(_.clone(this.getChildren()), function(el) {
1445 this.$el.children().remove();
1447 do_reload: function() {
1449 return this.session.session_reload().then(function () {
1450 instance.session.load_modules(true).then(
1451 self.menu.proxy('do_reload')); });
1454 do_notify: function() {
1455 var n = this.notification;
1456 return n.notify.apply(n, arguments);
1458 do_warn: function() {
1459 var n = this.notification;
1460 return n.warn.apply(n, arguments);
1462 on_logout: function() {
1464 if (!this.has_uncommitted_changes()) {
1465 this.session.session_logout().done(function () {
1466 $(window).unbind('hashchange', self.on_hashchange);
1467 self.do_push_state({});
1468 window.location.reload();
1472 bind_hashchange: function() {
1474 $(window).bind('hashchange', this.on_hashchange);
1476 var state = $.bbq.getState(true);
1477 if (_.isEmpty(state) || state.action == "login") {
1478 self.menu.has_been_loaded.done(function() {
1479 var first_menu_id = self.menu.$el.find("a:first").data("menu");
1481 self.menu.menu_click(first_menu_id);
1485 $(window).trigger('hashchange');
1488 on_hashchange: function(event) {
1490 var stringstate = event.getState(false);
1491 if (!_.isEqual(this._current_state, stringstate)) {
1492 var state = event.getState(true);
1493 if(!state.action && state.menu_id) {
1494 self.menu.has_been_loaded.done(function() {
1495 self.menu.do_reload().done(function() {
1496 self.menu.menu_click(state.menu_id);
1500 state._push_me = false; // no need to push state back...
1501 this.action_manager.do_load_state(state, !!this._current_state);
1504 this._current_state = stringstate;
1506 do_push_state: function(state) {
1507 this.set_title(state.title);
1509 var url = '#' + $.param(state);
1510 this._current_state = $.deparam($.param(state), false); // stringify all values
1511 $.bbq.pushState(url);
1512 this.trigger('state_pushed', state);
1514 on_menu_action: function(options) {
1516 return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1517 .then(function (result) {
1518 return self.action_mutex.exec(function() {
1519 if (options.needaction) {
1520 result.context = new instance.web.CompoundContext(result.context, {
1521 search_default_message_unread: true,
1522 search_disable_custom_filters: true,
1525 var completed = $.Deferred();
1526 $.when(self.action_manager.do_action(result, {
1527 clear_breadcrumbs: true,
1528 action_menu_id: self.menu.current_menu,
1529 })).fail(function() {
1530 self.menu.open_menu(options.previous_menu_id);
1531 }).always(function() {
1532 completed.resolve();
1534 setTimeout(function() {
1535 completed.resolve();
1537 // We block the menu when clicking on an element until the action has correctly finished
1538 // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1543 set_content_full_screen: function(fullscreen) {
1544 $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1545 this.$('.oe_webclient').toggleClass(
1546 'oe_content_full_screen', fullscreen);
1548 has_uncommitted_changes: function() {
1549 var $e = $.Event('clear_uncommitted_changes');
1550 instance.web.bus.trigger('clear_uncommitted_changes', $e);
1551 if ($e.isDefaultPrevented()) {
1554 return this._super.apply(this, arguments);
1559 instance.web.EmbeddedClient = instance.web.Client.extend({
1560 _template: 'EmbedClient',
1561 init: function(parent, origin, dbname, login, key, action_id, options) {
1562 this._super(parent, origin);
1563 this.bind_credentials(dbname, login, key);
1564 this.action_id = action_id;
1565 this.options = options || {};
1569 return $.when(this._super()).then(function() {
1570 return self.authenticate().then(function() {
1571 if (!self.action_id) {
1574 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1575 var action = result;
1576 action.flags = _.extend({
1577 //views_switcher : false,
1578 search_view : false,
1579 action_buttons : false,
1582 }, self.options, action.flags || {});
1584 self.do_action(action);
1590 do_action: function(/*...*/) {
1591 var am = this.action_manager;
1592 return am.do_action.apply(am, arguments);
1595 authenticate: function() {
1596 var s = instance.session;
1597 if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1600 return instance.session.session_authenticate(this.dbname, this.login, this.key);
1603 bind_credentials: function(dbname, login, key) {
1604 this.dbname = dbname;
1611 instance.web.embed = function (origin, dbname, login, key, action, options) {
1612 $('head').append($('<link>', {
1613 'rel': 'stylesheet',
1615 'href': origin +'/web/webclient/css'
1617 var currentScript = document.currentScript;
1618 if (!currentScript) {
1619 var sc = document.getElementsByTagName('script');
1620 currentScript = sc[sc.length-1];
1622 var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1623 client.insertAfter(currentScript);
1628 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: