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 A useful class to handle dialogs.
63 - $buttons: A jQuery element targeting a dom part where buttons can be added. It always exists
64 during the lifecycle of the dialog.
66 instance.web.Dialog = instance.web.Widget.extend({
71 @param {Widget} parent
72 @param {dictionary} options A dictionary that will be forwarded to jQueryUI Dialog. Additionaly, that
73 dictionary can contain the following keys:
74 - size: one of the following: 'large', 'medium', 'small'
75 - dialogClass: class to add to the body of dialog
76 - buttons: Deprecated. The buttons key is not propagated to jQueryUI Dialog. It must be a dictionary (key = button
77 label, value = click handler) or a list of dictionaries (each element in the dictionary is send to the
78 corresponding method of a jQuery element targeting the <button> tag). It is deprecated because all dialogs
79 in OpenERP must be personalized in some way (button in red, link instead of button, ...) and this
80 feature does not allow that kind of personalization.
81 - destroy_on_close: Default true. If true and the dialog is closed, it is automatically destroyed.
82 @param {jQuery object} content Some content to replace this.$el .
84 init: function (parent, options, content) {
87 this.content_to_set = content;
88 this.dialog_options = {
89 destroy_on_close: true,
90 size: 'large', //'medium', 'small'
94 _.extend(this.dialog_options, options);
96 this.on("closing", this, this._closing);
97 this.$buttons = $('<div class="modal-footer"><span class="oe_dialog_custom_buttons"/></div>');
99 renderElement: function() {
100 if (this.content_to_set) {
101 this.setElement(this.content_to_set);
102 } else if (this.template) {
107 Opens the popup. Inits the dialog if it is not already inited.
112 if (!this.dialog_inited) {
115 this.$buttons.insertAfter(this.$dialog_box.find(".modal-body"));
118 _add_buttons: function(buttons) {
120 var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
121 _.each(buttons, function(fn, text) {
122 // buttons can be object or array
123 if (!_.isFunction(fn)) {
127 var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {} }}}));
128 $customButons.append($but);
129 $but.on('click', function(ev) {
130 fn.call(self.$el, ev);
135 Initializes the popup.
137 @return The result returned by start().
139 init_dialog: function() {
141 var options = _.extend({}, this.dialog_options);
142 options.title = options.title || this.dialog_title;
143 if (options.buttons) {
144 this._add_buttons(options.buttons);
145 delete(options.buttons);
147 this.renderElement();
149 this.$dialog_box = $(QWeb.render('Dialog', options)).appendTo("body");
154 if (options.size !== 'large'){
155 var dialog_class_size = this.$dialog_box.find('.modal-lg').removeClass('modal-lg')
156 if (options.size === 'small'){
157 dialog_class_size.addClass('modal-sm');
161 this.$el.appendTo(this.$dialog_box.find(".modal-body"));
162 var $dialog_content = this.$dialog_box.find('.modal-content');
163 if (options.dialogClass){
164 $dialog_content.find(".modal-body").addClass(options.dialogClass);
166 $dialog_content.openerpClass();
168 this.$dialog_box.on('hidden.bs.modal', this, function(){
169 self.trigger("closing");
171 this.$dialog_box.modal('show');
173 this.dialog_inited = true;
174 var res = this.start();
178 Closes the popup, if destroy_on_close was passed to the constructor, it is also destroyed.
180 close: function(reason) {
181 if (this.dialog_inited && this.$el.is(":data(bs.modal)")) {
182 this.$el.parents('.modal').modal('hide');
185 _closing: function() {
186 if (this.__tmp_dialog_destroying)
188 if (this.dialog_options.destroy_on_close) {
189 this.__tmp_dialog_closing = true;
191 this.__tmp_dialog_closing = undefined;
195 Destroys the popup, also closes it.
197 destroy: function (reason) {
198 this.$buttons.remove();
200 _.each(this.getChildren(), function(el) {
203 if (! this.__tmp_dialog_closing) {
204 this.__tmp_dialog_destroying = true;
206 this.__tmp_dialog_destroying = undefined;
208 if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(bs.modal)")) {
209 //we need this to put the instruction to remove modal from DOM at the end
210 //of the queue, otherwise it might already have been removed before the modal-backdrop
211 //is removed when pressing escape key
212 var $parent = this.$el.parents('.modal');
213 setTimeout(function () {
221 instance.web.CrashManager = instance.web.Class.extend({
226 rpc_error: function(error) {
230 var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
232 new (handler)(this, error).display();
235 if (error.data.name === "openerp.http.SessionExpiredException" || error.data.name === "werkzeug.exceptions.Forbidden") {
236 this.show_warning({type: "Session Expired", data: { message: _t("Your OpenERP session expired. Please refresh the current web page.") }});
239 if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning"
240 || error.data.exception_type === "access_error") {
241 this.show_warning(error);
243 this.show_error(error);
246 show_warning: function(error) {
250 if (error.data.exception_type === "except_osv") {
251 error = _.extend({}, error, {data: _.extend({}, error.data, {message: error.data.arguments[0] + "\n\n" + error.data.arguments[1]})});
253 new instance.web.Dialog(this, {
255 title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
257 {text: _t("Ok"), click: function() { this.parents('.modal').modal('hide'); }}
259 }, $('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>')).open();
261 show_error: function(error) {
266 buttons[_t("Ok")] = function() {
267 this.parents('.modal').modal('hide');
269 new instance.web.Dialog(this, {
270 title: "OpenERP " + _.str.capitalize(error.type),
272 }, QWeb.render('CrashManager.error', {session: instance.session, error: error})).open();
274 show_message: function(exception) {
276 type: _t("Client Error"),
284 An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
286 instance.web.ExceptionHandler = {
288 @param parent The parent.
289 @param error The error object as returned by the JSON-RPC implementation.
291 init: function(parent, error) {},
293 Called to inform to display the widget, if necessary. A typical way would be to implement
294 this interface in a class extending instance.web.Dialog and simply display the dialog in this
297 display: function() {},
301 The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
302 instance.web.ExceptionHandler.
304 instance.web.crash_manager_registry = new instance.web.Registry();
307 * Handle redirection warnings, which behave more or less like a regular
308 * warning, with an additional redirection button.
310 instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
311 init: function(parent, error) {
315 display: function() {
317 error.data.message = error.data.arguments[0];
319 new instance.web.Dialog(this, {
321 title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
323 {text: _t("Ok"), click: function() { this.$el.parents('.modal').modal('hide'); }},
324 {text: error.data.arguments[2], click: function() {
325 window.location.href='#action='+error.data.arguments[1];
326 this.$el.parents('.modal').modal('hide');
329 }, QWeb.render('CrashManager.warning', {error: error})).open();
333 instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
335 instance.web.Loading = instance.web.Widget.extend({
336 template: _t("Loading"),
337 init: function(parent) {
340 this.blocked_ui = false;
341 this.session.on("request", this, this.request_call);
342 this.session.on("response", this, this.response_call);
343 this.session.on("response_failed", this, this.response_call);
345 destroy: function() {
346 this.on_rpc_event(-this.count);
349 request_call: function() {
350 this.on_rpc_event(1);
352 response_call: function() {
353 this.on_rpc_event(-1);
355 on_rpc_event : function(increment) {
357 if (!this.count && increment === 1) {
359 this.long_running_timer = setTimeout(function () {
360 self.blocked_ui = true;
361 instance.web.blockUI();
365 this.count += increment;
366 if (this.count > 0) {
367 if (instance.session.debug) {
368 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
370 this.$el.text(_t("Loading"));
373 this.getParent().$el.addClass('oe_wait');
376 clearTimeout(this.long_running_timer);
377 // Don't unblock if blocked by somebody else
378 if (self.blocked_ui) {
379 this.blocked_ui = false;
380 instance.web.unblockUI();
383 this.getParent().$el.removeClass('oe_wait');
388 instance.web.DatabaseManager = instance.web.Widget.extend({
389 init: function(parent) {
391 this.unblockUIFunction = instance.web.unblockUI;
392 $.validator.addMethod('matches', function (s, _, re) {
393 return new RegExp(re).test(s);
394 }, _t("Invalid database name"));
398 $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
399 var fetch_db = this.rpc("/web/database/get_list", {}).then(
401 self.db_list = result;
407 var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
408 self.lang_list = result;
410 return $.when(fetch_db, fetch_langs).always(self.do_render);
412 do_render: function() {
414 instance.webclient.toggle_bars(true);
415 self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
416 $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
417 $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
418 $('ul.oe_secondary_submenu > li:first').addClass('active');
419 $('ul.oe_secondary_submenu > li').bind('click', function (event) {
420 var menuitem = $(this);
421 menuitem.addClass('active').siblings().removeClass('active');
422 var form_id =menuitem.find('a').attr('href');
423 $(form_id).show().siblings().hide();
424 event.preventDefault();
426 $('#back-to-login').click(self.do_exit);
427 self.$el.find("td").addClass("oe_form_group_cell");
428 self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
429 self.$el.find("label").addClass("oe_form_label");
430 self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
431 self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
432 self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
433 self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
434 self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
435 self.$el.find("form[name=change_pwd_form]").validate({
437 old_pwd: _t("Please enter your previous password"),
438 new_pwd: _t("Please enter your new password"),
440 required: _t("Please confirm your new password"),
441 equalTo: _t("The confirmation does not match the password")
444 submitHandler: self.do_change_password
447 destroy: function () {
448 this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
452 * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
453 * from unblocking the UI
455 blockUI: function () {
456 instance.web.blockUI();
457 instance.web.unblockUI = function () {};
460 * Reinstates $.unblockUI so third parties can play with blockUI, and
463 unblockUI: function () {
464 instance.web.unblockUI = this.unblockUIFunction;
465 instance.web.unblockUI();
468 * Displays an error dialog resulting from the various RPC communications
469 * failing over themselves
471 * @param {Object} error error description
472 * @param {String} error.title title of the error dialog
473 * @param {String} error.error message of the error dialog
475 display_error: function (error) {
476 return new instance.web.Dialog(this, {
480 {text: _t("Ok"), click: function() { this.$el.parents('.modal').modal('hide'); }}
482 }, $('<div>').html(error.error)).open();
484 do_create: function(form) {
486 var fields = $(form).serializeArray();
487 self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
489 instance.web.redirect('/web');
491 alert("Failed to create database");
495 do_duplicate: function(form) {
497 var fields = $(form).serializeArray();
498 self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
500 self.display_error(result);
503 self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
507 do_drop: function(form) {
510 fields = $form.serializeArray(),
511 $db_list = $form.find('[name=drop_db]'),
513 if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
516 self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
518 self.display_error(result);
521 self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
525 do_backup: function(form) {
528 self.session.get_file({
530 success: function () {
531 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
533 error: function(error){
536 title: _t("Backup Database"),
537 error: 'AccessDenied'
541 complete: function() {
546 do_restore: function(form) {
550 url: '/web/database/restore',
553 success: function (body) {
554 // If empty body, everything went fine
555 if (!body) { return; }
557 if (body.indexOf('403 Forbidden') !== -1) {
559 title: _t("Access Denied"),
560 error: _t("Incorrect super-administrator password")
564 title: _t("Restore Database"),
565 error: _t("Could not restore the database")
569 complete: function() {
571 self.do_notify(_t("Restored"), _t("Database restored successfully"));
575 do_change_password: function(form) {
577 self.rpc("/web/database/change_password", {
578 'fields': $(form).serializeArray()
579 }).done(function(result) {
581 self.display_error(result);
585 self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
588 do_exit: function () {
590 instance.web.redirect('/web');
593 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
595 instance.web.login = function() {
596 instance.web.redirect('/web/login');
598 instance.web.client_actions.add("login", "instance.web.login");
600 instance.web.logout = function() {
601 instance.web.redirect('/web/session/logout');
603 instance.web.client_actions.add("logout", "instance.web.logout");
607 * Redirect to url by replacing window.location
608 * If wait is true, sleep 1s and wait for the server i.e. after a restart.
610 instance.web.redirect = function(url, wait) {
611 // Dont display a dialog if some xmlhttprequest are in progress
612 if (instance.client && instance.client.crashmanager) {
613 instance.client.crashmanager.active = false;
616 var load = function() {
617 var old = "" + window.location;
618 var old_no_hash = old.split("#")[0];
619 var url_no_hash = url.split("#")[0];
620 location.assign(url);
621 if (old_no_hash === url_no_hash) {
622 location.reload(true);
626 var wait_server = function() {
627 instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
628 setTimeout(wait_server, 250);
633 setTimeout(wait_server, 1000);
640 * Client action to reload the whole interface.
641 * If params.menu_id, it opens the given menu entry.
642 * If params.wait, reload will wait the openerp server to be reachable before reloading
644 instance.web.Reload = function(parent, action) {
645 var params = action.params || {};
646 var menu_id = params.menu_id || false;
647 var l = window.location;
649 var sobj = $.deparam(l.search.substr(1));
650 if (params.url_search) {
651 sobj = _.extend(sobj, params.url_search);
653 var search = '?' + $.param(sobj);
657 hash = "#menu_id=" + menu_id;
659 var url = l.protocol + "//" + l.host + l.pathname + search + hash;
661 instance.web.redirect(url, params.wait);
663 instance.web.client_actions.add("reload", "instance.web.Reload");
666 * Client action to go back in breadcrumb history.
667 * If can't go back in history stack, will go back to home.
669 instance.web.HistoryBack = function(parent) {
670 if (!parent.history_back()) {
671 instance.web.Home(parent);
674 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
677 * Client action to go back home.
679 instance.web.Home = function(parent, action) {
680 var url = '/' + (window.location.search || '');
681 instance.web.redirect(url, action && action.params && action.params.wait);
683 instance.web.client_actions.add("home", "instance.web.Home");
685 instance.web.ChangePassword = instance.web.Widget.extend({
686 template: "ChangePassword",
689 this.getParent().dialog_title = _t("Change Password");
690 var $button = self.$el.find('.oe_form_button');
691 $button.appendTo(this.getParent().$buttons);
692 $button.eq(2).click(function(){
693 self.getParent().close();
695 $button.eq(0).click(function(){
696 self.rpc("/web/session/change_password",{
697 'fields': $("form[name=change_password_form]").serializeArray()
698 }).done(function(result) {
700 self.display_error(result);
703 instance.webclient.on_logout();
708 display_error: function (error) {
709 return new instance.web.Dialog(this, {
713 {text: _t("Ok"), click: function() { this.$el.parents('.modal').modal('hide'); }}
715 }, $('<div>').html(error.error)).open();
718 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
720 instance.web.Menu = instance.web.Widget.extend({
724 this._super.apply(this, arguments);
725 this.has_been_loaded = $.Deferred();
726 this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
727 this.data = {data:{children:[]}};
728 this.on("menu_loaded", this, function (menu_data) {
730 // launch the fetch of needaction counters, asynchronous
731 if (!_.isEmpty(menu_data.all_menu_ids)) {
732 this.do_load_needaction(menu_data.all_menu_ids);
735 var lazyreflow = _.debounce(this.reflow.bind(this), 200);
736 instance.web.bus.on('resize', this, function() {
742 this._super.apply(this, arguments);
743 this.$secondary_menus = this.getParent().$el.find('.oe_secondary_menus_container');
744 this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
745 return this.do_reload();
747 do_reload: function() {
749 return this.rpc("/web/menu/load", {}).done(function(r) {
753 menu_loaded: function(data) {
755 this.data = {data: data};
756 this.renderElement();
757 this.$secondary_menus.html(QWeb.render("Menu.secondary", { widget : this }));
758 this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
759 // Hide second level submenus
760 this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
761 if (self.current_menu) {
762 self.open_menu(self.current_menu);
764 this.trigger('menu_loaded', data);
765 this.has_been_loaded.resolve();
767 do_load_needaction: function (menu_ids) {
769 menu_ids = _.compact(menu_ids);
770 if (_.isEmpty(menu_ids)) {
773 return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
774 self.on_needaction_loaded(r);
777 on_needaction_loaded: function(data) {
779 this.needaction_data = data;
780 _.each(this.needaction_data, function (item, menu_id) {
781 var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
782 $item.find('.badge').remove();
783 if (item.needaction_counter && item.needaction_counter > 0) {
784 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
789 * Reflow the menu items and dock overflowing items into a "More" menu item.
790 * Automatically called when 'menu_loaded' event is triggered and on window resizing.
794 this.$el.height('auto').show();
795 var $more_container = this.$('#menu_more_container').hide();
796 var $more = this.$('#menu_more');
797 $more.children('li').insertBefore($more_container);
798 var $toplevel_items = this.$el.children('li').not($more_container).hide();
799 $toplevel_items.each(function() {
800 var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
801 self.$el.parent().children(':visible').each(function() {
802 remaining_space -= $(this).outerWidth();
804 if ($(this).width() > remaining_space) {
809 $more.append($toplevel_items.filter(':hidden').show());
810 $more_container.toggle(!!$more.children().length);
811 // Hide toplevel item if there is only one
812 var $toplevel = this.$el.children("li:visible");
813 if ($toplevel.length === 1) {
818 * Opens a given menu by id, as if a user had browsed to that menu by hand
819 * except does not trigger any event on the way
821 * @param {Number} id database id of the terminal menu to select
823 open_menu: function (id) {
824 this.current_menu = id;
825 this.session.active_id = id;
826 var $clicked_menu, $sub_menu, $main_menu;
827 $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
828 this.trigger('open_menu', id, $clicked_menu);
830 if (this.$secondary_menus.has($clicked_menu).length) {
831 $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
832 $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
834 $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
835 $main_menu = $clicked_menu;
838 // Activate current main menu
839 this.$el.find('.active').removeClass('active');
840 $main_menu.addClass('active');
842 // Show current sub menu
843 this.$secondary_menus.find('.oe_secondary_menu').hide();
846 // Hide/Show the leftbar menu depending of the presence of sub-items
847 this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
849 // Activate current menu item and show parents
850 this.$secondary_menus.find('.active').removeClass('active');
851 if ($main_menu !== $clicked_menu) {
852 $clicked_menu.parents().show();
853 if ($clicked_menu.is('.oe_menu_toggler')) {
854 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
856 $clicked_menu.parent().addClass('active');
861 * Call open_menu with the first menu_item matching an action_id
863 * @param {Number} id the action_id to match
865 open_action: function (id) {
866 var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
867 var menu_id = $menu.data('menu');
869 this.open_menu(menu_id);
873 * Process a click on a menu item
875 * @param {Number} id the menu_id
876 * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
878 menu_click: function(id, needaction) {
881 // find back the menuitem in dom to get the action
882 var $item = this.$el.find('a[data-menu=' + id + ']');
884 $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
886 var action_id = $item.data('action-id');
887 // If first level menu doesnt have action trigger first leaf
889 if(this.$el.has($item).length) {
890 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
891 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
893 action_id = $items.data('action-id');
894 id = $items.data('menu');
899 this.trigger('menu_click', {
900 action_id: action_id,
901 needaction: needaction,
903 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
908 do_reload_needaction: function () {
910 if (self.current_menu) {
911 self.do_load_needaction([self.current_menu]).then(function () {
912 self.trigger("need_action_reloaded");
917 * Jquery event handler for menu click
919 * @param {Event} ev the jquery event
921 on_top_menu_click: function(ev) {
923 var id = $(ev.currentTarget).data('menu');
925 var menu = _.filter(this.data.data.children, function (menu) {return menu.id == id;})[0];
926 function add_menu_ids (menu) {
928 _.each(menu.children, function (menu) {
929 menu_ids.push(menu.id);
935 self.do_load_needaction(menu_ids).then(function () {
936 self.trigger("need_action_reloaded");
938 this.on_menu_click(ev);
940 on_menu_click: function(ev) {
942 var needaction = $(ev.target).is('div#menu_counter');
943 this.menu_click($(ev.currentTarget).data('menu'), needaction);
947 instance.web.UserMenu = instance.web.Widget.extend({
948 template: "UserMenu",
949 init: function(parent) {
951 this.update_promise = $.Deferred().resolve();
955 this._super.apply(this, arguments);
956 this.$el.on('click', '.dropdown-menu li a[data-menu]', function(ev) {
958 var f = self['on_menu_' + $(this).data('menu')];
964 do_update: function () {
966 var fct = function() {
967 var $avatar = self.$el.find('.oe_topbar_avatar');
968 $avatar.attr('src', $avatar.data('default-src'));
969 if (!self.session.uid)
971 var func = new instance.web.Model("res.users").get_func("read");
972 return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
973 var topbar_name = res.name;
974 if(instance.session.debug)
975 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
976 if(res.company_id[0] > 1)
977 topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
978 self.$el.find('.oe_topbar_name').text(topbar_name);
979 if (!instance.session.debug) {
980 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
982 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
983 $avatar.attr('src', avatar_src);
986 this.update_promise = this.update_promise.then(fct, fct);
988 on_menu_help: function() {
989 window.open('http://help.openerp.com', '_blank');
991 on_menu_logout: function() {
992 this.trigger('user_logout');
994 on_menu_settings: function() {
996 if (!this.getParent().has_uncommitted_changes()) {
997 self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
998 result.res_id = instance.session.uid;
999 self.getParent().action_manager.do_action(result);
1003 on_menu_account: function() {
1005 if (!this.getParent().has_uncommitted_changes()) {
1006 var P = new instance.web.Model('ir.config_parameter');
1007 P.call('get_param', ['database.uuid']).then(function(dbuuid) {
1009 'd': instance.session.db,
1010 'u': window.location.protocol + '//' + window.location.host,
1013 response_type: 'token',
1014 client_id: dbuuid || '',
1015 state: JSON.stringify(state),
1018 instance.web.redirect('https://accounts.openerp.com/oauth2/auth?'+$.param(params));
1022 on_menu_about: function() {
1024 self.rpc("/web/webclient/version_info", {}).done(function(res) {
1025 var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1026 $help.find('a.oe_activate_debug_mode').click(function (e) {
1028 window.location = $.param.querystring( window.location.href, 'debug');
1030 new instance.web.Dialog(this, {
1032 dialogClass: 'oe_act_window',
1039 instance.web.FullscreenWidget = instance.web.Widget.extend({
1041 * Widgets extending the FullscreenWidget will be displayed fullscreen,
1042 * and will have a fixed 1:1 zoom level on mobile devices.
1045 if(!$('#oe-fullscreenwidget-viewport').length){
1046 $('head').append('<meta id="oe-fullscreenwidget-viewport" name="viewport" content="initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">');
1048 instance.webclient.set_content_full_screen(true);
1049 return this._super();
1051 destroy: function(){
1052 instance.webclient.set_content_full_screen(false);
1053 $('#oe-fullscreenwidget-viewport').remove();
1054 return this._super();
1059 instance.web.Client = instance.web.Widget.extend({
1060 init: function(parent, origin) {
1061 instance.client = instance.webclient = this;
1062 this.client_options = {};
1063 this._super(parent);
1064 this.origin = origin;
1068 return instance.session.session_bind(this.origin).then(function() {
1069 var $e = $(QWeb.render(self._template, {widget: self}));
1070 self.replaceElement($e);
1073 return self.show_common();
1076 bind_events: function() {
1078 this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function() {
1079 $(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
1081 this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1082 ev.preventDefault();
1083 var $toggle = $(this);
1084 var doc_width = $(document).width();
1085 var $menu = $toggle.siblings('.oe_dropdown_menu');
1086 $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1087 var state = $menu.is('.oe_opened');
1088 setTimeout(function() {
1089 // Do not alter propagation
1090 $toggle.add($menu).toggleClass('oe_opened', !state);
1092 // Move $menu if outside window's edge
1093 var offset = $menu.offset();
1094 var menu_width = $menu.width();
1095 var x = doc_width - offset.left - menu_width - 2;
1097 $menu.offset({ left: offset.left + x }).width(menu_width);
1102 instance.web.bus.on('click', this, function(ev) {
1103 $.fn.tooltip('destroy');
1104 if (!$(ev.target).is('input[type=file]')) {
1105 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1109 show_common: function() {
1111 this.crashmanager = new instance.web.CrashManager();
1112 instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1113 self.notification = new instance.web.Notification(this);
1114 self.notification.appendTo(self.$el);
1115 self.loading = new instance.web.Loading(self);
1116 self.loading.appendTo(self.$el);
1117 self.action_manager = new instance.web.ActionManager(self);
1118 self.action_manager.appendTo(self.$('.oe_application'));
1120 toggle_bars: function(value) {
1121 this.$('tr:has(td.navbar),.oe_leftbar').toggle(value);
1123 has_uncommitted_changes: function() {
1128 instance.web.WebClient = instance.web.Client.extend({
1129 _template: 'WebClient',
1131 'click .oe_logo_edit_admin': 'logo_edit'
1133 init: function(parent, client_options) {
1134 this._super(parent);
1135 if (client_options) {
1136 _.extend(this.client_options, client_options);
1138 this._current_state = null;
1139 this.menu_dm = new instance.web.DropMisordered();
1140 this.action_mutex = new $.Mutex();
1141 this.set('title_part', {"zopenerp": "OpenERP"});
1145 this.on("change:title_part", this, this._title_changed);
1146 this._title_changed();
1147 return $.when(this._super()).then(function() {
1148 if (jQuery.deparam !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1151 if (self.session.session_is_valid()) {
1152 self.show_application();
1154 if (self.client_options.action) {
1155 self.action_manager.do_action(self.client_options.action);
1156 delete(self.client_options.action);
1160 to_kitten: function() {
1162 $("body").addClass("kitten-mode-activated");
1163 $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1165 var imgkit = Math.floor(Math.random() * 2 + 1);
1166 $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">';
1170 Sets the first part of the title of the window, dedicated to the current action.
1172 set_title: function(title) {
1173 this.set_title_part("action", title);
1176 Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
1177 a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
1178 title of the window separated by '-'.
1180 set_title_part: function(part, title) {
1181 var tmp = _.clone(this.get("title_part"));
1183 this.set("title_part", tmp);
1185 _title_changed: function() {
1186 var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
1188 _.each(parts, function(part) {
1189 var str = this.get("title_part")[part];
1191 tmp = tmp ? tmp + " - " + str : str;
1194 document.title = tmp;
1196 show_common: function() {
1199 window.onerror = function (message, file, line) {
1200 self.crashmanager.show_error({
1201 type: _t("Client Error"),
1203 data: {debug: file + ':' + line}
1207 show_application: function() {
1209 self.toggle_bars(true);
1211 self.menu = new instance.web.Menu(self);
1212 self.menu.replace(this.$el.find('.oe_menu_placeholder'));
1213 self.menu.on('menu_click', this, this.on_menu_action);
1214 self.user_menu = new instance.web.UserMenu(self);
1215 self.user_menu.replace(this.$el.find('.oe_user_menu_placeholder'));
1216 self.user_menu.on('user_logout', self, self.on_logout);
1217 self.user_menu.do_update();
1218 self.bind_hashchange();
1220 self.check_timezone();
1221 if (self.client_options.action_post_login) {
1222 self.action_manager.do_action(self.client_options.action_post_login);
1223 delete(self.client_options.action_post_login);
1226 update_logo: function() {
1227 var img = this.session.url('/web/binary/company_logo');
1228 this.$('.oe_logo img').attr('src', '').attr('src', img);
1229 this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1231 logo_edit: function(ev) {
1233 self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1234 self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1235 result.res_id = res['company_id'][0];
1236 result.target = "new";
1237 result.views = [[false, 'form']];
1239 action_buttons: true,
1241 self.action_manager.do_action(result);
1242 var form = self.action_manager.dialog_widget.views.form.controller;
1243 form.on("on_button_cancel", self.action_manager, self.action_manager.dialog_stop);
1244 form.on('record_saved', self, function() {
1245 self.action_manager.dialog_stop();
1252 check_timezone: function() {
1254 return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1255 var user_offset = result[0]['tz_offset'];
1256 var offset = -(new Date().getTimezoneOffset());
1257 // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1258 var browser_offset = (offset < 0) ? "-" : "+";
1259 browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1260 browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1261 if (browser_offset !== user_offset) {
1262 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1263 $icon.on('click', function() {
1264 var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1265 user_timezone: instance.session.user_context.tz || 'UTC',
1266 user_offset: user_offset,
1267 browser_offset: browser_offset,
1269 notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1270 notification.close();
1271 }).find('a').on('click', function() {
1272 notification.close();
1273 self.user_menu.on_menu_settings();
1277 $icon.appendTo(self.$('.oe_systray'));
1281 destroy_content: function() {
1282 _.each(_.clone(this.getChildren()), function(el) {
1285 this.$el.children().remove();
1287 do_reload: function() {
1289 return this.session.session_reload().then(function () {
1290 instance.session.load_modules(true).then(
1291 self.menu.proxy('do_reload')); });
1294 do_notify: function() {
1295 var n = this.notification;
1296 return n.notify.apply(n, arguments);
1298 do_warn: function() {
1299 var n = this.notification;
1300 return n.warn.apply(n, arguments);
1302 on_logout: function() {
1304 if (!this.has_uncommitted_changes()) {
1305 self.action_manager.do_action('logout');
1308 bind_hashchange: function() {
1310 $(window).bind('hashchange', this.on_hashchange);
1312 var state = $.bbq.getState(true);
1313 if (_.isEmpty(state) || state.action == "login") {
1314 self.menu.has_been_loaded.done(function() {
1315 new instance.web.Model("res.users").call("read", [self.session.uid, ["action_id"]]).done(function(data) {
1316 if(data.action_id) {
1317 self.action_manager.do_action(data.action_id[0]);
1318 self.menu.open_action(data.action_id[0]);
1320 var first_menu_id = self.menu.$el.find("a:first").data("menu");
1322 self.menu.menu_click(first_menu_id);
1327 $(window).trigger('hashchange');
1330 on_hashchange: function(event) {
1332 var stringstate = event.getState(false);
1333 if (!_.isEqual(this._current_state, stringstate)) {
1334 var state = event.getState(true);
1335 if(!state.action && state.menu_id) {
1336 self.menu.has_been_loaded.done(function() {
1337 self.menu.do_reload().done(function() {
1338 self.menu.menu_click(state.menu_id);
1342 state._push_me = false; // no need to push state back...
1343 this.action_manager.do_load_state(state, !!this._current_state);
1346 this._current_state = stringstate;
1348 do_push_state: function(state) {
1349 this.set_title(state.title);
1351 var url = '#' + $.param(state);
1352 this._current_state = $.deparam($.param(state), false); // stringify all values
1353 $.bbq.pushState(url);
1354 this.trigger('state_pushed', state);
1356 on_menu_action: function(options) {
1358 return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1359 .then(function (result) {
1360 return self.action_mutex.exec(function() {
1361 if (options.needaction) {
1362 result.context = new instance.web.CompoundContext(result.context, {
1363 search_default_message_unread: true,
1364 search_disable_custom_filters: true,
1367 var completed = $.Deferred();
1368 $.when(self.action_manager.do_action(result, {
1369 clear_breadcrumbs: true,
1370 action_menu_id: self.menu.current_menu,
1371 })).fail(function() {
1372 self.menu.open_menu(options.previous_menu_id);
1373 }).always(function() {
1374 completed.resolve();
1376 setTimeout(function() {
1377 completed.resolve();
1379 // We block the menu when clicking on an element until the action has correctly finished
1380 // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1385 set_content_full_screen: function(fullscreen) {
1386 $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1387 this.$('.oe_webclient').toggleClass(
1388 'oe_content_full_screen', fullscreen);
1390 has_uncommitted_changes: function() {
1391 var $e = $.Event('clear_uncommitted_changes');
1392 instance.web.bus.trigger('clear_uncommitted_changes', $e);
1393 if ($e.isDefaultPrevented()) {
1396 return this._super.apply(this, arguments);
1401 instance.web.EmbeddedClient = instance.web.Client.extend({
1402 _template: 'EmbedClient',
1403 init: function(parent, origin, dbname, login, key, action_id, options) {
1404 this._super(parent, origin);
1405 this.bind_credentials(dbname, login, key);
1406 this.action_id = action_id;
1407 this.options = options || {};
1411 return $.when(this._super()).then(function() {
1412 return self.authenticate().then(function() {
1413 if (!self.action_id) {
1416 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1417 var action = result;
1418 action.flags = _.extend({
1419 //views_switcher : false,
1420 search_view : false,
1421 action_buttons : false,
1424 }, self.options, action.flags || {});
1426 self.do_action(action);
1432 do_action: function(/*...*/) {
1433 var am = this.action_manager;
1434 return am.do_action.apply(am, arguments);
1437 authenticate: function() {
1438 var s = instance.session;
1439 if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1442 return instance.session.session_authenticate(this.dbname, this.login, this.key);
1445 bind_credentials: function(dbname, login, key) {
1446 this.dbname = dbname;
1453 instance.web.embed = function (origin, dbname, login, key, action, options) {
1454 $('head').append($('<link>', {
1455 'rel': 'stylesheet',
1457 'href': origin +'/web/webclient/css'
1459 var currentScript = document.currentScript;
1460 if (!currentScript) {
1461 var sc = document.getElementsByTagName('script');
1462 currentScript = sc[sc.length-1];
1464 var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1465 client.insertAfter(currentScript);
1470 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: