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', {
46 * The very minimal function everything should call to create a dialog
47 * in OpenERP Web Client.
49 instance.web.dialog = function(element) {
50 var result = element.dialog.apply(element, _.rest(_.toArray(arguments)));
51 result.dialog("widget").openerpClass();
56 A useful class to handle dialogs.
59 - $buttons: A jQuery element targeting a dom part where buttons can be added. It always exists
60 during the lifecycle of the dialog.
62 instance.web.Dialog = instance.web.Widget.extend({
67 @param {Widget} parent
68 @param {dictionary} options A dictionary that will be forwarded to jQueryUI Dialog. Additionaly, that
69 dictionary can contain the following keys:
70 - buttons: Deprecated. The buttons key is not propagated to jQueryUI Dialog. It must be a dictionary (key = button
71 label, value = click handler) or a list of dictionaries (each element in the dictionary is send to the
72 corresponding method of a jQuery element targeting the <button> tag). It is deprecated because all dialogs
73 in OpenERP must be personalized in some way (button in red, link instead of button, ...) and this
74 feature does not allow that kind of personalization.
75 - destroy_on_close: Default true. If true and the dialog is closed, it is automatically destroyed.
76 @param {jQuery object} content Some content to replace this.$el .
78 init: function (parent, options, content) {
81 this.content_to_set = content;
82 this.dialog_options = {
84 destroy_on_close: true,
90 max_height: $(window.top).height() - 200,
92 position: [false, 40],
94 beforeClose: function () {
95 self.trigger("closing");
97 resizeStop: function() {
98 self.trigger("resized");
102 _.extend(this.dialog_options, options);
104 this.on("closing", this, this._closing);
105 this.$buttons = $('<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"><span class="oe_dialog_custom_buttons"/></div>');
107 _get_options: function() {
109 var o = _.extend({}, this.dialog_options);
111 width: $(window.top).width(),
112 height: $(window.top).height(),
114 _.each(sizes, function(available_size, unit) {
115 o[unit] = self._get_size(o[unit], available_size);
116 o['min_' + unit] = self._get_size(o['min_' + unit] || 0, available_size);
117 o['max_' + unit] = self._get_size(o['max_' + unit] || 0, available_size);
118 if (o[unit] !== 'auto' && o['min_' + unit] && o[unit] < o['min_' + unit]) {
119 o[unit] = o['min_' + unit];
121 if (o[unit] !== 'auto' && o['max_' + unit] && o[unit] > o['max_' + unit]) {
122 o[unit] = o['max_' + unit];
125 o.title = o.title || this.dialog_title;
128 _get_size: function(val, available_size) {
129 val = val.toString();
130 if (val === 'auto') {
132 } else if (val.slice(-1) === "%") {
133 return Math.round(available_size / 100 * parseInt(val.slice(0, -1), 10));
135 return parseInt(val, 10);
138 renderElement: function() {
139 if (this.content_to_set) {
140 this.setElement(this.content_to_set);
141 } else if (this.template) {
146 Opens the popup. Inits the dialog if it is not already inited.
151 if (!this.dialog_inited) {
154 this.$el.dialog('open');
155 this.$el.dialog("widget").append(this.$buttons);
158 _add_buttons: function(buttons) {
160 var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
161 _.each(buttons, function(fn, text) {
162 // buttons can be object or array
163 if (!_.isFunction(fn)) {
167 var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {} }}}));
168 $customButons.append($but);
169 $but.on('click', function(ev) {
170 fn.call(self.$el, ev);
175 Initializes the popup.
177 @return The result returned by start().
179 init_dialog: function() {
180 var options = this._get_options();
181 if (options.buttons) {
182 this._add_buttons(options.buttons);
183 delete(options.buttons);
185 this.renderElement();
186 instance.web.dialog(this.$el, options);
187 if (options.height === 'auto' && options.max_height) {
188 this.$el.css({ 'max-height': options.max_height, 'overflow-y': 'auto' });
190 this.dialog_inited = true;
191 var res = this.start();
195 Closes the popup, if destroy_on_close was passed to the constructor, it is also destroyed.
198 if (this.dialog_inited && this.$el.is(":data(dialog)")) {
199 this.$el.dialog('close');
202 _closing: function() {
203 if (this.__tmp_dialog_destroying)
205 if (this.dialog_options.destroy_on_close) {
206 this.__tmp_dialog_closing = true;
208 this.__tmp_dialog_closing = undefined;
212 Destroys the popup, also closes it.
214 destroy: function () {
215 this.$buttons.remove();
216 _.each(this.getChildren(), function(el) {
219 if (! this.__tmp_dialog_closing) {
220 this.__tmp_dialog_destroying = true;
222 this.__tmp_dialog_destroying = undefined;
224 if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(dialog)")) {
225 this.$el.dialog('destroy');
231 instance.web.CrashManager = instance.web.Class.extend({
236 rpc_error: function(error) {
240 if (error.data.fault_code) {
241 var split = ("" + error.data.fault_code).split('\n')[0].split(' -- ');
242 if (split.length > 1) {
243 error.type = split.shift();
244 error.data.fault_code = error.data.fault_code.substr(error.type.length + 4);
247 if (error.code === 200 && error.type) {
248 this.show_warning(error);
250 this.show_error(error);
253 show_warning: function(error) {
257 instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), {
258 title: "OpenERP " + _.str.capitalize(error.type),
260 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
264 show_error: function(error) {
269 buttons[_t("Ok")] = function() {
270 $(this).dialog("close");
272 var dialog = new instance.web.Dialog(this, {
273 title: "OpenERP " + _.str.capitalize(error.type),
280 dialog.$el.html(QWeb.render('CrashManager.error', {session: instance.session, error: error}));
282 show_message: function(exception) {
284 type: _t("Client Error"),
291 instance.web.Loading = instance.web.Widget.extend({
293 init: function(parent) {
296 this.blocked_ui = false;
297 this.session.on("request", this, this.request_call);
298 this.session.on("response", this, this.response_call);
299 this.session.on("response_failed", this, this.response_call);
301 destroy: function() {
302 this.on_rpc_event(-this.count);
305 request_call: function() {
306 this.on_rpc_event(1);
308 response_call: function() {
309 this.on_rpc_event(-1);
311 on_rpc_event : function(increment) {
313 if (!this.count && increment === 1) {
315 this.long_running_timer = setTimeout(function () {
316 self.blocked_ui = true;
317 instance.web.blockUI();
321 this.count += increment;
322 if (this.count > 0) {
323 if (instance.session.debug) {
324 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
326 this.$el.text(_t("Loading"));
329 this.getParent().$el.addClass('oe_wait');
332 clearTimeout(this.long_running_timer);
333 // Don't unblock if blocked by somebody else
334 if (self.blocked_ui) {
335 this.blocked_ui = false;
336 instance.web.unblockUI();
339 this.getParent().$el.removeClass('oe_wait');
344 instance.web.DatabaseManager = instance.web.Widget.extend({
345 init: function(parent) {
347 this.unblockUIFunction = instance.web.unblockUI;
348 $.validator.addMethod('matches', function (s, _, re) {
349 return new RegExp(re).test(s);
350 }, _t("Invalid database name"));
354 $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
355 var fetch_db = this.rpc("/web/database/get_list", {}).then(
357 self.db_list = result;
363 var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
364 self.lang_list = result;
366 return $.when(fetch_db, fetch_langs).done(self.do_render);
368 do_render: function() {
370 instance.webclient.toggle_bars(true);
371 self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
372 $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
373 $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
374 $('ul.oe_secondary_submenu > li:first').addClass('oe_active')
375 $('ul.oe_secondary_submenu > li').bind('click', function (event) {
376 var menuitem = $(this);
377 menuitem.addClass('oe_active').siblings().removeClass('oe_active');
378 var form_id =menuitem.find('a').attr('href');
379 $(form_id).show().siblings().hide();
380 event.preventDefault();
382 $('#back-to-login').click(self.do_exit);
383 self.$el.find("td").addClass("oe_form_group_cell");
384 self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
385 self.$el.find("label").addClass("oe_form_label");
386 self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
387 self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
388 self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
389 self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
390 self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
391 self.$el.find("form[name=change_pwd_form]").validate({
393 old_pwd: "Please enter your previous password",
394 new_pwd: "Please enter your new password",
396 required: "Please confirm your new password",
397 equalTo: "The confirmation does not match the password"
400 submitHandler: self.do_change_password
403 destroy: function () {
404 this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
408 * Converts a .serializeArray() result into a dict. Does not bother folding
409 * multiple identical keys into an array, last key wins.
411 * @param {Array} array
413 to_object: function (array) {
415 _(array).each(function (record) {
416 result[record.name] = record.value;
421 * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
422 * from unblocking the UI
424 blockUI: function () {
425 instance.web.blockUI();
426 instance.web.unblockUI = function () {};
429 * Reinstates $.unblockUI so third parties can play with blockUI, and
432 unblockUI: function () {
433 instance.web.unblockUI = this.unblockUIFunction;
434 instance.web.unblockUI();
437 * Displays an error dialog resulting from the various RPC communications
438 * failing over themselves
440 * @param {Object} error error description
441 * @param {String} error.title title of the error dialog
442 * @param {String} error.error message of the error dialog
444 display_error: function (error) {
445 return instance.web.dialog($('<div>'), {
449 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
451 }).html(error.error);
453 do_create: function(form) {
455 var fields = $(form).serializeArray();
456 self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
457 var form_obj = self.to_object(fields);
458 var client_action = {
459 type: 'ir.actions.client',
462 'db': form_obj['db_name'],
464 'password': form_obj['create_admin_pwd'],
465 'login_successful': function() {
466 self.do_action("reload");
470 self.do_action(client_action);
473 do_duplicate: function(form) {
475 var fields = $(form).serializeArray();
476 self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
478 self.display_error(result);
481 self.do_notify("Duplicating database", "The database has been duplicated.");
485 do_drop: function(form) {
488 fields = $form.serializeArray(),
489 $db_list = $form.find('[name=drop_db]'),
491 if (!db || !confirm("Do you really want to delete the database: " + db + " ?")) {
494 self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
496 self.display_error(result);
499 self.do_notify("Dropping database", "The database '" + db + "' has been dropped");
503 do_backup: function(form) {
506 self.session.get_file({
508 success: function () {
509 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
511 error: function(error){
514 title: 'Backup Database',
515 error: 'AccessDenied'
519 complete: function() {
524 do_restore: function(form) {
528 url: '/web/database/restore',
531 success: function (body) {
532 // If empty body, everything went fine
533 if (!body) { return; }
535 if (body.indexOf('403 Forbidden') !== -1) {
537 title: 'Access Denied',
538 error: 'Incorrect super-administrator password'
542 title: 'Restore Database',
543 error: 'Could not restore the database'
547 complete: function() {
549 self.do_notify(_t("Restored"), _t("Database restored successfully"));
553 do_change_password: function(form) {
555 self.rpc("/web/database/change_password", {
556 'fields': $(form).serializeArray()
557 }).done(function(result) {
559 self.display_error(result);
563 self.do_notify("Changed Password", "Password has been changed successfully");
566 do_exit: function () {
568 instance.webclient.toggle_bars(false);
569 this.do_action('login');
572 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
574 instance.web.Login = instance.web.Widget.extend({
576 remember_credentials: true,
578 init: function(parent, action) {
580 this.has_local_storage = typeof(localStorage) != 'undefined';
582 this.selected_db = null;
583 this.selected_login = null;
584 this.params = action.params || {};
585 if (_.isEmpty(this.params)) {
586 this.params = $.bbq.getState(true);
589 if (this.params.login_successful) {
590 this.on('login_successful', this, this.params.login_successful);
593 if (this.has_local_storage && this.remember_credentials) {
594 this.selected_db = localStorage.getItem('last_db_login_success');
595 this.selected_login = localStorage.getItem('last_login_login_success');
596 if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) {
597 this.selected_password = localStorage.getItem('last_password_login_success');
603 self.$el.find("form").submit(self.on_submit);
604 self.$el.find('.oe_login_manage_db').click(function() {
605 self.do_action("database_manager");
608 if ($.deparam.querystring().db) {
609 self.params.db = $.deparam.querystring().db;
611 // used by dbmanager.do_create via internal client action
612 if (self.params.db && self.params.login && self.params.password) {
613 d = self.do_login(self.params.db, self.params.login, self.params.password);
615 if (self.params.db) {
616 self.on_db_loaded([self.params.db])
618 d = self.rpc("/web/database/get_list", {}).done(self.on_db_loaded).fail(self.on_db_failed);
623 on_db_loaded: function (result) {
624 this.db_list = result;
625 this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db}));
626 if(this.db_list.length === 0) {
627 this.do_action("database_manager");
628 } else if(this.db_list.length === 1) {
629 this.$('div.oe_login_dbpane').hide();
631 this.$('div.oe_login_dbpane').show();
634 on_db_failed: function (error, event) {
635 if (error.data.fault_code === 'AccessDenied') {
636 event.preventDefault();
639 on_submit: function(ev) {
643 var db = this.$("form [name=db]").val();
645 this.do_warn("Login", "No database selected !");
648 var login = this.$("form input[name=login]").val();
649 var password = this.$("form input[name=password]").val();
651 this.do_login(db, login, password);
654 * Performs actual login operation, and UI-related stuff
656 * @param {String} db database to log in
657 * @param {String} login user login
658 * @param {String} password user password
660 do_login: function (db, login, password) {
663 self.$(".oe_login_pane").fadeOut("slow");
664 return this.session.session_authenticate(db, login, password).then(function() {
665 if (self.has_local_storage) {
666 if(self.remember_credentials) {
667 localStorage.setItem('last_db_login_success', db);
668 localStorage.setItem('last_login_login_success', login);
669 if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) {
670 localStorage.setItem('last_password_login_success', password);
673 localStorage.setItem('last_db_login_success', '');
674 localStorage.setItem('last_login_login_success', '');
675 localStorage.setItem('last_password_login_success', '');
678 self.trigger('login_successful');
680 self.$(".oe_login_pane").fadeIn("fast", function() {
681 self.show_error("Invalid username or password");
685 show_error: function(message) {
686 this.$el.addClass("oe_login_invalid");
687 this.$(".oe_login_error_message").text(message);
689 hide_error: function() {
690 this.$el.removeClass('oe_login_invalid');
693 instance.web.client_actions.add("login", "instance.web.Login");
696 * Redirect to url by replacing window.location
697 * If wait is true, sleep 1s and wait for the server i.e. after a restart.
699 instance.web.redirect = function(url, wait) {
700 // Dont display a dialog if some xmlhttprequest are in progress
701 if (instance.client && instance.client.crashmanager) {
702 instance.client.crashmanager.active = false;
705 var wait_server = function() {
706 instance.session.rpc("/web/webclient/version_info", {}).done(function() {
707 window.location = url;
709 setTimeout(wait_server, 250);
714 setTimeout(wait_server, 1000);
716 window.location = url;
721 * Client action to reload the whole interface.
722 * If params.menu_id, it opens the given menu entry.
723 * If params.wait, reload will wait the openerp server to be reachable before reloading
725 instance.web.Reload = function(parent, action) {
726 var params = action.params || {};
727 var menu_id = params.menu_id || false;
728 var l = window.location;
730 var sobj = $.deparam(l.search.substr(1));
731 sobj.ts = new Date().getTime();
732 var search = '?' + $.param(sobj);
736 hash = "#menu_id=" + menu_id;
738 var url = l.protocol + "//" + l.host + l.pathname + search + hash;
740 instance.web.redirect(url, params.wait);
742 instance.web.client_actions.add("reload", "instance.web.Reload");
745 * Client action to go back in breadcrumb history.
746 * If can't go back in history stack, will go back to home.
748 instance.web.HistoryBack = function(parent) {
749 if (!parent.history_back()) {
750 instance.web.Home(parent);
753 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
756 * Client action to go back home.
758 instance.web.Home = function(parent, action) {
759 var url = '/' + (window.location.search || '');
760 instance.web.redirect(url, action.params && action.params.wait);
762 instance.web.client_actions.add("home", "instance.web.Home");
764 instance.web.ChangePassword = instance.web.Widget.extend({
765 template: "ChangePassword",
768 this.getParent().dialog_title = "Change Password";
769 var $button = self.$el.find('.oe_form_button');
770 $button.appendTo(this.getParent().$buttons);
771 $button.eq(2).click(function(){
772 self.getParent().close();
774 $button.eq(0).click(function(){
775 self.rpc("/web/session/change_password",{
776 'fields': $("form[name=change_password_form]").serializeArray()
777 }).done(function(result) {
779 self.display_error(result);
782 instance.webclient.on_logout();
787 display_error: function (error) {
788 return instance.web.dialog($('<div>'), {
792 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
794 }).html(error.error);
797 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
799 instance.web.Menu = instance.web.Widget.extend({
802 this._super.apply(this, arguments);
803 this.has_been_loaded = $.Deferred();
804 this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
805 this.data = {data:{children:[]}};
808 this._super.apply(this, arguments);
809 this.$secondary_menus = this.getParent().$el.find('.oe_secondary_menus_container');
810 this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
811 return this.do_reload();
813 do_reload: function() {
815 return this.rpc("/web/menu/load", {}).done(function(r) {
819 menu_loaded: function(data) {
822 this.renderElement();
823 this.limit_entries();
824 // Hide toplevel item if there is only one
825 var $toplevel = this.$("li")
826 if($toplevel.length == 1) {
829 this.$secondary_menus.html(QWeb.render("Menu.secondary", { widget : this }));
830 this.$el.on('click', 'a[data-menu]', this.on_menu_click);
831 // Hide second level submenus
832 this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
833 if (self.current_menu) {
834 self.open_menu(self.current_menu);
836 this.trigger('menu_loaded', data);
837 this.has_been_loaded.resolve();
838 // Now launch the fetch of needaction counters, asynchronous
839 this.rpc("web/menu/load_needaction", {menu_ids: false}).done(function(r) {
840 self.on_needaction_loaded(r);
843 on_needaction_loaded: function(data) {
845 this.needaction_data = data;
846 _.each(this.needaction_data.data, function (item, menu_id) {
847 var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
848 $item.remove('oe_menu_counter');
849 if (item.needaction_counter && item.needaction_counter > 0) {
850 $item.append('<div class="oe_tag oe_tag_dark oe_menu_counter">' + item.needaction_counter + '</div>');
854 limit_entries: function() {
855 var maximum_visible_links = this.maximum_visible_links;
856 if (maximum_visible_links === 'auto') {
857 maximum_visible_links = this.auto_limit_entries();
859 if (maximum_visible_links < this.data.data.children.length) {
860 var $more = $(QWeb.render('Menu.more')),
861 $index = this.$el.find('li').eq(maximum_visible_links - 1);
863 //$('.oe_topbar').append($more);
864 $more.find('.oe_menu_more').append($index.next().nextAll());
867 auto_limit_entries: function() {
868 // TODO: auto detect overflow and bind window on resize
869 var width = $(window).width();
870 return Math.floor(width / 125);
873 * Opens a given menu by id, as if a user had browsed to that menu by hand
874 * except does not trigger any event on the way
876 * @param {Number} id database id of the terminal menu to select
878 open_menu: function (id) {
879 this.current_menu = id;
880 this.session.active_id = id;
881 var $clicked_menu, $sub_menu, $main_menu;
882 $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
883 this.trigger('open_menu', id, $clicked_menu);
885 if (this.$secondary_menus.has($clicked_menu).length) {
886 $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
887 $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
889 $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
890 $main_menu = $clicked_menu;
893 // Activate current main menu
894 this.$el.find('.oe_active').removeClass('oe_active');
895 $main_menu.addClass('oe_active');
897 // Show current sub menu
898 this.$secondary_menus.find('.oe_secondary_menu').hide();
901 // Hide/Show the leftbar menu depending of the presence of sub-items
902 this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
904 // Activate current menu item and show parents
905 this.$secondary_menus.find('.oe_active').removeClass('oe_active');
906 if ($main_menu !== $clicked_menu) {
907 $clicked_menu.parents().show();
908 if ($clicked_menu.is('.oe_menu_toggler')) {
909 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
911 $clicked_menu.parent().addClass('oe_active');
916 * Call open_menu with the first menu_item matching an action_id
918 * @param {Number} id the action_id to match
920 open_action: function (id) {
921 var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
922 var menu_id = $menu.data('menu');
924 this.open_menu(menu_id);
928 * Process a click on a menu item
930 * @param {Number} id the menu_id
931 * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
933 menu_click: function(id, needaction) {
936 // find back the menuitem in dom to get the action
937 var $item = this.$el.find('a[data-menu=' + id + ']');
939 $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
941 var action_id = $item.data('action-id');
942 // If first level menu doesnt have action trigger first leaf
944 if(this.$el.has($item).length) {
945 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
946 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
948 action_id = $items.data('action-id');
949 id = $items.data('menu');
954 this.trigger('menu_click', {
955 action_id: action_id,
956 needaction: needaction,
958 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
964 * Jquery event handler for menu click
966 * @param {Event} ev the jquery event
968 on_menu_click: function(ev) {
970 var needaction = $(ev.target).is('div.oe_menu_counter');
971 this.menu_click($(ev.currentTarget).data('menu'), needaction);
975 instance.web.UserMenu = instance.web.Widget.extend({
976 template: "UserMenu",
977 init: function(parent) {
979 this.update_promise = $.Deferred().resolve();
983 this._super.apply(this, arguments);
984 this.$el.on('click', '.oe_dropdown_menu li a[data-menu]', function(ev) {
986 var f = self['on_menu_' + $(this).data('menu')];
992 do_update: function () {
994 var fct = function() {
995 var $avatar = self.$el.find('.oe_topbar_avatar');
996 $avatar.attr('src', $avatar.data('default-src'));
997 if (!self.session.uid)
999 var func = new instance.web.Model("res.users").get_func("read");
1000 return func(self.session.uid, ["name", "company_id"]).then(function(res) {
1001 var topbar_name = res.name;
1002 if(instance.session.debug)
1003 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1004 if(res.company_id[0] > 1)
1005 topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
1006 self.$el.find('.oe_topbar_name').text(topbar_name);
1007 if(!instance.session.debug) {
1008 self.rpc("/web/database/get_list", {}).done( function(result) {
1009 if (result.length > 1) {
1010 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1012 self.$el.find('.oe_topbar_name').text(topbar_name);
1015 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
1016 $avatar.attr('src', avatar_src);
1019 this.update_promise = this.update_promise.then(fct, fct);
1021 on_menu_logout: function() {
1022 this.trigger('user_logout');
1024 on_menu_settings: function() {
1026 if (!this.getParent().has_uncommitted_changes()) {
1027 self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
1028 result.res_id = instance.session.uid;
1029 self.getParent().action_manager.do_action(result);
1033 on_menu_about: function() {
1035 self.rpc("/web/webclient/version_info", {}).done(function(res) {
1036 var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1037 $help.find('a.oe_activate_debug_mode').click(function (e) {
1039 window.location = $.param.querystring( window.location.href, 'debug');
1041 instance.web.dialog($help, {autoOpen: true,
1042 modal: true, width: 507, height: 290, resizable: false, title: _t("About")});
1047 instance.web.Client = instance.web.Widget.extend({
1048 init: function(parent, origin) {
1049 instance.client = instance.webclient = this;
1050 this._super(parent);
1051 this.origin = origin;
1055 return instance.session.session_bind(this.origin).then(function() {
1056 var $e = $(QWeb.render(self._template, {widget: self}));
1057 self.replaceElement($e);
1060 return self.show_common();
1063 bind_events: function() {
1065 this.$el.on('mouseenter', '.oe_systray > div:not([data-tipsy=true])', function() {
1066 $(this).attr('data-tipsy', 'true').tipsy().trigger('mouseenter');
1068 this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1069 ev.preventDefault();
1070 var $toggle = $(this);
1071 var $menu = $toggle.siblings('.oe_dropdown_menu');
1072 $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1073 var state = $menu.is('.oe_opened');
1074 setTimeout(function() {
1075 // Do not alter propagation
1076 $toggle.add($menu).toggleClass('oe_opened', !state);
1078 // Move $menu if outside window's edge
1079 var doc_width = $(document).width();
1080 var offset = $menu.offset();
1081 var menu_width = $menu.width();
1082 var x = doc_width - offset.left - menu_width - 2;
1084 $menu.offset({ left: offset.left + x }).width(menu_width);
1089 instance.web.bus.on('click', this, function(ev) {
1091 if (!$(ev.target).is('input[type=file]')) {
1092 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1096 show_common: function() {
1098 this.crashmanager = new instance.web.CrashManager();
1099 instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1100 self.notification = new instance.web.Notification(this);
1101 self.notification.appendTo(self.$el);
1102 self.loading = new instance.web.Loading(self);
1103 self.loading.appendTo(self.$el);
1104 self.action_manager = new instance.web.ActionManager(self);
1105 self.action_manager.appendTo(self.$('.oe_application'));
1107 toggle_bars: function(value) {
1108 this.$('tr:has(td.oe_topbar),.oe_leftbar').toggle(value);
1110 has_uncommitted_changes: function() {
1115 instance.web.WebClient = instance.web.Client.extend({
1116 _template: 'WebClient',
1117 init: function(parent) {
1118 this._super(parent);
1119 this._current_state = null;
1123 return $.when(this._super()).then(function() {
1124 self.$(".oe_logo").attr("href", $.param.fragment("" + window.location, "", 2).slice(0, -1));
1125 if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1126 $("body").addClass("kitten-mode-activated");
1128 $.blockUI.defaults.message = '<img src="http://www.amigrave.com/kitten.gif">';
1131 if (!self.session.session_is_valid()) {
1134 self.show_application();
1138 set_title: function(title) {
1139 title = _.str.clean(title);
1140 var sep = _.isEmpty(title) ? '' : ' - ';
1141 document.title = title + sep + 'OpenERP';
1143 show_common: function() {
1146 window.onerror = function (message, file, line) {
1147 self.crashmanager.show_error({
1148 type: _t("Client Error"),
1150 data: {debug: file + ':' + line}
1154 show_login: function() {
1155 this.toggle_bars(false);
1157 var state = $.bbq.getState(true);
1159 type: 'ir.actions.client',
1164 this.action_manager.do_action(action);
1165 this.action_manager.inner_widget.on('login_successful', this, function() {
1166 this.show_application(); // will load the state we just pushed
1169 show_application: function() {
1171 self.toggle_bars(true);
1172 self.menu = new instance.web.Menu(self);
1173 self.menu.replace(this.$el.find('.oe_menu_placeholder'));
1174 self.menu.on('menu_click', this, this.on_menu_action);
1175 self.user_menu = new instance.web.UserMenu(self);
1176 self.user_menu.replace(this.$el.find('.oe_user_menu_placeholder'));
1177 self.user_menu.on('user_logout', self, self.on_logout);
1178 self.user_menu.do_update();
1179 self.bind_hashchange();
1181 self.check_timezone();
1183 check_timezone: function() {
1185 var user_offset = instance.session.user_context.tz_offset;
1186 var offset = -(new Date().getTimezoneOffset());
1187 // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1188 var browser_offset = (offset < 0) ? "-" : "+";
1189 browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1190 browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1191 if (browser_offset !== user_offset) {
1192 var notification = this.do_warn(_t("Timezone"), QWeb.render('WebClient.timezone_notification', {
1193 user_timezone: instance.session.user_context.tz || 'UTC',
1194 user_offset: user_offset,
1195 browser_offset: browser_offset,
1197 notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1198 notification.close();
1199 }).find('a').on('click', function() {
1200 notification.close();
1201 self.user_menu.on_menu_settings();
1206 destroy_content: function() {
1207 _.each(_.clone(this.getChildren()), function(el) {
1210 this.$el.children().remove();
1212 do_reload: function() {
1214 return this.session.session_reload().then(function () {
1215 instance.session.load_modules(true).then(
1216 self.menu.proxy('do_reload')); });
1219 do_notify: function() {
1220 var n = this.notification;
1221 return n.notify.apply(n, arguments);
1223 do_warn: function() {
1224 var n = this.notification;
1225 return n.warn.apply(n, arguments);
1227 on_logout: function() {
1229 if (!this.has_uncommitted_changes()) {
1230 this.session.session_logout().done(function () {
1231 $(window).unbind('hashchange', self.on_hashchange);
1232 self.do_push_state({});
1233 window.location.reload();
1237 bind_hashchange: function() {
1239 $(window).bind('hashchange', this.on_hashchange);
1241 var state = $.bbq.getState(true);
1242 if (_.isEmpty(state) || state.action == "login") {
1243 self.menu.has_been_loaded.done(function() {
1244 var first_menu_id = self.menu.$el.find("a:first").data("menu");
1246 self.menu.menu_click(first_menu_id);
1250 $(window).trigger('hashchange');
1253 on_hashchange: function(event) {
1255 var state = event.getState(true);
1256 if (!_.isEqual(this._current_state, state)) {
1257 if(!state.action && state.menu_id) {
1258 self.menu.has_been_loaded.done(function() {
1259 self.menu.do_reload().done(function() {
1260 self.menu.menu_click(state.menu_id);
1264 state._push_me = false; // no need to push state back...
1265 this.action_manager.do_load_state(state, !!this._current_state);
1268 this._current_state = state;
1270 do_push_state: function(state) {
1271 this.set_title(state.title);
1273 var url = '#' + $.param(state);
1274 this._current_state = _.clone(state);
1275 $.bbq.pushState(url);
1276 this.trigger('state_pushed', state);
1278 on_menu_action: function(options) {
1280 return this.rpc("/web/action/load", { action_id: options.action_id })
1281 .then(function (result) {
1282 if (options.needaction) {
1283 result.context = new instance.web.CompoundContext(
1285 {search_default_message_unread: true});
1287 return $.when(self.action_manager.do_action(result, {
1288 clear_breadcrumbs: true,
1289 action_menu_id: self.menu.current_menu,
1290 })).fail(function() {
1291 self.menu.open_menu(options.previous_menu_id);
1295 set_content_full_screen: function(fullscreen) {
1297 $(".oe_webclient", this.$el).addClass("oe_content_full_screen");
1298 $("body").css({'overflow-y':'hidden'});
1300 $(".oe_webclient", this.$el).removeClass("oe_content_full_screen");
1301 $("body").css({'overflow-y':'scroll'});
1304 has_uncommitted_changes: function() {
1305 var $e = $.Event('clear_uncommitted_changes');
1306 instance.web.bus.trigger('clear_uncommitted_changes', $e);
1307 if ($e.isDefaultPrevented()) {
1310 return this._super.apply(this, arguments);
1315 instance.web.EmbeddedClient = instance.web.Client.extend({
1316 _template: 'EmbedClient',
1317 init: function(parent, origin, dbname, login, key, action_id, options) {
1318 this._super(parent, origin);
1320 this.dbname = dbname;
1323 this.action_id = action_id;
1324 this.options = options || {};
1328 return $.when(this._super()).then(function() {
1329 return instance.session.session_authenticate(self.dbname, self.login, self.key, true).then(function() {
1330 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1331 var action = result;
1332 action.flags = _.extend({
1333 //views_switcher : false,
1334 search_view : false,
1335 action_buttons : false,
1338 }, self.options, action.flags || {});
1340 self.action_manager.do_action(action);
1347 instance.web.embed = function (origin, dbname, login, key, action, options) {
1348 $('head').append($('<link>', {
1349 'rel': 'stylesheet',
1351 'href': origin +'/web/webclient/css'
1353 var currentScript = document.currentScript;
1354 if (!currentScript) {
1355 var sc = document.getElementsByTagName('script');
1356 currentScript = sc[sc.length-1];
1358 var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1359 client.insertAfter(currentScript);
1364 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: