1 /*---------------------------------------------------------
3 *---------------------------------------------------------*/
4 openerp.web.chrome = function(instance) {
5 var QWeb = instance.web.qweb,
8 instance.web.Notification = instance.web.Widget.extend({
9 template: 'Notification',
11 this._super.apply(this, arguments);
12 instance.web.notification = this;
15 this._super.apply(this, arguments);
21 notify: function(title, text, sticky) {
27 return this.$el.notify('create', {
32 warn: function(title, text, sticky) {
38 return this.$el.notify('create', 'oe_notification_alert', {
45 instance.web.action_notify = function(element, action) {
46 element.do_notify(action.params.title, action.params.text, action.params.sticky);
48 instance.web.client_actions.add("action_notify", "instance.web.action_notify");
50 instance.web.action_warn = function(element, action) {
51 element.do_warn(action.params.title, action.params.text, action.params.sticky);
53 instance.web.client_actions.add("action_warn", "instance.web.action_warn");
56 * The very minimal function everything should call to create a dialog
57 * in OpenERP Web Client.
59 instance.web.dialog = function(element) {
60 var result = element.dialog.apply(element, _.rest(_.toArray(arguments)));
61 result.dialog("widget").openerpClass();
66 A useful class to handle dialogs.
69 - $buttons: A jQuery element targeting a dom part where buttons can be added. It always exists
70 during the lifecycle of the dialog.
72 instance.web.Dialog = instance.web.Widget.extend({
77 @param {Widget} parent
78 @param {dictionary} options A dictionary that will be forwarded to jQueryUI Dialog. Additionaly, that
79 dictionary can contain the following keys:
80 - buttons: Deprecated. The buttons key is not propagated to jQueryUI Dialog. It must be a dictionary (key = button
81 label, value = click handler) or a list of dictionaries (each element in the dictionary is send to the
82 corresponding method of a jQuery element targeting the <button> tag). It is deprecated because all dialogs
83 in OpenERP must be personalized in some way (button in red, link instead of button, ...) and this
84 feature does not allow that kind of personalization.
85 - destroy_on_close: Default true. If true and the dialog is closed, it is automatically destroyed.
86 @param {jQuery object} content Some content to replace this.$el .
88 init: function (parent, options, content) {
91 this.content_to_set = content;
92 this.dialog_options = {
94 destroy_on_close: true,
100 max_height: $(window.top).height() - 200,
102 position: [false, 40],
104 beforeClose: function () {
105 self.trigger("closing");
107 resizeStop: function() {
108 self.trigger("resized");
112 _.extend(this.dialog_options, options);
114 this.on("closing", this, this._closing);
115 this.$buttons = $('<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"><span class="oe_dialog_custom_buttons"/></div>');
117 _get_options: function() {
119 var o = _.extend({}, this.dialog_options);
121 width: $(window.top).width(),
122 height: $(window.top).height(),
124 _.each(sizes, function(available_size, unit) {
125 o[unit] = self._get_size(o[unit], available_size);
126 o['min_' + unit] = self._get_size(o['min_' + unit] || 0, available_size);
127 o['max_' + unit] = self._get_size(o['max_' + unit] || 0, available_size);
128 if (o[unit] !== 'auto' && o['min_' + unit] && o[unit] < o['min_' + unit]) {
129 o[unit] = o['min_' + unit];
131 if (o[unit] !== 'auto' && o['max_' + unit] && o[unit] > o['max_' + unit]) {
132 o[unit] = o['max_' + unit];
135 o.title = o.title || this.dialog_title;
138 _get_size: function(val, available_size) {
139 val = val.toString();
140 if (val === 'auto') {
142 } else if (val.slice(-1) === "%") {
143 return Math.round(available_size / 100 * parseInt(val.slice(0, -1), 10));
145 return parseInt(val, 10);
148 renderElement: function() {
149 if (this.content_to_set) {
150 this.setElement(this.content_to_set);
151 } else if (this.template) {
156 Opens the popup. Inits the dialog if it is not already inited.
161 if (!this.dialog_inited) {
164 this.$el.dialog('open');
165 this.$el.dialog("widget").append(this.$buttons);
168 _add_buttons: function(buttons) {
170 var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
171 _.each(buttons, function(fn, text) {
172 // buttons can be object or array
173 if (!_.isFunction(fn)) {
177 var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {} }}}));
178 $customButons.append($but);
179 $but.on('click', function(ev) {
180 fn.call(self.$el, ev);
185 Initializes the popup.
187 @return The result returned by start().
189 init_dialog: function() {
190 var options = this._get_options();
191 if (options.buttons) {
192 this._add_buttons(options.buttons);
193 delete(options.buttons);
195 this.renderElement();
196 instance.web.dialog(this.$el, options);
197 if (options.height === 'auto' && options.max_height) {
198 this.$el.css({ 'max-height': options.max_height, 'overflow-y': 'auto' });
200 this.dialog_inited = true;
201 var res = this.start();
205 Closes the popup, if destroy_on_close was passed to the constructor, it is also destroyed.
208 if (this.dialog_inited && this.$el.is(":data(dialog)")) {
209 this.$el.dialog('close');
212 _closing: function() {
213 if (this.__tmp_dialog_destroying)
215 if (this.dialog_options.destroy_on_close) {
216 this.__tmp_dialog_closing = true;
218 this.__tmp_dialog_closing = undefined;
222 Destroys the popup, also closes it.
224 destroy: function () {
225 this.$buttons.remove();
226 _.each(this.getChildren(), function(el) {
229 if (! this.__tmp_dialog_closing) {
230 this.__tmp_dialog_destroying = true;
232 this.__tmp_dialog_destroying = undefined;
234 if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(dialog)")) {
235 this.$el.dialog('destroy');
241 instance.web.CrashManager = instance.web.Class.extend({
246 rpc_error: function(error) {
250 // yes, exception handling is shitty
251 if (error.code === 300 && error.data && error.data.type == "client_exception" && error.data.debug.match("SessionExpiredException")) {
252 this.show_warning({type: "Session Expired", data: {
253 fault_code: _t("Your OpenERP session expired. Please refresh the current web page.")
257 if (error.data.fault_code) {
258 var split = ("" + error.data.fault_code).split('\n')[0].split(' -- ');
259 if (split.length > 1) {
260 error.type = split.shift();
261 error.data.fault_code = error.data.fault_code.substr(error.type.length + 4);
264 if (error.code === 200 && error.type) {
265 this.show_warning(error);
267 this.show_error(error);
270 show_warning: function(error) {
274 instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), {
275 title: "OpenERP " + _.str.capitalize(error.type),
277 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
281 show_error: function(error) {
286 buttons[_t("Ok")] = function() {
287 $(this).dialog("close");
289 var dialog = new instance.web.Dialog(this, {
290 title: "OpenERP " + _.str.capitalize(error.type),
297 dialog.$el.html(QWeb.render('CrashManager.error', {session: instance.session, error: error}));
299 show_message: function(exception) {
301 type: _t("Client Error"),
308 instance.web.Loading = instance.web.Widget.extend({
309 template: _t("Loading"),
310 init: function(parent) {
313 this.blocked_ui = false;
314 this.session.on("request", this, this.request_call);
315 this.session.on("response", this, this.response_call);
316 this.session.on("response_failed", this, this.response_call);
318 destroy: function() {
319 this.on_rpc_event(-this.count);
322 request_call: function() {
323 this.on_rpc_event(1);
325 response_call: function() {
326 this.on_rpc_event(-1);
328 on_rpc_event : function(increment) {
330 if (!this.count && increment === 1) {
332 this.long_running_timer = setTimeout(function () {
333 self.blocked_ui = true;
334 instance.web.blockUI();
338 this.count += increment;
339 if (this.count > 0) {
340 if (instance.session.debug) {
341 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
343 this.$el.text(_t("Loading"));
346 this.getParent().$el.addClass('oe_wait');
349 clearTimeout(this.long_running_timer);
350 // Don't unblock if blocked by somebody else
351 if (self.blocked_ui) {
352 this.blocked_ui = false;
353 instance.web.unblockUI();
356 this.getParent().$el.removeClass('oe_wait');
361 instance.web.DatabaseManager = instance.web.Widget.extend({
362 init: function(parent) {
364 this.unblockUIFunction = instance.web.unblockUI;
365 $.validator.addMethod('matches', function (s, _, re) {
366 return new RegExp(re).test(s);
367 }, _t("Invalid database name"));
371 $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
372 var fetch_db = this.rpc("/web/database/get_list", {}).then(
374 self.db_list = result;
380 var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
381 self.lang_list = result;
383 return $.when(fetch_db, fetch_langs).always(self.do_render);
385 do_render: function() {
387 instance.webclient.toggle_bars(true);
388 self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
389 $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
390 $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
391 $('ul.oe_secondary_submenu > li:first').addClass('oe_active')
392 $('ul.oe_secondary_submenu > li').bind('click', function (event) {
393 var menuitem = $(this);
394 menuitem.addClass('oe_active').siblings().removeClass('oe_active');
395 var form_id =menuitem.find('a').attr('href');
396 $(form_id).show().siblings().hide();
397 event.preventDefault();
399 $('#back-to-login').click(self.do_exit);
400 self.$el.find("td").addClass("oe_form_group_cell");
401 self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
402 self.$el.find("label").addClass("oe_form_label");
403 self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
404 self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
405 self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
406 self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
407 self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
408 self.$el.find("form[name=change_pwd_form]").validate({
410 old_pwd: _t("Please enter your previous password"),
411 new_pwd: _t("Please enter your new password"),
413 required: _t("Please confirm your new password"),
414 equalTo: _t("The confirmation does not match the password")
417 submitHandler: self.do_change_password
420 destroy: function () {
421 this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
425 * Converts a .serializeArray() result into a dict. Does not bother folding
426 * multiple identical keys into an array, last key wins.
428 * @param {Array} array
430 to_object: function (array) {
432 _(array).each(function (record) {
433 result[record.name] = record.value;
438 * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
439 * from unblocking the UI
441 blockUI: function () {
442 instance.web.blockUI();
443 instance.web.unblockUI = function () {};
446 * Reinstates $.unblockUI so third parties can play with blockUI, and
449 unblockUI: function () {
450 instance.web.unblockUI = this.unblockUIFunction;
451 instance.web.unblockUI();
454 * Displays an error dialog resulting from the various RPC communications
455 * failing over themselves
457 * @param {Object} error error description
458 * @param {String} error.title title of the error dialog
459 * @param {String} error.error message of the error dialog
461 display_error: function (error) {
462 return instance.web.dialog($('<div>'), {
466 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
468 }).html(error.error);
470 do_create: function(form) {
472 var fields = $(form).serializeArray();
473 self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
474 var form_obj = self.to_object(fields);
475 var client_action = {
476 type: 'ir.actions.client',
479 'db': form_obj['db_name'],
481 'password': form_obj['create_admin_pwd'],
482 'login_successful': function() {
483 var url = '/?db=' + form_obj['db_name'];
484 if (self.session.debug) {
487 instance.web.redirect(url);
492 self.do_action(client_action);
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.webclient.show_login();
593 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
595 instance.web.Login = instance.web.Widget.extend({
597 remember_credentials: true,
599 'change input[name=db],select[name=db]': function(ev) {
600 this.set('database_selector', $(ev.currentTarget).val());
604 init: function(parent, action) {
606 this.has_local_storage = typeof(localStorage) != 'undefined';
608 this.selected_db = null;
609 this.selected_login = null;
610 this.params = action.params || {};
611 if (_.isEmpty(this.params)) {
612 this.params = $.bbq.getState(true);
614 if (action && action.params && action.params.db) {
615 this.params.db = action.params.db;
616 } else if ($.deparam.querystring().db) {
617 this.params.db = $.deparam.querystring().db;
619 if (this.params.db) {
620 this.selected_db = this.params.db;
623 if (this.params.login_successful) {
624 this.on('login_successful', this, this.params.login_successful);
629 self.$el.find("form").submit(self.on_submit);
630 self.$el.find('.oe_login_manage_db').click(function() {
631 self.do_action("database_manager");
633 self.on('change:database_selector', this, function() {
634 this.database_selected(this.get('database_selector'));
637 if ($.param.fragment().token) {
638 self.params.token = $.param.fragment().token;
640 // used by dbmanager.do_create via internal client action
641 if (self.params.db && self.params.login && self.params.password) {
642 d = self.do_login(self.params.db, self.params.login, self.params.password);
644 d = self.rpc("/web/database/get_list", {})
645 .done(self.on_db_loaded)
646 .fail(self.on_db_failed)
648 if (self.selected_db && self.has_local_storage && self.remember_credentials) {
649 self.$("[name=login]").val(localStorage.getItem(self.selected_db + '|last_login') || '');
650 if (self.session.debug) {
651 self.$("[name=password]").val(localStorage.getItem(self.selected_db + '|last_password') || '');
658 remember_last_used_database: function(db) {
659 // This cookie will be used server side in order to avoid db reloading on first visit
660 var ttl = 24 * 60 * 60 * 365;
662 'last_used_database=' + db,
665 'expires=' + new Date(new Date().getTime() + ttl * 1000).toGMTString()
668 database_selected: function(db) {
669 var params = $.deparam.querystring();
671 this.remember_last_used_database(db);
672 this.$('.oe_login_dbpane').empty().text(_t('Loading...'));
673 this.$('[name=login], [name=password]').prop('readonly', true);
674 instance.web.redirect('/?' + $.param(params));
676 on_db_loaded: function (result) {
678 this.db_list = result;
679 if (!this.selected_db) {
680 this.selected_db = result[0];
682 this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db}));
683 if(this.db_list.length === 0) {
684 this.do_action("database_manager");
685 } else if(this.db_list.length === 1) {
686 this.$('div.oe_login_dbpane').hide();
688 this.$('div.oe_login_dbpane').show();
691 on_db_failed: function (error, event) {
692 if (error.data.fault_code === 'AccessDenied') {
693 event.preventDefault();
696 on_submit: function(ev) {
700 var db = this.$("form [name=db]").val();
702 this.do_warn(_t("Login"), _t("No database selected !"));
705 var login = this.$("form input[name=login]").val();
706 var password = this.$("form input[name=password]").val();
708 this.do_login(db, login, password);
711 * Performs actual login operation, and UI-related stuff
713 * @param {String} db database to log in
714 * @param {String} login user login
715 * @param {String} password user password
717 do_login: function (db, login, password) {
720 self.$(".oe_login_pane").fadeOut("slow");
721 return this.session.session_authenticate(db, login, password).then(function() {
722 self.remember_last_used_database(db);
723 if (self.has_local_storage && self.remember_credentials) {
724 localStorage.setItem(db + '|last_login', login);
725 if (self.session.debug) {
726 localStorage.setItem(db + '|last_password', password);
729 self.trigger('login_successful');
731 self.$(".oe_login_pane").fadeIn("fast", function() {
732 self.show_error(_t("Invalid username or password"));
736 show_error: function(message) {
737 this.$el.addClass("oe_login_invalid");
738 this.$(".oe_login_error_message").text(message);
740 hide_error: function() {
741 this.$el.removeClass('oe_login_invalid');
744 instance.web.client_actions.add("login", "instance.web.Login");
748 * Redirect to url by replacing window.location
749 * If wait is true, sleep 1s and wait for the server i.e. after a restart.
751 instance.web.redirect = function(url, wait) {
752 // Dont display a dialog if some xmlhttprequest are in progress
753 if (instance.client && instance.client.crashmanager) {
754 instance.client.crashmanager.active = false;
757 var wait_server = function() {
758 instance.session.rpc("/web/webclient/version_info", {}).done(function() {
759 window.location = url;
761 setTimeout(wait_server, 250);
766 setTimeout(wait_server, 1000);
768 window.location = url;
773 * Client action to reload the whole interface.
774 * If params.menu_id, it opens the given menu entry.
775 * If params.wait, reload will wait the openerp server to be reachable before reloading
777 instance.web.Reload = function(parent, action) {
778 var params = action.params || {};
779 var menu_id = params.menu_id || false;
780 var l = window.location;
782 var sobj = $.deparam(l.search.substr(1));
783 sobj.ts = new Date().getTime();
784 if (params.url_search) {
785 sobj = _.extend(sobj, params.url_search);
787 var search = '?' + $.param(sobj);
791 hash = "#menu_id=" + menu_id;
793 var url = l.protocol + "//" + l.host + l.pathname + search + hash;
795 instance.web.redirect(url, params.wait);
797 instance.web.client_actions.add("reload", "instance.web.Reload");
800 * Client action to go back in breadcrumb history.
801 * If can't go back in history stack, will go back to home.
803 instance.web.HistoryBack = function(parent) {
804 if (!parent.history_back()) {
805 instance.web.Home(parent);
808 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
811 * Client action to go back home.
813 instance.web.Home = function(parent, action) {
814 var url = '/' + (window.location.search || '');
815 instance.web.redirect(url, action.params && action.params.wait);
817 instance.web.client_actions.add("home", "instance.web.Home");
819 instance.web.ChangePassword = instance.web.Widget.extend({
820 template: "ChangePassword",
823 this.getParent().dialog_title = _t("Change Password");
824 var $button = self.$el.find('.oe_form_button');
825 $button.appendTo(this.getParent().$buttons);
826 $button.eq(2).click(function(){
827 self.getParent().close();
829 $button.eq(0).click(function(){
830 self.rpc("/web/session/change_password",{
831 'fields': $("form[name=change_password_form]").serializeArray()
832 }).done(function(result) {
834 self.display_error(result);
837 instance.webclient.on_logout();
842 display_error: function (error) {
843 return instance.web.dialog($('<div>'), {
847 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
849 }).html(error.error);
852 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
854 instance.web.Menu = instance.web.Widget.extend({
858 this._super.apply(this, arguments);
859 this.has_been_loaded = $.Deferred();
860 this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
861 this.data = {data:{children:[]}};
862 this.on("menu_loaded", this, function (menu_data) {
864 // launch the fetch of needaction counters, asynchronous
865 if (!_.isEmpty(menu_data.all_menu_ids)) {
866 this.do_load_needaction(menu_data.all_menu_ids);
869 var lazyreflow = _.debounce(this.reflow.bind(this), 200);
870 instance.web.bus.on('resize', this, function() {
876 this._super.apply(this, arguments);
877 this.$secondary_menus = this.getParent().$el.find('.oe_secondary_menus_container');
878 this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
879 return this.do_reload();
881 do_reload: function() {
883 return this.rpc("/web/menu/load", {}).done(function(r) {
887 menu_loaded: function(data) {
889 this.data = {data: data};
890 this.renderElement();
891 this.$secondary_menus.html(QWeb.render("Menu.secondary", { widget : this }));
892 this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
893 // Hide second level submenus
894 this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
895 if (self.current_menu) {
896 self.open_menu(self.current_menu);
898 this.trigger('menu_loaded', data);
899 this.has_been_loaded.resolve();
901 do_load_needaction: function (menu_ids) {
903 menu_ids = _.compact(menu_ids);
904 if (_.isEmpty(menu_ids)) {
907 return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
908 self.on_needaction_loaded(r);
911 on_needaction_loaded: function(data) {
913 this.needaction_data = data;
914 _.each(this.needaction_data, function (item, menu_id) {
915 var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
916 $item.find('.oe_menu_counter').remove();
917 if (item.needaction_counter && item.needaction_counter > 0) {
918 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
923 * Reflow the menu items and dock overflowing items into a "More" menu item.
924 * Automatically called when 'menu_loaded' event is triggered and on window resizing.
928 this.$el.height('auto').show();
929 var $more_container = this.$('.oe_menu_more_container').hide();
930 var $more = this.$('.oe_menu_more');
931 $more.children('li').insertBefore($more_container);
932 var $toplevel_items = this.$el.children('li').not($more_container).hide();
933 $toplevel_items.each(function() {
934 var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
935 self.$el.parent().children(':visible').each(function() {
936 remaining_space -= $(this).outerWidth();
938 if ($(this).width() > remaining_space) {
943 $more.append($toplevel_items.filter(':hidden').show());
944 $more_container.toggle(!!$more.children().length);
945 // Hide toplevel item if there is only one
946 var $toplevel = this.$el.children("li:visible");
947 if ($toplevel.length === 1) {
952 * Opens a given menu by id, as if a user had browsed to that menu by hand
953 * except does not trigger any event on the way
955 * @param {Number} id database id of the terminal menu to select
957 open_menu: function (id) {
958 this.current_menu = id;
959 this.session.active_id = id;
960 var $clicked_menu, $sub_menu, $main_menu;
961 $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
962 this.trigger('open_menu', id, $clicked_menu);
964 if (this.$secondary_menus.has($clicked_menu).length) {
965 $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
966 $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
968 $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
969 $main_menu = $clicked_menu;
972 // Activate current main menu
973 this.$el.find('.oe_active').removeClass('oe_active');
974 $main_menu.addClass('oe_active');
976 // Show current sub menu
977 this.$secondary_menus.find('.oe_secondary_menu').hide();
980 // Hide/Show the leftbar menu depending of the presence of sub-items
981 this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
983 // Activate current menu item and show parents
984 this.$secondary_menus.find('.oe_active').removeClass('oe_active');
985 if ($main_menu !== $clicked_menu) {
986 $clicked_menu.parents().show();
987 if ($clicked_menu.is('.oe_menu_toggler')) {
988 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
990 $clicked_menu.parent().addClass('oe_active');
995 * Call open_menu with the first menu_item matching an action_id
997 * @param {Number} id the action_id to match
999 open_action: function (id) {
1000 var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
1001 var menu_id = $menu.data('menu');
1003 this.open_menu(menu_id);
1007 * Process a click on a menu item
1009 * @param {Number} id the menu_id
1010 * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
1012 menu_click: function(id, needaction) {
1013 if (!id) { return; }
1015 // find back the menuitem in dom to get the action
1016 var $item = this.$el.find('a[data-menu=' + id + ']');
1017 if (!$item.length) {
1018 $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
1020 var action_id = $item.data('action-id');
1021 // If first level menu doesnt have action trigger first leaf
1023 if(this.$el.has($item).length) {
1024 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
1025 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
1027 action_id = $items.data('action-id');
1028 id = $items.data('menu');
1033 this.trigger('menu_click', {
1034 action_id: action_id,
1035 needaction: needaction,
1037 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
1042 do_reload_needaction: function () {
1044 if (self.current_menu) {
1045 self.do_load_needaction([self.current_menu]).then(function () {
1046 self.trigger("need_action_reloaded");
1051 * Jquery event handler for menu click
1053 * @param {Event} ev the jquery event
1055 on_top_menu_click: function(ev) {
1057 var id = $(ev.currentTarget).data('menu');
1058 var menu_ids = [id];
1059 var menu = _.filter(this.data.data.children, function (menu) {return menu.id == id;})[0];
1060 function add_menu_ids (menu) {
1061 if (menu.children) {
1062 _.each(menu.children, function (menu) {
1063 menu_ids.push(menu.id);
1069 self.do_load_needaction(menu_ids).then(function () {
1070 self.trigger("need_action_reloaded");
1072 this.on_menu_click(ev);
1074 on_menu_click: function(ev) {
1075 ev.preventDefault();
1076 var needaction = $(ev.target).is('div.oe_menu_counter');
1077 this.menu_click($(ev.currentTarget).data('menu'), needaction);
1081 instance.web.UserMenu = instance.web.Widget.extend({
1082 template: "UserMenu",
1083 init: function(parent) {
1084 this._super(parent);
1085 this.update_promise = $.Deferred().resolve();
1089 this._super.apply(this, arguments);
1090 this.$el.on('click', '.oe_dropdown_menu li a[data-menu]', function(ev) {
1091 ev.preventDefault();
1092 var f = self['on_menu_' + $(this).data('menu')];
1098 do_update: function () {
1100 var fct = function() {
1101 var $avatar = self.$el.find('.oe_topbar_avatar');
1102 $avatar.attr('src', $avatar.data('default-src'));
1103 if (!self.session.uid)
1105 var func = new instance.web.Model("res.users").get_func("read");
1106 return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
1107 var topbar_name = res.name;
1108 if(instance.session.debug)
1109 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1110 if(res.company_id[0] > 1)
1111 topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
1112 self.$el.find('.oe_topbar_name').text(topbar_name);
1113 if (!instance.session.debug) {
1114 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1116 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
1117 $avatar.attr('src', avatar_src);
1120 this.update_promise = this.update_promise.then(fct, fct);
1122 on_menu_help: function() {
1123 window.open('http://help.openerp.com', '_blank');
1125 on_menu_logout: function() {
1126 this.trigger('user_logout');
1128 on_menu_settings: function() {
1130 if (!this.getParent().has_uncommitted_changes()) {
1131 self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
1132 result.res_id = instance.session.uid;
1133 self.getParent().action_manager.do_action(result);
1137 on_menu_about: function() {
1139 self.rpc("/web/webclient/version_info", {}).done(function(res) {
1140 var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1141 $help.find('a.oe_activate_debug_mode').click(function (e) {
1143 window.location = $.param.querystring( window.location.href, 'debug');
1145 instance.web.dialog($help, {autoOpen: true,
1146 modal: true, width: 507, height: 290, resizable: false, title: _t("About")});
1151 instance.web.Client = instance.web.Widget.extend({
1152 init: function(parent, origin) {
1153 instance.client = instance.webclient = this;
1154 this._super(parent);
1155 this.origin = origin;
1159 return instance.session.session_bind(this.origin).then(function() {
1160 var $e = $(QWeb.render(self._template, {widget: self}));
1161 self.replaceElement($e);
1164 return self.show_common();
1167 bind_events: function() {
1169 this.$el.on('mouseenter', '.oe_systray > div:not([data-tipsy=true])', function() {
1170 $(this).attr('data-tipsy', 'true').tipsy().trigger('mouseenter');
1172 this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1173 ev.preventDefault();
1174 var $toggle = $(this);
1175 var $menu = $toggle.siblings('.oe_dropdown_menu');
1176 $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1177 var state = $menu.is('.oe_opened');
1178 setTimeout(function() {
1179 // Do not alter propagation
1180 $toggle.add($menu).toggleClass('oe_opened', !state);
1182 // Move $menu if outside window's edge
1183 var doc_width = $(document).width();
1184 var offset = $menu.offset();
1185 var menu_width = $menu.width();
1186 var x = doc_width - offset.left - menu_width - 2;
1188 $menu.offset({ left: offset.left + x }).width(menu_width);
1193 instance.web.bus.on('click', this, function(ev) {
1195 if (!$(ev.target).is('input[type=file]')) {
1196 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1200 show_common: function() {
1202 this.crashmanager = new instance.web.CrashManager();
1203 instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1204 self.notification = new instance.web.Notification(this);
1205 self.notification.appendTo(self.$el);
1206 self.loading = new instance.web.Loading(self);
1207 self.loading.appendTo(self.$el);
1208 self.action_manager = new instance.web.ActionManager(self);
1209 self.action_manager.appendTo(self.$('.oe_application'));
1211 toggle_bars: function(value) {
1212 this.$('tr:has(td.oe_topbar),.oe_leftbar').toggle(value);
1214 has_uncommitted_changes: function() {
1219 instance.web.WebClient = instance.web.Client.extend({
1220 _template: 'WebClient',
1222 'click .oe_logo_edit_admin': 'logo_edit'
1224 init: function(parent) {
1225 this._super(parent);
1226 this._current_state = null;
1227 this.menu_dm = new instance.web.DropMisordered();
1228 this.action_mutex = new $.Mutex();
1232 return $.when(this._super()).then(function() {
1233 if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1234 $("body").addClass("kitten-mode-activated");
1235 $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1237 $.blockUI.defaults.message = '<img src="http://www.amigrave.com/kitten.gif">';
1240 if (!self.session.session_is_valid()) {
1243 self.show_application();
1247 set_title: function(title) {
1248 title = _.str.clean(title);
1249 var sep = _.isEmpty(title) ? '' : ' - ';
1250 document.title = title + sep + 'OpenERP';
1252 show_common: function() {
1255 window.onerror = function (message, file, line) {
1256 self.crashmanager.show_error({
1257 type: _t("Client Error"),
1259 data: {debug: file + ':' + line}
1263 show_login: function() {
1264 this.toggle_bars(false);
1266 var state = $.bbq.getState(true);
1268 type: 'ir.actions.client',
1273 this.action_manager.do_action(action);
1274 this.action_manager.inner_widget.on('login_successful', this, function() {
1275 this.show_application(); // will load the state we just pushed
1278 show_application: function() {
1280 self.toggle_bars(true);
1282 self.menu = new instance.web.Menu(self);
1283 self.menu.replace(this.$el.find('.oe_menu_placeholder'));
1284 self.menu.on('menu_click', this, this.on_menu_action);
1285 self.user_menu = new instance.web.UserMenu(self);
1286 self.user_menu.replace(this.$el.find('.oe_user_menu_placeholder'));
1287 self.user_menu.on('user_logout', self, self.on_logout);
1288 self.user_menu.do_update();
1289 self.bind_hashchange();
1291 self.check_timezone();
1293 update_logo: function() {
1294 var img = this.session.url('/web/binary/company_logo');
1295 this.$('.oe_logo img').attr('src', '').attr('src', img);
1296 this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1298 logo_edit: function(ev) {
1300 self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1301 self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1302 result.res_id = res['company_id'][0];
1303 result.target = "new";
1304 result.views = [[false, 'form']];
1306 action_buttons: true,
1308 self.action_manager.do_action(result);
1309 var form = self.action_manager.dialog_widget.views.form.controller;
1310 form.on("on_button_cancel", self.action_manager.dialog, self.action_manager.dialog.close);
1311 form.on('record_saved', self, function() {
1312 self.action_manager.dialog.close();
1319 check_timezone: function() {
1321 return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1322 var user_offset = result[0]['tz_offset'];
1323 var offset = -(new Date().getTimezoneOffset());
1324 // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1325 var browser_offset = (offset < 0) ? "-" : "+";
1326 browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1327 browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1328 if (browser_offset !== user_offset) {
1329 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1330 $icon.on('click', function() {
1331 var notification = self.do_warn(_t("Timezone mismatch"), QWeb.render('WebClient.timezone_notification', {
1332 user_timezone: instance.session.user_context.tz || 'UTC',
1333 user_offset: user_offset,
1334 browser_offset: browser_offset,
1336 notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1337 notification.close();
1338 }).find('a').on('click', function() {
1339 notification.close();
1340 self.user_menu.on_menu_settings();
1344 $icon.appendTo(self.$('.oe_systray'));
1348 destroy_content: function() {
1349 _.each(_.clone(this.getChildren()), function(el) {
1352 this.$el.children().remove();
1354 do_reload: function() {
1356 return this.session.session_reload().then(function () {
1357 instance.session.load_modules(true).then(
1358 self.menu.proxy('do_reload')); });
1361 do_notify: function() {
1362 var n = this.notification;
1363 return n.notify.apply(n, arguments);
1365 do_warn: function() {
1366 var n = this.notification;
1367 return n.warn.apply(n, arguments);
1369 on_logout: function() {
1371 if (!this.has_uncommitted_changes()) {
1372 this.session.session_logout().done(function () {
1373 $(window).unbind('hashchange', self.on_hashchange);
1374 self.do_push_state({});
1375 window.location.reload();
1379 bind_hashchange: function() {
1381 $(window).bind('hashchange', this.on_hashchange);
1383 var state = $.bbq.getState(true);
1384 if (_.isEmpty(state) || state.action == "login") {
1385 self.menu.has_been_loaded.done(function() {
1386 var first_menu_id = self.menu.$el.find("a:first").data("menu");
1388 self.menu.menu_click(first_menu_id);
1392 $(window).trigger('hashchange');
1395 on_hashchange: function(event) {
1397 var stringstate = event.getState(false);
1398 if (!_.isEqual(this._current_state, stringstate)) {
1399 var state = event.getState(true);
1400 if(!state.action && state.menu_id) {
1401 self.menu.has_been_loaded.done(function() {
1402 self.menu.do_reload().done(function() {
1403 self.menu.menu_click(state.menu_id);
1407 state._push_me = false; // no need to push state back...
1408 this.action_manager.do_load_state(state, !!this._current_state);
1411 this._current_state = stringstate;
1413 do_push_state: function(state) {
1414 this.set_title(state.title);
1416 var url = '#' + $.param(state);
1417 this._current_state = $.deparam($.param(state), false); // stringify all values
1418 $.bbq.pushState(url);
1419 this.trigger('state_pushed', state);
1421 on_menu_action: function(options) {
1423 return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1424 .then(function (result) {
1425 return self.action_mutex.exec(function() {
1426 if (options.needaction) {
1427 result.context = new instance.web.CompoundContext(result.context, {
1428 search_default_message_unread: true,
1429 search_disable_custom_filters: true,
1432 var completed = $.Deferred();
1433 $.when(self.action_manager.do_action(result, {
1434 clear_breadcrumbs: true,
1435 action_menu_id: self.menu.current_menu,
1436 })).fail(function() {
1437 self.menu.open_menu(options.previous_menu_id);
1438 }).always(function() {
1439 completed.resolve();
1441 setTimeout(function() {
1442 completed.resolve();
1444 // We block the menu when clicking on an element until the action has correctly finished
1445 // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1450 set_content_full_screen: function(fullscreen) {
1451 $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1452 this.$('.oe_webclient').toggleClass(
1453 'oe_content_full_screen', fullscreen);
1455 has_uncommitted_changes: function() {
1456 var $e = $.Event('clear_uncommitted_changes');
1457 instance.web.bus.trigger('clear_uncommitted_changes', $e);
1458 if ($e.isDefaultPrevented()) {
1461 return this._super.apply(this, arguments);
1466 instance.web.EmbeddedClient = instance.web.Client.extend({
1467 _template: 'EmbedClient',
1468 init: function(parent, origin, dbname, login, key, action_id, options) {
1469 this._super(parent, origin);
1470 this.bind_credentials(dbname, login, key);
1471 this.action_id = action_id;
1472 this.options = options || {};
1476 return $.when(this._super()).then(function() {
1477 return self.authenticate().then(function() {
1478 if (!self.action_id) {
1481 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1482 var action = result;
1483 action.flags = _.extend({
1484 //views_switcher : false,
1485 search_view : false,
1486 action_buttons : false,
1489 }, self.options, action.flags || {});
1491 self.do_action(action);
1497 do_action: function(/*...*/) {
1498 var am = this.action_manager;
1499 return am.do_action.apply(am, arguments);
1502 authenticate: function() {
1503 var s = instance.session;
1504 if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1507 return instance.session.session_authenticate(this.dbname, this.login, this.key, true);
1510 bind_credentials: function(dbname, login, key) {
1511 this.dbname = dbname;
1518 instance.web.embed = function (origin, dbname, login, key, action, options) {
1519 $('head').append($('<link>', {
1520 'rel': 'stylesheet',
1522 'href': origin +'/web/webclient/css'
1524 var currentScript = document.currentScript;
1525 if (!currentScript) {
1526 var sc = document.getElementsByTagName('script');
1527 currentScript = sc[sc.length-1];
1529 var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1530 client.insertAfter(currentScript);
1535 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: