1 /*---------------------------------------------------------
3 *---------------------------------------------------------*/
6 var instance = openerp;
7 openerp.web.chrome = {};
9 var QWeb = instance.web.qweb,
12 instance.web.Notification = instance.web.Widget.extend({
13 template: 'Notification',
15 this._super.apply(this, arguments);
16 instance.web.notification = this;
19 this._super.apply(this, arguments);
25 notify: function(title, text, sticky) {
31 return this.$el.notify('create', {
36 warn: function(title, text, sticky) {
42 return this.$el.notify('create', 'oe_notification_alert', {
49 instance.web.action_notify = function(element, action) {
50 element.do_notify(action.params.title, action.params.text, action.params.sticky);
52 instance.web.client_actions.add("action_notify", "instance.web.action_notify");
54 instance.web.action_warn = function(element, action) {
55 element.do_warn(action.params.title, action.params.text, action.params.sticky);
57 instance.web.client_actions.add("action_warn", "instance.web.action_warn");
60 * The very minimal function everything should call to create a dialog
61 * in OpenERP Web Client.
63 instance.web.dialog = function(element) {
64 var result = element.dialog.apply(element, _.rest(_.toArray(arguments)));
65 result.dialog("widget").openerpClass();
70 A useful class to handle dialogs.
73 - $buttons: A jQuery element targeting a dom part where buttons can be added. It always exists
74 during the lifecycle of the dialog.
76 instance.web.Dialog = instance.web.Widget.extend({
81 @param {Widget} parent
82 @param {dictionary} options A dictionary that will be forwarded to jQueryUI Dialog. Additionaly, that
83 dictionary can contain the following keys:
84 - buttons: Deprecated. The buttons key is not propagated to jQueryUI Dialog. It must be a dictionary (key = button
85 label, value = click handler) or a list of dictionaries (each element in the dictionary is send to the
86 corresponding method of a jQuery element targeting the <button> tag). It is deprecated because all dialogs
87 in OpenERP must be personalized in some way (button in red, link instead of button, ...) and this
88 feature does not allow that kind of personalization.
89 - destroy_on_close: Default true. If true and the dialog is closed, it is automatically destroyed.
90 @param {jQuery object} content Some content to replace this.$el .
92 init: function (parent, options, content) {
95 this.content_to_set = content;
96 this.dialog_options = {
98 destroy_on_close: true,
104 max_height: $(window.top).height() - 200,
106 position: [false, 40],
108 beforeClose: function () {
109 self.trigger("closing");
111 resizeStop: function() {
112 self.trigger("resized");
116 _.extend(this.dialog_options, options);
118 this.on("closing", this, this._closing);
119 this.$buttons = $('<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"><span class="oe_dialog_custom_buttons"/></div>');
121 _get_options: function() {
123 var o = _.extend({}, this.dialog_options);
125 width: $(window.top).width(),
126 height: $(window.top).height(),
128 _.each(sizes, function(available_size, unit) {
129 o[unit] = self._get_size(o[unit], available_size);
130 o['min_' + unit] = self._get_size(o['min_' + unit] || 0, available_size);
131 o['max_' + unit] = self._get_size(o['max_' + unit] || 0, available_size);
132 if (o[unit] !== 'auto' && o['min_' + unit] && o[unit] < o['min_' + unit]) {
133 o[unit] = o['min_' + unit];
135 if (o[unit] !== 'auto' && o['max_' + unit] && o[unit] > o['max_' + unit]) {
136 o[unit] = o['max_' + unit];
139 o.title = o.title || this.dialog_title;
142 _get_size: function(val, available_size) {
143 val = val.toString();
144 if (val === 'auto') {
146 } else if (val.slice(-1) === "%") {
147 return Math.round(available_size / 100 * parseInt(val.slice(0, -1), 10));
149 return parseInt(val, 10);
152 renderElement: function() {
153 if (this.content_to_set) {
154 this.setElement(this.content_to_set);
155 } else if (this.template) {
160 Opens the popup. Inits the dialog if it is not already inited.
165 if (!this.dialog_inited) {
168 this.$el.dialog('open');
169 this.$el.dialog("widget").append(this.$buttons);
172 _add_buttons: function(buttons) {
174 var $customButons = this.$buttons.find('.oe_dialog_custom_buttons').empty();
175 _.each(buttons, function(fn, text) {
176 // buttons can be object or array
177 if (!_.isFunction(fn)) {
181 var $but = $(QWeb.render('WidgetButton', { widget : { string: text, node: { attrs: {} }}}));
182 $customButons.append($but);
183 $but.on('click', function(ev) {
184 fn.call(self.$el, ev);
189 Initializes the popup.
191 @return The result returned by start().
193 init_dialog: function() {
194 var options = this._get_options();
195 if (options.buttons) {
196 this._add_buttons(options.buttons);
197 delete(options.buttons);
199 this.renderElement();
200 instance.web.dialog(this.$el, options);
201 if (options.height === 'auto' && options.max_height) {
202 this.$el.css({ 'max-height': options.max_height, 'overflow-y': 'auto' });
204 this.dialog_inited = true;
205 var res = this.start();
209 Closes the popup, if destroy_on_close was passed to the constructor, it is also destroyed.
212 if (this.dialog_inited && this.$el.is(":data(dialog)")) {
213 this.$el.dialog('close');
216 _closing: function() {
217 if (this.__tmp_dialog_destroying)
219 if (this.dialog_options.destroy_on_close) {
220 this.__tmp_dialog_closing = true;
222 this.__tmp_dialog_closing = undefined;
226 Destroys the popup, also closes it.
228 destroy: function () {
229 this.$buttons.remove();
230 _.each(this.getChildren(), function(el) {
233 if (! this.__tmp_dialog_closing) {
234 this.__tmp_dialog_destroying = true;
236 this.__tmp_dialog_destroying = undefined;
238 if (this.dialog_inited && !this.isDestroyed() && this.$el.is(":data(dialog)")) {
239 this.$el.dialog('destroy');
245 instance.web.CrashManager = instance.web.Class.extend({
250 rpc_error: function(error) {
254 var handler = instance.web.crash_manager_registry.get_object(error.data.name, true);
256 new (handler)(this, error).display();
259 if (error.data.name === "openerp.addons.web.session SessionExpiredException") {
260 this.show_warning({type: "Session Expired", data: { message: _t("Your OpenERP session expired. Please refresh the current web page.") }});
263 if (error.data.exception_type === "except_osv" || error.data.exception_type === "warning"
264 || error.data.exception_type === "access_error") {
265 this.show_warning(error);
267 this.show_error(error);
270 show_warning: function(error) {
274 if (error.data.exception_type === "except_osv") {
275 error = _.extend({}, error, {data: _.extend({}, error.data, {message: error.data.arguments[0] + "\n\n" + error.data.arguments[1]})});
277 instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), {
278 title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
280 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
284 show_error: function(error) {
289 buttons[_t("Ok")] = function() {
290 $(this).dialog("close");
292 var dialog = new instance.web.Dialog(this, {
293 title: "OpenERP " + _.str.capitalize(error.type),
300 dialog.$el.html(QWeb.render('CrashManager.error', {session: instance.session, error: error}));
302 show_message: function(exception) {
304 type: _t("Client Error"),
312 An interface to implement to handle exceptions. Register implementation in instance.web.crash_manager_registry.
314 instance.web.ExceptionHandler = {
316 @param parent The parent.
317 @param error The error object as returned by the JSON-RPC implementation.
319 init: function(parent, error) {},
321 Called to inform to display the widget, if necessary. A typical way would be to implement
322 this interface in a class extending instance.web.Dialog and simply display the dialog in this
325 display: function() {},
329 The registry to handle exceptions. It associate a fully qualified python exception name with a class implementing
330 instance.web.ExceptionHandler.
332 instance.web.crash_manager_registry = new instance.web.Registry();
335 * Handle redirection warnings, which behave more or less like a regular
336 * warning, with an additional redirection button.
338 instance.web.RedirectWarningHandler = instance.web.Dialog.extend(instance.web.ExceptionHandler, {
339 init: function(parent, error) {
343 display: function() {
345 error.data.message = error.data.arguments[0];
347 instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), {
348 title: "OpenERP " + (_.str.capitalize(error.type) || "Warning"),
350 {text: _t("Ok"), click: function() { $(this).dialog("close"); }},
351 {text: error.data.arguments[2], click: function() {
352 window.location.href='#action='+error.data.arguments[1];
353 $(this).dialog("close");
360 instance.web.crash_manager_registry.add('openerp.exceptions.RedirectWarning', 'instance.web.RedirectWarningHandler');
362 instance.web.Loading = instance.web.Widget.extend({
363 template: _t("Loading"),
364 init: function(parent) {
367 this.blocked_ui = false;
368 this.session.on("request", this, this.request_call);
369 this.session.on("response", this, this.response_call);
370 this.session.on("response_failed", this, this.response_call);
372 destroy: function() {
373 this.on_rpc_event(-this.count);
376 request_call: function() {
377 this.on_rpc_event(1);
379 response_call: function() {
380 this.on_rpc_event(-1);
382 on_rpc_event : function(increment) {
384 if (!this.count && increment === 1) {
386 this.long_running_timer = setTimeout(function () {
387 self.blocked_ui = true;
388 instance.web.blockUI();
392 this.count += increment;
393 if (this.count > 0) {
394 if (instance.session.debug) {
395 this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count));
397 this.$el.text(_t("Loading"));
400 this.getParent().$el.addClass('oe_wait');
403 clearTimeout(this.long_running_timer);
404 // Don't unblock if blocked by somebody else
405 if (self.blocked_ui) {
406 this.blocked_ui = false;
407 instance.web.unblockUI();
410 this.getParent().$el.removeClass('oe_wait');
415 instance.web.DatabaseManager = instance.web.Widget.extend({
416 init: function(parent) {
418 this.unblockUIFunction = instance.web.unblockUI;
419 $.validator.addMethod('matches', function (s, _, re) {
420 return new RegExp(re).test(s);
421 }, _t("Invalid database name"));
425 $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
426 var fetch_db = this.rpc("/web/database/get_list", {}).then(
428 self.db_list = result;
434 var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) {
435 self.lang_list = result;
437 return $.when(fetch_db, fetch_langs).always(self.do_render);
439 do_render: function() {
441 instance.webclient.toggle_bars(true);
442 self.$el.html(QWeb.render("DatabaseManager", { widget : self }));
443 $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self }));
444 $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self }));
445 $('ul.oe_secondary_submenu > li:first').addClass('oe_active');
446 $('ul.oe_secondary_submenu > li').bind('click', function (event) {
447 var menuitem = $(this);
448 menuitem.addClass('oe_active').siblings().removeClass('oe_active');
449 var form_id =menuitem.find('a').attr('href');
450 $(form_id).show().siblings().hide();
451 event.preventDefault();
453 $('#back-to-login').click(self.do_exit);
454 self.$el.find("td").addClass("oe_form_group_cell");
455 self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label");
456 self.$el.find("label").addClass("oe_form_label");
457 self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create });
458 self.$el.find("form[name=duplicate_db_form]").validate({ submitHandler: self.do_duplicate });
459 self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop });
460 self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup });
461 self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore });
462 self.$el.find("form[name=change_pwd_form]").validate({
464 old_pwd: _t("Please enter your previous password"),
465 new_pwd: _t("Please enter your new password"),
467 required: _t("Please confirm your new password"),
468 equalTo: _t("The confirmation does not match the password")
471 submitHandler: self.do_change_password
474 destroy: function () {
475 this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty();
479 * Converts a .serializeArray() result into a dict. Does not bother folding
480 * multiple identical keys into an array, last key wins.
482 * @param {Array} array
484 to_object: function (array) {
486 _(array).each(function (record) {
487 result[record.name] = record.value;
492 * Blocks UI and replaces $.unblockUI by a noop to prevent third parties
493 * from unblocking the UI
495 blockUI: function () {
496 instance.web.blockUI();
497 instance.web.unblockUI = function () {};
500 * Reinstates $.unblockUI so third parties can play with blockUI, and
503 unblockUI: function () {
504 instance.web.unblockUI = this.unblockUIFunction;
505 instance.web.unblockUI();
508 * Displays an error dialog resulting from the various RPC communications
509 * failing over themselves
511 * @param {Object} error error description
512 * @param {String} error.title title of the error dialog
513 * @param {String} error.error message of the error dialog
515 display_error: function (error) {
516 return instance.web.dialog($('<div>'), {
520 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
522 }).html(error.error);
524 do_create: function(form) {
526 var fields = $(form).serializeArray();
527 self.rpc("/web/database/create", {'fields': fields}).done(function(result) {
528 var form_obj = self.to_object(fields);
529 var client_action = {
530 type: 'ir.actions.client',
533 'db': form_obj['db_name'],
535 'password': form_obj['create_admin_pwd'],
536 'login_successful': function() {
537 var url = '/?db=' + form_obj['db_name'];
538 if (self.session.debug) {
541 instance.web.redirect(url);
546 self.do_action(client_action);
549 do_duplicate: function(form) {
551 var fields = $(form).serializeArray();
552 self.rpc("/web/database/duplicate", {'fields': fields}).then(function(result) {
554 self.display_error(result);
557 self.do_notify(_t("Duplicating database"), _t("The database has been duplicated."));
561 do_drop: function(form) {
564 fields = $form.serializeArray(),
565 $db_list = $form.find('[name=drop_db]'),
567 if (!db || !confirm(_.str.sprintf(_t("Do you really want to delete the database: %s ?"), db))) {
570 self.rpc("/web/database/drop", {'fields': fields}).done(function(result) {
572 self.display_error(result);
575 self.do_notify(_t("Dropping database"), _.str.sprintf(_t("The database %s has been dropped"), db));
579 do_backup: function(form) {
582 self.session.get_file({
584 success: function () {
585 self.do_notify(_t("Backed"), _t("Database backed up successfully"));
587 error: function(error){
590 title: _t("Backup Database"),
591 error: 'AccessDenied'
595 complete: function() {
600 do_restore: function(form) {
604 url: '/web/database/restore',
607 success: function (body) {
608 // If empty body, everything went fine
609 if (!body) { return; }
611 if (body.indexOf('403 Forbidden') !== -1) {
613 title: _t("Access Denied"),
614 error: _t("Incorrect super-administrator password")
618 title: _t("Restore Database"),
619 error: _t("Could not restore the database")
623 complete: function() {
625 self.do_notify(_t("Restored"), _t("Database restored successfully"));
629 do_change_password: function(form) {
631 self.rpc("/web/database/change_password", {
632 'fields': $(form).serializeArray()
633 }).done(function(result) {
635 self.display_error(result);
639 self.do_notify(_t("Changed Password"), _t("Password has been changed successfully"));
642 do_exit: function () {
644 instance.webclient.show_login();
647 instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager");
649 instance.web.Login = instance.web.Widget.extend({
651 remember_credentials: true,
653 'change input[name=db],select[name=db]': function(ev) {
654 this.set('database_selector', $(ev.currentTarget).val());
658 init: function(parent, action) {
660 this.has_local_storage = typeof(localStorage) != 'undefined';
662 this.selected_db = null;
663 this.selected_login = null;
664 this.params = action.params || {};
665 if (_.isEmpty(this.params)) {
666 this.params = $.bbq.getState(true);
668 if (action && action.params && action.params.db) {
669 this.params.db = action.params.db;
670 } else if ($.deparam.querystring().db) {
671 this.params.db = $.deparam.querystring().db;
673 if (this.params.db) {
674 this.selected_db = this.params.db;
677 if (this.params.login_successful) {
678 this.on('login_successful', this, this.params.login_successful);
680 // some cleanup to remove any trace of that last login feature
681 if (typeof(localStorage) != 'undefined') {
683 _.each(_.range(localStorage.length), function(i) {
684 var key = localStorage.key(i);
685 if (key.match(/^.*?\|last_password$/)) {
689 _.each(toRemove, function(k) {
690 localStorage.removeItem(k);
696 self.$el.find("form").submit(self.on_submit);
697 self.$el.find('.oe_login_manage_db').click(function() {
698 self.do_action("database_manager");
700 self.on('change:database_selector', this, function() {
701 this.database_selected(this.get('database_selector'));
704 if ($.param.fragment().token) {
705 self.params.token = $.param.fragment().token;
707 // used by dbmanager.do_create via internal client action
708 if (self.params.db && self.params.login && self.params.password) {
709 d = self.do_login(self.params.db, self.params.login, self.params.password);
711 d = self.rpc("/web/database/get_list", {})
712 .done(self.on_db_loaded)
713 .fail(self.on_db_failed)
715 if (self.selected_db && self.has_local_storage && self.remember_credentials) {
716 self.$("[name=login]").val(localStorage.getItem(self.selected_db + '|last_login') || '');
722 database_selected: function(db) {
723 var params = $.deparam.querystring();
725 this.$('.oe_login_dbpane').empty().text(_t('Loading...'));
726 this.$('[name=login], [name=password]').prop('readonly', true);
727 instance.web.redirect('/?' + $.param(params));
729 on_db_loaded: function (result) {
731 this.db_list = result;
732 if (!this.selected_db) {
733 this.selected_db = result[0];
735 this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db}));
736 if(this.db_list.length === 0) {
737 this.do_action("database_manager");
738 } else if(this.db_list.length === 1) {
739 this.$('div.oe_login_dbpane').hide();
741 this.$('div.oe_login_dbpane').show();
744 on_db_failed: function (error, event) {
745 if (error.data.name === 'openerp.exceptions.AccessDenied') {
746 event.preventDefault();
749 on_submit: function(ev) {
753 var db = this.$("form [name=db]").val();
755 this.do_warn(_t("Login"), _t("No database selected !"));
758 var login = this.$("form input[name=login]").val();
759 var password = this.$("form input[name=password]").val();
761 this.do_login(db, login, password);
764 * Performs actual login operation, and UI-related stuff
766 * @param {String} db database to log in
767 * @param {String} login user login
768 * @param {String} password user password
770 do_login: function (db, login, password) {
773 self.$(".oe_login_pane").fadeOut("slow");
774 return this.session.session_authenticate(db, login, password).then(function() {
775 if (self.has_local_storage && self.remember_credentials) {
776 localStorage.setItem(db + '|last_login', login);
778 self.trigger('login_successful');
780 self.$(".oe_login_pane").fadeIn("fast", function() {
781 self.show_error(_t("Invalid username or password"));
785 show_error: function(message) {
786 this.$el.addClass("oe_login_invalid");
787 this.$(".oe_login_error_message").text(message);
789 hide_error: function() {
790 this.$el.removeClass('oe_login_invalid');
793 instance.web.client_actions.add("login", "instance.web.Login");
797 * Redirect to url by replacing window.location
798 * If wait is true, sleep 1s and wait for the server i.e. after a restart.
800 instance.web.redirect = function(url, wait) {
801 // Dont display a dialog if some xmlhttprequest are in progress
802 if (instance.client && instance.client.crashmanager) {
803 instance.client.crashmanager.active = false;
806 var load = function() {
807 var old = "" + window.location;
809 window.location.reload();
811 window.location = url;
815 var wait_server = function() {
816 instance.session.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
817 setTimeout(wait_server, 250);
822 setTimeout(wait_server, 1000);
829 * Client action to reload the whole interface.
830 * If params.menu_id, it opens the given menu entry.
831 * If params.wait, reload will wait the openerp server to be reachable before reloading
833 instance.web.Reload = function(parent, action) {
834 var params = action.params || {};
835 var menu_id = params.menu_id || false;
836 var l = window.location;
838 var sobj = $.deparam(l.search.substr(1));
839 if (params.url_search) {
840 sobj = _.extend(sobj, params.url_search);
842 var search = '?' + $.param(sobj);
846 hash = "#menu_id=" + menu_id;
848 var url = l.protocol + "//" + l.host + l.pathname + search + hash;
850 instance.web.redirect(url, params.wait);
852 instance.web.client_actions.add("reload", "instance.web.Reload");
855 * Client action to go back in breadcrumb history.
856 * If can't go back in history stack, will go back to home.
858 instance.web.HistoryBack = function(parent) {
859 if (!parent.history_back()) {
860 instance.web.Home(parent);
863 instance.web.client_actions.add("history_back", "instance.web.HistoryBack");
866 * Client action to go back home.
868 instance.web.Home = function(parent, action) {
869 var url = '/' + (window.location.search || '');
870 instance.web.redirect(url, action.params && action.params.wait);
872 instance.web.client_actions.add("home", "instance.web.Home");
874 instance.web.ChangePassword = instance.web.Widget.extend({
875 template: "ChangePassword",
878 this.getParent().dialog_title = _t("Change Password");
879 var $button = self.$el.find('.oe_form_button');
880 $button.appendTo(this.getParent().$buttons);
881 $button.eq(2).click(function(){
882 self.getParent().close();
884 $button.eq(0).click(function(){
885 self.rpc("/web/session/change_password",{
886 'fields': $("form[name=change_password_form]").serializeArray()
887 }).done(function(result) {
889 self.display_error(result);
892 instance.webclient.on_logout();
897 display_error: function (error) {
898 return instance.web.dialog($('<div>'), {
902 {text: _t("Ok"), click: function() { $(this).dialog("close"); }}
904 }).html(error.error);
907 instance.web.client_actions.add("change_password", "instance.web.ChangePassword");
909 instance.web.Menu = instance.web.Widget.extend({
913 this._super.apply(this, arguments);
914 this.has_been_loaded = $.Deferred();
915 this.maximum_visible_links = 'auto'; // # of menu to show. 0 = do not crop, 'auto' = algo
916 this.data = {data:{children:[]}};
917 this.on("menu_loaded", this, function (menu_data) {
919 // launch the fetch of needaction counters, asynchronous
920 if (!_.isEmpty(menu_data.all_menu_ids)) {
921 this.do_load_needaction(menu_data.all_menu_ids);
924 var lazyreflow = _.debounce(this.reflow.bind(this), 200);
925 instance.web.bus.on('resize', this, function() {
931 this._super.apply(this, arguments);
932 this.$secondary_menus = this.getParent().$el.find('.oe_secondary_menus_container');
933 this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click);
934 return this.do_reload();
936 do_reload: function() {
938 return this.rpc("/web/menu/load", {}).done(function(r) {
942 menu_loaded: function(data) {
944 this.data = {data: data};
945 this.renderElement();
946 this.$secondary_menus.html(QWeb.render("Menu.secondary", { widget : this }));
947 this.$el.on('click', 'a[data-menu]', this.on_top_menu_click);
948 // Hide second level submenus
949 this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide();
950 if (self.current_menu) {
951 self.open_menu(self.current_menu);
953 this.trigger('menu_loaded', data);
954 this.has_been_loaded.resolve();
956 do_load_needaction: function (menu_ids) {
958 menu_ids = _.compact(menu_ids);
959 if (_.isEmpty(menu_ids)) {
962 return this.rpc("/web/menu/load_needaction", {'menu_ids': menu_ids}).done(function(r) {
963 self.on_needaction_loaded(r);
966 on_needaction_loaded: function(data) {
968 this.needaction_data = data;
969 _.each(this.needaction_data, function (item, menu_id) {
970 var $item = self.$secondary_menus.find('a[data-menu="' + menu_id + '"]');
971 $item.find('.oe_menu_counter').remove();
972 if (item.needaction_counter && item.needaction_counter > 0) {
973 $item.append(QWeb.render("Menu.needaction_counter", { widget : item }));
978 * Reflow the menu items and dock overflowing items into a "More" menu item.
979 * Automatically called when 'menu_loaded' event is triggered and on window resizing.
983 this.$el.height('auto').show();
984 var $more_container = this.$('.oe_menu_more_container').hide();
985 var $more = this.$('.oe_menu_more');
986 $more.children('li').insertBefore($more_container);
987 var $toplevel_items = this.$el.children('li').not($more_container).hide();
988 $toplevel_items.each(function() {
989 var remaining_space = self.$el.parent().width() - $more_container.outerWidth();
990 self.$el.parent().children(':visible').each(function() {
991 remaining_space -= $(this).outerWidth();
993 if ($(this).width() > remaining_space) {
998 $more.append($toplevel_items.filter(':hidden').show());
999 $more_container.toggle(!!$more.children().length);
1000 // Hide toplevel item if there is only one
1001 var $toplevel = this.$el.children("li:visible");
1002 if ($toplevel.length === 1) {
1007 * Opens a given menu by id, as if a user had browsed to that menu by hand
1008 * except does not trigger any event on the way
1010 * @param {Number} id database id of the terminal menu to select
1012 open_menu: function (id) {
1013 this.current_menu = id;
1014 this.session.active_id = id;
1015 var $clicked_menu, $sub_menu, $main_menu;
1016 $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']');
1017 this.trigger('open_menu', id, $clicked_menu);
1019 if (this.$secondary_menus.has($clicked_menu).length) {
1020 $sub_menu = $clicked_menu.parents('.oe_secondary_menu');
1021 $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']');
1023 $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']');
1024 $main_menu = $clicked_menu;
1027 // Activate current main menu
1028 this.$el.find('.oe_active').removeClass('oe_active');
1029 $main_menu.addClass('oe_active');
1031 // Show current sub menu
1032 this.$secondary_menus.find('.oe_secondary_menu').hide();
1035 // Hide/Show the leftbar menu depending of the presence of sub-items
1036 this.$secondary_menus.parent('.oe_leftbar').toggle(!!$sub_menu.children().length);
1038 // Activate current menu item and show parents
1039 this.$secondary_menus.find('.oe_active').removeClass('oe_active');
1040 if ($main_menu !== $clicked_menu) {
1041 $clicked_menu.parents().show();
1042 if ($clicked_menu.is('.oe_menu_toggler')) {
1043 $clicked_menu.toggleClass('oe_menu_opened').siblings('.oe_secondary_submenu:first').toggle();
1045 $clicked_menu.parent().addClass('oe_active');
1050 * Call open_menu with the first menu_item matching an action_id
1052 * @param {Number} id the action_id to match
1054 open_action: function (id) {
1055 var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]');
1056 var menu_id = $menu.data('menu');
1058 this.open_menu(menu_id);
1062 * Process a click on a menu item
1064 * @param {Number} id the menu_id
1065 * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context
1067 menu_click: function(id, needaction) {
1068 if (!id) { return; }
1070 // find back the menuitem in dom to get the action
1071 var $item = this.$el.find('a[data-menu=' + id + ']');
1072 if (!$item.length) {
1073 $item = this.$secondary_menus.find('a[data-menu=' + id + ']');
1075 var action_id = $item.data('action-id');
1076 // If first level menu doesnt have action trigger first leaf
1078 if(this.$el.has($item).length) {
1079 var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']');
1080 var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]');
1082 action_id = $items.data('action-id');
1083 id = $items.data('menu');
1088 this.trigger('menu_click', {
1089 action_id: action_id,
1090 needaction: needaction,
1092 previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu)
1097 do_reload_needaction: function () {
1099 if (self.current_menu) {
1100 self.do_load_needaction([self.current_menu]).then(function () {
1101 self.trigger("need_action_reloaded");
1106 * Jquery event handler for menu click
1108 * @param {Event} ev the jquery event
1110 on_top_menu_click: function(ev) {
1112 var id = $(ev.currentTarget).data('menu');
1113 var menu_ids = [id];
1114 var menu = _.filter(this.data.data.children, function (menu) {return menu.id == id;})[0];
1115 function add_menu_ids (menu) {
1116 if (menu.children) {
1117 _.each(menu.children, function (menu) {
1118 menu_ids.push(menu.id);
1124 self.do_load_needaction(menu_ids).then(function () {
1125 self.trigger("need_action_reloaded");
1127 this.on_menu_click(ev);
1129 on_menu_click: function(ev) {
1130 ev.preventDefault();
1131 var needaction = $(ev.target).is('div.oe_menu_counter');
1132 this.menu_click($(ev.currentTarget).data('menu'), needaction);
1136 instance.web.UserMenu = instance.web.Widget.extend({
1137 template: "UserMenu",
1138 init: function(parent) {
1139 this._super(parent);
1140 this.update_promise = $.Deferred().resolve();
1144 this._super.apply(this, arguments);
1145 this.$el.on('click', '.oe_dropdown_menu li a[data-menu]', function(ev) {
1146 ev.preventDefault();
1147 var f = self['on_menu_' + $(this).data('menu')];
1153 do_update: function () {
1155 var fct = function() {
1156 var $avatar = self.$el.find('.oe_topbar_avatar');
1157 $avatar.attr('src', $avatar.data('default-src'));
1158 if (!self.session.uid)
1160 var func = new instance.web.Model("res.users").get_func("read");
1161 return self.alive(func(self.session.uid, ["name", "company_id"])).then(function(res) {
1162 var topbar_name = res.name;
1163 if(instance.session.debug)
1164 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1165 if(res.company_id[0] > 1)
1166 topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]);
1167 self.$el.find('.oe_topbar_name').text(topbar_name);
1168 if (!instance.session.debug) {
1169 topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db);
1171 var avatar_src = self.session.url('/web/binary/image', {model:'res.users', field: 'image_small', id: self.session.uid});
1172 $avatar.attr('src', avatar_src);
1175 this.update_promise = this.update_promise.then(fct, fct);
1177 on_menu_help: function() {
1178 window.open('http://help.openerp.com', '_blank');
1180 on_menu_logout: function() {
1181 this.trigger('user_logout');
1183 on_menu_settings: function() {
1185 if (!this.getParent().has_uncommitted_changes()) {
1186 self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) {
1187 result.res_id = instance.session.uid;
1188 self.getParent().action_manager.do_action(result);
1192 on_menu_account: function() {
1194 if (!this.getParent().has_uncommitted_changes()) {
1195 var P = new instance.web.Model('ir.config_parameter');
1196 P.call('get_param', ['database.uuid']).then(function(dbuuid) {
1198 'd': instance.session.db,
1199 'u': window.location.protocol + '//' + window.location.host,
1202 response_type: 'token',
1203 client_id: dbuuid || '',
1204 state: JSON.stringify(state),
1207 instance.web.redirect('https://accounts.openerp.com/oauth2/auth?'+$.param(params));
1211 on_menu_about: function() {
1213 self.rpc("/web/webclient/version_info", {}).done(function(res) {
1214 var $help = $(QWeb.render("UserMenu.about", {version_info: res}));
1215 $help.find('a.oe_activate_debug_mode').click(function (e) {
1217 window.location = $.param.querystring( window.location.href, 'debug');
1219 instance.web.dialog($help, {autoOpen: true,
1220 modal: true, width: 507, height: 290, resizable: false, title: _t("About")});
1225 instance.web.Client = instance.web.Widget.extend({
1226 init: function(parent, origin) {
1227 instance.client = instance.webclient = this;
1228 this._super(parent);
1229 this.origin = origin;
1233 return instance.session.session_bind(this.origin).then(function() {
1234 var $e = $(QWeb.render(self._template, {widget: self}));
1235 self.replaceElement($e);
1238 return self.show_common();
1241 bind_events: function() {
1243 this.$el.on('mouseenter', '.oe_systray > div:not([data-tipsy=true])', function() {
1244 $(this).attr('data-tipsy', 'true').tipsy().trigger('mouseenter');
1246 this.$el.on('click', '.oe_dropdown_toggle', function(ev) {
1247 ev.preventDefault();
1248 var $toggle = $(this);
1249 var $menu = $toggle.siblings('.oe_dropdown_menu');
1250 $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
1251 var state = $menu.is('.oe_opened');
1252 setTimeout(function() {
1253 // Do not alter propagation
1254 $toggle.add($menu).toggleClass('oe_opened', !state);
1256 // Move $menu if outside window's edge
1257 var doc_width = $(document).width();
1258 var offset = $menu.offset();
1259 var menu_width = $menu.width();
1260 var x = doc_width - offset.left - menu_width - 2;
1262 $menu.offset({ left: offset.left + x }).width(menu_width);
1267 instance.web.bus.on('click', this, function(ev) {
1269 if (!$(ev.target).is('input[type=file]')) {
1270 self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
1274 show_common: function() {
1276 this.crashmanager = new instance.web.CrashManager();
1277 instance.session.on('error', this.crashmanager, this.crashmanager.rpc_error);
1278 self.notification = new instance.web.Notification(this);
1279 self.notification.appendTo(self.$el);
1280 self.loading = new instance.web.Loading(self);
1281 self.loading.appendTo(self.$el);
1282 self.action_manager = new instance.web.ActionManager(self);
1283 self.action_manager.appendTo(self.$('.oe_application'));
1285 toggle_bars: function(value) {
1286 this.$('tr:has(td.oe_topbar),.oe_leftbar').toggle(value);
1288 has_uncommitted_changes: function() {
1293 instance.web.WebClient = instance.web.Client.extend({
1294 _template: 'WebClient',
1296 'click .oe_logo_edit_admin': 'logo_edit'
1298 init: function(parent) {
1299 this._super(parent);
1300 this._current_state = null;
1301 this.menu_dm = new instance.web.DropMisordered();
1302 this.action_mutex = new $.Mutex();
1303 this.set('title_part', {"zopenerp": "OpenERP"});
1307 this.on("change:title_part", this, this._title_changed);
1308 this._title_changed();
1309 return $.when(this._super()).then(function() {
1310 if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) {
1311 $("body").addClass("kitten-mode-activated");
1312 $("body").css("background-image", "url(" + instance.session.origin + "/web/static/src/img/back-enable.jpg" + ")");
1314 var imgkit = Math.floor(Math.random() * 2 + 1);
1315 $.blockUI.defaults.message = '<img src="http://www.amigrave.com/loading-kitten/' + imgkit + '.gif" class="loading-kitten">';
1318 if (!self.session.session_is_valid()) {
1321 self.show_application();
1326 Sets the first part of the title of the window, dedicated to the current action.
1328 set_title: function(title) {
1329 this.set_title_part("action", title);
1332 Sets an arbitrary part of the title of the window. Title parts are identified by strings. Each time
1333 a title part is changed, all parts are gathered, ordered by alphabetical order and displayed in the
1334 title of the window separated by '-'.
1336 set_title_part: function(part, title) {
1337 var tmp = _.clone(this.get("title_part"));
1339 this.set("title_part", tmp);
1341 _title_changed: function() {
1342 var parts = _.sortBy(_.keys(this.get("title_part")), function(x) { return x; });
1344 _.each(parts, function(part) {
1345 var str = this.get("title_part")[part];
1347 tmp = tmp ? tmp + " - " + str : str;
1350 document.title = tmp;
1352 show_common: function() {
1355 window.onerror = function (message, file, line) {
1356 self.crashmanager.show_error({
1357 type: _t("Client Error"),
1359 data: {debug: file + ':' + line}
1363 show_login: function() {
1364 this.toggle_bars(false);
1366 var state = $.bbq.getState(true);
1368 type: 'ir.actions.client',
1373 this.action_manager.do_action(action);
1374 this.action_manager.inner_widget.on('login_successful', this, function() {
1375 this.show_application(); // will load the state we just pushed
1378 show_application: function() {
1380 self.toggle_bars(true);
1382 self.menu = new instance.web.Menu(self);
1383 self.menu.replace(this.$el.find('.oe_menu_placeholder'));
1384 self.menu.on('menu_click', this, this.on_menu_action);
1385 self.user_menu = new instance.web.UserMenu(self);
1386 self.user_menu.replace(this.$el.find('.oe_user_menu_placeholder'));
1387 self.user_menu.on('user_logout', self, self.on_logout);
1388 self.user_menu.do_update();
1389 self.bind_hashchange();
1391 self.check_timezone();
1393 update_logo: function() {
1394 var img = this.session.url('/web/binary/company_logo');
1395 this.$('.oe_logo img').attr('src', '').attr('src', img);
1396 this.$('.oe_logo_edit').toggleClass('oe_logo_edit_admin', this.session.uid === 1);
1398 logo_edit: function(ev) {
1400 self.alive(new instance.web.Model("res.users").get_func("read")(this.session.uid, ["company_id"])).then(function(res) {
1401 self.rpc("/web/action/load", { action_id: "base.action_res_company_form" }).done(function(result) {
1402 result.res_id = res['company_id'][0];
1403 result.target = "new";
1404 result.views = [[false, 'form']];
1406 action_buttons: true,
1408 self.action_manager.do_action(result);
1409 var form = self.action_manager.dialog_widget.views.form.controller;
1410 form.on("on_button_cancel", self.action_manager.dialog, self.action_manager.dialog.close);
1411 form.on('record_saved', self, function() {
1412 self.action_manager.dialog.close();
1419 check_timezone: function() {
1421 return self.alive(new instance.web.Model('res.users').call('read', [[this.session.uid], ['tz_offset']])).then(function(result) {
1422 var user_offset = result[0]['tz_offset'];
1423 var offset = -(new Date().getTimezoneOffset());
1424 // _.str.sprintf()'s zero front padding is buggy with signed decimals, so doing it manually
1425 var browser_offset = (offset < 0) ? "-" : "+";
1426 browser_offset += _.str.sprintf("%02d", Math.abs(offset / 60));
1427 browser_offset += _.str.sprintf("%02d", Math.abs(offset % 60));
1428 if (browser_offset !== user_offset) {
1429 var $icon = $(QWeb.render('WebClient.timezone_systray'));
1430 $icon.on('click', function() {
1431 var notification = self.do_warn(_t("Timezone Mismatch"), QWeb.render('WebClient.timezone_notification', {
1432 user_timezone: instance.session.user_context.tz || 'UTC',
1433 user_offset: user_offset,
1434 browser_offset: browser_offset,
1436 notification.element.find('.oe_webclient_timezone_notification').on('click', function() {
1437 notification.close();
1438 }).find('a').on('click', function() {
1439 notification.close();
1440 self.user_menu.on_menu_settings();
1444 $icon.appendTo(self.$('.oe_systray'));
1448 destroy_content: function() {
1449 _.each(_.clone(this.getChildren()), function(el) {
1452 this.$el.children().remove();
1454 do_reload: function() {
1456 return this.session.session_reload().then(function () {
1457 instance.session.load_modules(true).then(
1458 self.menu.proxy('do_reload')); });
1461 do_notify: function() {
1462 var n = this.notification;
1463 return n.notify.apply(n, arguments);
1465 do_warn: function() {
1466 var n = this.notification;
1467 return n.warn.apply(n, arguments);
1469 on_logout: function() {
1471 if (!this.has_uncommitted_changes()) {
1472 this.session.session_logout().done(function () {
1473 $(window).unbind('hashchange', self.on_hashchange);
1474 self.do_push_state({});
1475 window.location.reload();
1479 bind_hashchange: function() {
1481 $(window).bind('hashchange', this.on_hashchange);
1483 var state = $.bbq.getState(true);
1484 if (_.isEmpty(state) || state.action == "login") {
1485 self.menu.has_been_loaded.done(function() {
1486 var first_menu_id = self.menu.$el.find("a:first").data("menu");
1488 self.menu.menu_click(first_menu_id);
1492 $(window).trigger('hashchange');
1495 on_hashchange: function(event) {
1497 var stringstate = event.getState(false);
1498 if (!_.isEqual(this._current_state, stringstate)) {
1499 var state = event.getState(true);
1500 if(!state.action && state.menu_id) {
1501 self.menu.has_been_loaded.done(function() {
1502 self.menu.do_reload().done(function() {
1503 self.menu.menu_click(state.menu_id);
1507 state._push_me = false; // no need to push state back...
1508 this.action_manager.do_load_state(state, !!this._current_state);
1511 this._current_state = stringstate;
1513 do_push_state: function(state) {
1514 this.set_title(state.title);
1516 var url = '#' + $.param(state);
1517 this._current_state = $.deparam($.param(state), false); // stringify all values
1518 $.bbq.pushState(url);
1519 this.trigger('state_pushed', state);
1521 on_menu_action: function(options) {
1523 return this.menu_dm.add(this.rpc("/web/action/load", { action_id: options.action_id }))
1524 .then(function (result) {
1525 return self.action_mutex.exec(function() {
1526 if (options.needaction) {
1527 result.context = new instance.web.CompoundContext(result.context, {
1528 search_default_message_unread: true,
1529 search_disable_custom_filters: true,
1532 var completed = $.Deferred();
1533 $.when(self.action_manager.do_action(result, {
1534 clear_breadcrumbs: true,
1535 action_menu_id: self.menu.current_menu,
1536 })).fail(function() {
1537 self.menu.open_menu(options.previous_menu_id);
1538 }).always(function() {
1539 completed.resolve();
1541 setTimeout(function() {
1542 completed.resolve();
1544 // We block the menu when clicking on an element until the action has correctly finished
1545 // loading. If something crash, there is a 2 seconds timeout before it's unblocked.
1550 set_content_full_screen: function(fullscreen) {
1551 $(document.body).css('overflow-y', fullscreen ? 'hidden' : 'scroll');
1552 this.$('.oe_webclient').toggleClass(
1553 'oe_content_full_screen', fullscreen);
1555 has_uncommitted_changes: function() {
1556 var $e = $.Event('clear_uncommitted_changes');
1557 instance.web.bus.trigger('clear_uncommitted_changes', $e);
1558 if ($e.isDefaultPrevented()) {
1561 return this._super.apply(this, arguments);
1566 instance.web.EmbeddedClient = instance.web.Client.extend({
1567 _template: 'EmbedClient',
1568 init: function(parent, origin, dbname, login, key, action_id, options) {
1569 this._super(parent, origin);
1570 this.bind_credentials(dbname, login, key);
1571 this.action_id = action_id;
1572 this.options = options || {};
1576 return $.when(this._super()).then(function() {
1577 return self.authenticate().then(function() {
1578 if (!self.action_id) {
1581 return self.rpc("/web/action/load", { action_id: self.action_id }).done(function(result) {
1582 var action = result;
1583 action.flags = _.extend({
1584 //views_switcher : false,
1585 search_view : false,
1586 action_buttons : false,
1589 }, self.options, action.flags || {});
1591 self.do_action(action);
1597 do_action: function(/*...*/) {
1598 var am = this.action_manager;
1599 return am.do_action.apply(am, arguments);
1602 authenticate: function() {
1603 var s = instance.session;
1604 if (s.session_is_valid() && s.db === this.dbname && s.login === this.login) {
1607 return instance.session.session_authenticate(this.dbname, this.login, this.key);
1610 bind_credentials: function(dbname, login, key) {
1611 this.dbname = dbname;
1618 instance.web.embed = function (origin, dbname, login, key, action, options) {
1619 $('head').append($('<link>', {
1620 'rel': 'stylesheet',
1622 'href': origin +'/web/webclient/css'
1624 var currentScript = document.currentScript;
1625 if (!currentScript) {
1626 var sc = document.getElementsByTagName('script');
1627 currentScript = sc[sc.length-1];
1629 var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options);
1630 client.insertAfter(currentScript);
1635 // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: