diff options
| author | Olivier Dony <odo@openerp.com> | 2012-10-01 16:53:08 +0200 |
|---|---|---|
| committer | Olivier Dony <odo@openerp.com> | 2012-10-01 16:53:08 +0200 |
| commit | 9e6920415e41a052cffa7b898bf044dbf82b5ccb (patch) | |
| tree | 05e8d64788da1ff9f65e7406e3ff3d96d58a5762 /addons/web/static/src/js | |
| parent | 4ed79f3457a0252dcee4780385dc474ff2af356e (diff) | |
| parent | 1370ae133d4477d0a7a2239732f529a1eef39824 (diff) | |
[MERGE] Sync with trunk
bzr revid: odo@openerp.com-20121001145308-ri5rp2cazxqxcxme
Diffstat (limited to 'addons/web/static/src/js')
| -rw-r--r-- | addons/web/static/src/js/boot.js | 14 | ||||
| -rw-r--r-- | addons/web/static/src/js/chrome.js | 848 | ||||
| -rw-r--r-- | addons/web/static/src/js/corelib.js | 221 | ||||
| -rw-r--r-- | addons/web/static/src/js/coresetup.js | 395 | ||||
| -rw-r--r-- | addons/web/static/src/js/data.js | 289 | ||||
| -rw-r--r-- | addons/web/static/src/js/data_export.js | 118 | ||||
| -rw-r--r-- | addons/web/static/src/js/data_import.js | 366 | ||||
| -rw-r--r-- | addons/web/static/src/js/formats.js | 85 | ||||
| -rw-r--r-- | addons/web/static/src/js/search.js | 291 | ||||
| -rw-r--r-- | addons/web/static/src/js/test_support.js | 13 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_editor.js | 1207 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_form.js | 2100 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list.js | 724 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list_editable.js | 965 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_tree.js | 124 | ||||
| -rw-r--r-- | addons/web/static/src/js/views.js | 794 |
16 files changed, 4340 insertions, 4214 deletions
diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index f83dbc97095..05ce709e80a 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -24,7 +24,7 @@ init: function(modules) { // By default only web will be loaded, the rest will be by loaded // by openerp.web.Session on the first session_authenticate - modules = modules || ["web"]; + modules = _.union(['web'], modules || []); var new_instance = { // links to the global openerp _openerp: openerp, @@ -37,7 +37,10 @@ }; openerp.instances[new_instance._session_id] = new_instance; for(var i=0; i < modules.length; i++) { - openerp[modules[i]](new_instance,new_instance[modules[i]]); + new_instance[modules[i]] = {}; + if (openerp[modules[i]]) { + openerp[modules[i]](new_instance,new_instance[modules[i]]); + } } return new_instance; } @@ -48,17 +51,12 @@ * OpenERP Web web module split *---------------------------------------------------------*/ openerp.web = function(session) { - var files = ["corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import","view_editor"]; + var files = ["corelib","coresetup","dates","formats","chrome","data","views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import"]; for(var i=0; i<files.length; i++) { if(openerp.web[files[i]]) { openerp.web[files[i]](session); } } - session.log = function() { - if (session.connection.debug && window.console) { - console.log.apply(console, arguments); - } - } }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index a36421f2b2b..014313d5428 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -9,12 +9,11 @@ instance.web.Notification = instance.web.Widget.extend({ template: 'Notification', init: function() { this._super.apply(this, arguments); - // move to instance.web.notification - instance.notification = this; + instance.web.notification = this; }, start: function() { this._super.apply(this, arguments); - this.$element.notify({ + this.$el.notify({ speed: 500, expires: 2500 }); @@ -25,7 +24,7 @@ instance.web.Notification = instance.web.Widget.extend({ if (sticky) { opts.expires = false; } - this.$element.notify('create', { + this.$el.notify('create', { title: title, text: text }, opts); @@ -36,7 +35,7 @@ instance.web.Notification = instance.web.Widget.extend({ if (sticky) { opts.expires = false; } - this.$element.notify('create', 'oe_notification_alert', { + this.$el.notify('create', 'oe_notification_alert', { title: title, text: text }, opts); @@ -58,20 +57,18 @@ instance.web.Dialog = instance.web.Widget.extend({ init: function (parent, options, content) { var self = this; this._super(parent); - if (content) { - this.$element = content instanceof $ ? content : $(content); - } + this.content_to_set = content; this.dialog_options = { modal: true, destroy_on_close: true, - width: $(window).width() * (($(window).width() > 1024) ? 0.5 : 0.75), + width: 900, min_width: 0, max_width: '95%', height: 'auto', min_height: 0, - max_height: this.get_height('100%') - 140, + max_height: this.get_height('100%') - 200, autoOpen: false, - position: [false, 50], + position: [false, 40], buttons: {}, beforeClose: function () { self.on_close(); }, resizeStop: this.on_resized @@ -82,13 +79,11 @@ instance.web.Dialog = instance.web.Widget.extend({ } } if (options) { + if (options.buttons) { + this.params_buttons = true; + } _.extend(this.dialog_options, options); } - if (this.dialog_options.autoOpen) { - this.open(); - } else { - instance.web.dialog(this.$element, this.get_options()); - } }, get_options: function(options) { var self = this, @@ -120,32 +115,67 @@ instance.web.Dialog = instance.web.Widget.extend({ return parseInt(val, 10); } }, - open: function(options) { - // TODO fme: bind window on resize - if (this.template) { - this.$element.html(this.renderElement()); + renderElement: function() { + if (this.content_to_set) { + this.setElement(this.content_to_set); + } else if (this.template) { + this._super(); } + }, + open: function(options) { + if (! this.dialog_inited) + this.init_dialog(); var o = this.get_options(options); - instance.web.dialog(this.$element, o).dialog('open'); + if (! this.params_buttons) { + this.$buttons.appendTo($("body")); + } + instance.web.dialog(this.$el, o).dialog('open'); + if (! this.params_buttons) { + this.$buttons.appendTo(this.$el.dialog("widget")); + } if (o.height === 'auto' && o.max_height) { - this.$element.css({ 'max-height': o.max_height, 'overflow-y': 'auto' }); + this.$el.css({ 'max-height': o.max_height, 'overflow-y': 'auto' }); } return this; }, + init_dialog: function(options) { + this.renderElement(); + var o = this.get_options(options); + instance.web.dialog(this.$el, o); + if (! this.params_buttons) { + this.$buttons = $('<div class="ui-dialog-buttonpane ui-widget-content ui-helper-clearfix" />'); + this.$el.dialog("widget").append(this.$buttons); + } + var res = this.start(); + this.dialog_inited = true; + return res; + }, close: function() { - this.$element.dialog('close'); + this.$el.dialog('close'); }, on_close: function() { + if (this.__tmp_dialog_destroying) + return; if (this.dialog_options.destroy_on_close) { - this.$element.dialog('destroy'); + this.__tmp_dialog_closing = true; + this.destroy(); + this.__tmp_dialog_closing = undefined; } }, on_resized: function() { - //openerp.log("Dialog resized to %d x %d", this.$element.width(), this.$element.height()); }, destroy: function () { - this.close(); - this.$element.dialog('destroy'); + _.each(this.getChildren(), function(el) { + el.destroy(); + }); + if (! this.__tmp_dialog_closing) { + this.__tmp_dialog_destroying = true; + this.close(); + this.__tmp_dialog_destroying = undefined; + } + if (! this.isDestroyed()) { + this.$el.dialog('destroy'); + } this._super(); } }); @@ -176,29 +206,9 @@ instance.web.CrashManager = instance.web.CallbackEnabled.extend({ on_traceback: function(error) { var self = this; var buttons = {}; - if (instance.connection.openerp_entreprise) { - buttons[_t("Send OpenERP Enterprise Report")] = function() { - var $this = $(this); - var issuename = $('#issuename').val(); - var explanation = $('#explanation').val(); - var remark = $('#remark').val(); - // Call the send method from server to send mail with details - new instance.web.DataSet(self, 'publisher_warranty.contract').call_and_eval('send', [error.data,explanation,remark,issuename]).then(function(result){ - if (result === false) { - alert('There was a communication error.'); - } else { - $this.dialog('close'); - } - }); - }; - buttons[_t("Dont send")] = function() { - $(this).dialog("close"); - }; - } else { - buttons[_t("Ok")] = function() { - $(this).dialog("close"); - }; - } + buttons[_t("Ok")] = function() { + $(this).dialog("close"); + }; var dialog = new instance.web.Dialog(this, { title: "OpenERP " + _.str.capitalize(error.type), width: '80%', @@ -207,8 +217,15 @@ instance.web.CrashManager = instance.web.CallbackEnabled.extend({ min_height: '600px', buttons: buttons }).open(); - dialog.$element.html(QWeb.render('CrashManager.error', {session: instance.connection, error: error})); - } + dialog.$el.html(QWeb.render('CrashManager.error', {session: instance.session, error: error})); + }, + on_javascript_exception: function(exception) { + this.on_traceback({ + type: _t("Client Error"), + message: exception, + data: {debug: ""} + }); + }, }); instance.web.Loading = instance.web.Widget.extend({ @@ -217,47 +234,49 @@ instance.web.Loading = instance.web.Widget.extend({ this._super(parent); this.count = 0; this.blocked_ui = false; - var self = this; - this.request_call = function() { - self.on_rpc_event(1); - }; - this.response_call = function() { - self.on_rpc_event(-1); - }; - this.session.on_rpc_request.add_first(this.request_call); - this.session.on_rpc_response.add_last(this.response_call); + this.session.on("request", this, this.request_call); + this.session.on("response", this, this.response_call); + this.session.on("error", this, this.response_call); }, destroy: function() { - this.session.on_rpc_request.remove(this.request_call); - this.session.on_rpc_response.remove(this.response_call); this.on_rpc_event(-this.count); this._super(); }, + request_call: function() { + this.on_rpc_event(1); + }, + response_call: function() { + this.on_rpc_event(-1); + }, on_rpc_event : function(increment) { var self = this; if (!this.count && increment === 1) { // Block UI after 3s this.long_running_timer = setTimeout(function () { self.blocked_ui = true; - $.blockUI(); + instance.web.blockUI(); }, 3000); } this.count += increment; if (this.count > 0) { - this.$element.text(_.str.sprintf( _t("Loading (%d)"), this.count)); - this.$element.show(); - this.getParent().$element.addClass('oe_wait'); + if (instance.session.debug) { + this.$el.text(_.str.sprintf( _t("Loading (%d)"), this.count)); + } else { + this.$el.text(_t("Loading")); + } + this.$el.show(); + this.getParent().$el.addClass('oe_wait'); } else { this.count = 0; clearTimeout(this.long_running_timer); // Don't unblock if blocked by somebody else if (self.blocked_ui) { this.blocked_ui = false; - $.unblockUI(); + instance.web.unblockUI(); } - this.$element.fadeOut(); - this.getParent().$element.removeClass('oe_wait'); + this.$el.fadeOut(); + this.getParent().$el.removeClass('oe_wait'); } } }); @@ -265,13 +284,14 @@ instance.web.Loading = instance.web.Widget.extend({ instance.web.DatabaseManager = instance.web.Widget.extend({ init: function(parent) { this._super(parent); - this.unblockUIFunction = $.unblockUI; + this.unblockUIFunction = instance.web.unblockUI; $.validator.addMethod('matches', function (s, _, re) { return new RegExp(re).test(s); }, _t("Invalid database name")); }, start: function() { var self = this; + $('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty(); var fetch_db = this.rpc("/web/database/get_list", {}).pipe( function(result) { self.db_list = result.db_list; @@ -287,17 +307,27 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ }, do_render: function() { var self = this; - self.$element.html(QWeb.render("DatabaseManager", { widget : self })); - self.$element.find(".oe_database_manager_menu").tabs({ - show: function(event, ui) { - $('*[autofocus]:first', ui.panel).focus(); - } + instance.webclient.toggle_bars(true); + self.$el.html(QWeb.render("DatabaseManager", { widget : self })); + $('.oe_user_menu_placeholder').append(QWeb.render("DatabaseManager.user_menu",{ widget : self })); + $('.oe_secondary_menus_container').append(QWeb.render("DatabaseManager.menu",{ widget : self })); + $('ul.oe_secondary_submenu > li:first').addClass('oe_active') + $('ul.oe_secondary_submenu > li').bind('click', function (event) { + var menuitem = $(this); + menuitem.addClass('oe_active').siblings().removeClass('oe_active'); + var form_id =menuitem.find('a').attr('href'); + $(form_id).show().siblings().hide(); + event.preventDefault(); }); - self.$element.find("form[name=create_db_form]").validate({ submitHandler: self.do_create }); - self.$element.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop }); - self.$element.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup }); - self.$element.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore }); - self.$element.find("form[name=change_pwd_form]").validate({ + $('#back-to-login').click(self.do_exit); + self.$el.find("td").addClass("oe_form_group_cell"); + self.$el.find("tr td:first-child").addClass("oe_form_group_cell_label"); + self.$el.find("label").addClass("oe_form_label"); + self.$el.find("form[name=create_db_form]").validate({ submitHandler: self.do_create }); + self.$el.find("form[name=drop_db_form]").validate({ submitHandler: self.do_drop }); + self.$el.find("form[name=backup_db_form]").validate({ submitHandler: self.do_backup }); + self.$el.find("form[name=restore_db_form]").validate({ submitHandler: self.do_restore }); + self.$el.find("form[name=change_pwd_form]").validate({ messages: { old_pwd: "Please enter your previous password", new_pwd: "Please enter your new password", @@ -308,10 +338,9 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ }, submitHandler: self.do_change_password }); - self.$element.find("#back_to_login").click(self.do_exit); }, destroy: function () { - this.$element.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty(); + this.$el.find('#db-create, #db-drop, #db-backup, #db-restore, #db-change-password, #back-to-login').unbind('click').end().empty(); this._super(); }, /** @@ -332,16 +361,16 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ * from unblocking the UI */ blockUI: function () { - $.blockUI(); - $.unblockUI = function () {}; + instance.web.blockUI(); + instance.web.unblockUI = function () {}; }, /** * Reinstates $.unblockUI so third parties can play with blockUI, and * unblocks the UI */ unblockUI: function () { - $.unblockUI = this.unblockUIFunction; - $.unblockUI(); + instance.web.unblockUI = this.unblockUIFunction; + instance.web.unblockUI(); }, /** * Displays an error dialog resulting from the various RPC communications @@ -365,10 +394,20 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ var fields = $(form).serializeArray(); self.rpc("/web/database/create", {'fields': fields}, function(result) { var form_obj = self.to_object(fields); - self.getParent().do_login( form_obj['db_name'], 'admin', form_obj['create_admin_pwd']); - self.destroy(); + var client_action = { + type: 'ir.actions.client', + tag: 'login', + params: { + 'db': form_obj['db_name'], + 'login': 'admin', + 'password': form_obj['create_admin_pwd'], + 'login_successful': function() { + self.do_action("reload"); + }, + }, + }; + self.do_action(client_action); }); - }, do_drop: function(form) { var self = this; @@ -396,7 +435,14 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ success: function () { self.do_notify(_t("Backed"), _t("Database backed up successfully")); }, - error: instance.webclient.crashmanager.on_rpc_error, + error: function(error){ + if(error){ + self.display_error({ + title: 'Backup Database', + error: 'AccessDenied' + }); + } + }, complete: function() { self.unblockUI(); } @@ -440,23 +486,33 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ self.display_error(result); return; } + self.unblockUI(); self.do_notify("Changed Password", "Password has been changed successfully"); }); }, do_exit: function () { + this.$el.remove(); + instance.webclient.toggle_bars(false); + this.do_action('login'); } }); +instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager"); instance.web.Login = instance.web.Widget.extend({ template: "Login", remember_credentials: true, - _db_list: null, - init: function(parent) { + init: function(parent, params) { this._super(parent); this.has_local_storage = typeof(localStorage) != 'undefined'; + this.db_list = null; this.selected_db = null; this.selected_login = null; + this.params = params || {}; + + if (this.params.login_successful) { + this.on('login_successful', this, this.params.login_successful); + } if (this.has_local_storage && this.remember_credentials) { this.selected_db = localStorage.getItem('last_db_login_success'); @@ -468,58 +524,47 @@ instance.web.Login = instance.web.Widget.extend({ }, start: function() { var self = this; - - self.$element.find("form").submit(self.on_submit); - - self.$element.find('.oe_login_manage_db').click(function() { - self.$element.find('.oe_login_bottom').hide(); - self.$element.find('.oe_login_pane').hide(); - self.databasemanager = new instance.web.DatabaseManager(self); - self.databasemanager.appendTo(self.$element); - self.databasemanager.do_exit.add_last(function() { - self.databasemanager.destroy(); - self.$element.find('.oe_login_bottom').show(); - self.$element.find('.oe_login_pane').show(); - self.load_db_list(true).then(self.proxy('_db_list_loaded')); - }); + self.$el.find("form").submit(self.on_submit); + self.$el.find('.oe_login_manage_db').click(function() { + self.do_action("database_manager"); }); - return self.load_db_list().then(self.proxy('_db_list_loaded')); - }, - load_db_list: function (force) { - var d = $.when(), self = this; - if (_.isNull(this._db_list) || force) { - d = self.rpc("/web/database/get_list", {}, function(result) { - self._db_list = _.clone(result.db_list); - }, function(error, event) { - if (error.data.fault_code === 'AccessDenied') { - event.preventDefault(); - } - }); + var d; + if (self.params.db) { + if (self.params.login && self.params.password) { + d = self.do_login(self.params.db, self.params.login, self.params.password); + } + } else { + d = self.rpc("/web/database/get_list", {}).done(self.on_db_loaded).fail(self.on_db_failed); } return d; }, - _db_list_loaded: function () { - var list = this._db_list, - dbdiv = this.$element.find('div.oe_login_dbpane'); - this.$element.find("[name=db]").replaceWith(instance.web.qweb.render('Login.dblist', { db_list: list, selected_db: this.selected_db})); - if(list && list.length === 1) { - dbdiv.hide(); + on_db_loaded: function (result) { + this.db_list = result.db_list; + this.$("[name=db]").replaceWith(QWeb.render('Login.dblist', { db_list: this.db_list, selected_db: this.selected_db})); + if(this.db_list.length === 0) { + this.do_action("database_manager"); + } else if(this.db_list.length === 1) { + this.$('div.oe_login_dbpane').hide(); } else { - dbdiv.show(); + this.$('div.oe_login_dbpane').show(); + } + }, + on_db_failed: function (error, event) { + if (error.data.fault_code === 'AccessDenied') { + event.preventDefault(); } }, on_submit: function(ev) { if(ev) { ev.preventDefault(); } - var $e = this.$element; - var db = $e.find("form [name=db]").val(); + var db = this.$("form [name=db]").val(); if (!db) { this.do_warn("Login", "No database selected !"); return false; } - var login = $e.find("form input[name=login]").val(); - var password = $e.find("form input[name=password]").val(); + var login = this.$("form input[name=login]").val(); + var password = this.$("form input[name=password]").val(); this.do_login(db, login, password); }, @@ -532,7 +577,8 @@ instance.web.Login = instance.web.Widget.extend({ */ do_login: function (db, login, password) { var self = this; - this.$element.removeClass('oe_login_invalid'); + self.hide_error(); + self.$(".oe_login_pane").fadeOut("slow"); return this.session.session_authenticate(db, login, password).pipe(function() { if (self.has_local_storage) { if(self.remember_credentials) { @@ -547,13 +593,102 @@ instance.web.Login = instance.web.Widget.extend({ localStorage.setItem('last_password_login_success', ''); } } - self.$(".oe_login_pane").fadeOut("slow"); - self.trigger("login"); - },function () { - self.$element.addClass("oe_login_invalid"); + self.trigger('login_successful'); + }, function () { + self.$(".oe_login_pane").fadeIn("fast", function() { + self.show_error("Invalid username or password"); + }); }); + }, + show_error: function(message) { + this.$el.addClass("oe_login_invalid"); + this.$(".oe_login_error_message").text(message); + }, + hide_error: function() { + this.$el.removeClass('oe_login_invalid'); + }, +}); +instance.web.client_actions.add("login", "instance.web.Login"); + +/** + * Client action to reload the whole interface. + * If params has an entry 'menu_id', it opens the given menu entry. + */ +instance.web.Reload = instance.web.Widget.extend({ + init: function(parent, params) { + this._super(parent); + this.menu_id = (params && params.menu_id) || false; + }, + start: function() { + var l = window.location; + + var sobj = $.deparam(l.search.substr(1)); + sobj.ts = new Date().getTime(); + var search = '?' + $.param(sobj); + + var hash = l.hash; + if (this.menu_id) { + hash = "#menu_id=" + this.menu_id; + } + var url = l.protocol + "//" + l.host + l.pathname + search + hash; + window.location = url; } }); +instance.web.client_actions.add("reload", "instance.web.Reload"); + +/** + * Client action to go back in breadcrumb history. + * If can't go back in history stack, will go back to home. + */ +instance.web.HistoryBack = instance.web.Widget.extend({ + init: function(parent, params) { + if (!parent.history_back()) { + window.location = '/' + (window.location.search || ''); + } + } +}); +instance.web.client_actions.add("history_back", "instance.web.HistoryBack"); + +/** + * Client action to go back home. + */ +instance.web.Home = instance.web.Widget.extend({ + init: function(parent, params) { + window.location = '/' + (window.location.search || ''); + } +}); +instance.web.client_actions.add("home", "instance.web.Home"); + +instance.web.ChangePassword = instance.web.Widget.extend({ + template: "ChangePassword", + start: function() { + var self = this; + self.$el.validate({ + submitHandler: function (form) { + self.rpc("/web/session/change_password",{ + 'fields': $(form).serializeArray() + }, function(result) { + if (result.error) { + self.display_error(result); + return; + } else { + instance.webclient.on_logout(); + } + }); + } + }); + }, + display_error: function (error) { + return instance.web.dialog($('<div>'), { + modal: true, + title: error.title, + buttons: [ + {text: _t("Ok"), click: function() { $(this).dialog("close"); }} + ] + }).html(error.error); + }, +}) +instance.web.client_actions.add("change_password", "instance.web.ChangePassword"); instance.web.Menu = instance.web.Widget.extend({ template: 'Menu', @@ -565,7 +700,8 @@ instance.web.Menu = instance.web.Widget.extend({ }, start: function() { this._super.apply(this, arguments); - this.$secondary_menus = this.getParent().$element.find('.oe_secondary_menus_container'); + this.$secondary_menus = this.getParent().$el.find('.oe_secondary_menus_container'); + this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click); return this.do_reload(); }, do_reload: function() { @@ -576,13 +712,13 @@ instance.web.Menu = instance.web.Widget.extend({ this.data = data; this.renderElement(); this.limit_entries(); + // Hide toplevel item if there is only one + var $toplevel = this.$("li") + if($toplevel.length == 1) { + $toplevel.hide(); + } this.$secondary_menus.html(QWeb.render("Menu.secondary", { widget : this })); - this.$element.on('click', 'a.oe_menu_more_link', function() { - self.$element.find('.oe_menu_more').toggle(); - return false; - }); - this.$element.on('click', 'a[data-menu]', this.on_menu_click); - this.$secondary_menus.on('click', 'a[data-menu]', this.on_menu_click); + this.$el.on('click', 'a[data-menu]', this.on_menu_click); // Hide second level submenus this.$secondary_menus.find('.oe_menu_toggler').siblings('.oe_secondary_submenu').hide(); if (self.current_menu) { @@ -597,20 +733,17 @@ instance.web.Menu = instance.web.Widget.extend({ } if (maximum_visible_links < this.data.data.children.length) { var $more = $(QWeb.render('Menu.more')), - $index = this.$element.find('li').eq(maximum_visible_links - 1); + $index = this.$el.find('li').eq(maximum_visible_links - 1); $index.after($more); + //$('.oe_topbar').append($more); $more.find('.oe_menu_more').append($index.next().nextAll()); } - this.do_hide_more(); }, auto_limit_entries: function() { // TODO: auto detect overflow and bind window on resize var width = $(window).width(); return Math.floor(width / 125); }, - do_hide_more: function() { - this.$element.find('.oe_menu_more').hide(); - }, /** * Opens a given menu by id, as if a user had browsed to that menu by hand * except does not trigger any event on the way @@ -618,20 +751,22 @@ instance.web.Menu = instance.web.Widget.extend({ * @param {Number} id database id of the terminal menu to select */ open_menu: function (id) { + this.current_menu = id; + this.session.active_id = id; var $clicked_menu, $sub_menu, $main_menu; - $clicked_menu = this.$element.add(this.$secondary_menus).find('a[data-menu=' + id + ']'); + $clicked_menu = this.$el.add(this.$secondary_menus).find('a[data-menu=' + id + ']'); this.trigger('open_menu', id, $clicked_menu); if (this.$secondary_menus.has($clicked_menu).length) { $sub_menu = $clicked_menu.parents('.oe_secondary_menu'); - $main_menu = this.$element.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']'); + $main_menu = this.$el.find('a[data-menu=' + $sub_menu.data('menu-parent') + ']'); } else { $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + $clicked_menu.attr('data-menu') + ']'); $main_menu = $clicked_menu; } // Activate current main menu - this.$element.find('.oe_active').removeClass('oe_active'); + this.$el.find('.oe_active').removeClass('oe_active'); $main_menu.addClass('oe_active'); // Show current sub menu @@ -658,7 +793,7 @@ instance.web.Menu = instance.web.Widget.extend({ * @param {Number} id the action_id to match */ open_action: function (id) { - var $menu = this.$element.add(this.$secondary_menus).find('a[data-action-id=' + id + ']'); + var $menu = this.$el.add(this.$secondary_menus).find('a[data-action-id="' + id + '"]'); var menu_id = $menu.data('menu'); if (menu_id) { this.open_menu(menu_id); @@ -668,34 +803,37 @@ instance.web.Menu = instance.web.Widget.extend({ * Process a click on a menu item * * @param {Number} id the menu_id + * @param {Boolean} [needaction=false] whether the triggered action should execute in a `needs action` context */ - menu_click: function(id) { - if (id) { - this.do_hide_more(); - // find back the menuitem in dom to get the action - var $item = this.$element.find('a[data-menu=' + id + ']'); - if (!$item.length) { - $item = this.$secondary_menus.find('a[data-menu=' + id + ']'); - } - var action_id = $item.data('action-id'); - // If first level menu doesnt have action trigger first leaf - if (!action_id) { - if(this.$element.has($item).length) { - $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']'); - $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]'); - if($items.length) { - action_id = $items.data('action-id'); - id = $items.data('menu'); - } + menu_click: function(id, needaction) { + if (!id) { return; } + + // find back the menuitem in dom to get the action + var $item = this.$el.find('a[data-menu=' + id + ']'); + if (!$item.length) { + $item = this.$secondary_menus.find('a[data-menu=' + id + ']'); + } + var action_id = $item.data('action-id'); + // If first level menu doesnt have action trigger first leaf + if (!action_id) { + if(this.$el.has($item).length) { + var $sub_menu = this.$secondary_menus.find('.oe_secondary_menu[data-menu-parent=' + id + ']'); + var $items = $sub_menu.find('a[data-action-id]').filter('[data-action-id!=""]'); + if($items.length) { + action_id = $items.data('action-id'); + id = $items.data('menu'); } } - this.open_menu(id); - this.current_menu = id; - this.session.active_id = id; - if (action_id) { - this.trigger('menu_click', action_id, id, $item); - } } + if (action_id) { + this.trigger('menu_click', { + action_id: action_id, + needaction: needaction, + id: id, + previous_menu_id: this.current_menu // Here we don't know if action will fail (in which case we have to revert menu) + }, $item); + } + this.open_menu(id); }, /** * Jquery event handler for menu click @@ -703,9 +841,9 @@ instance.web.Menu = instance.web.Widget.extend({ * @param {Event} ev the jquery event */ on_menu_click: function(ev) { - this.menu_click($(ev.currentTarget).data('menu')); - ev.stopPropagation(); - return false; + ev.preventDefault(); + var needaction = $(ev.target).is('div.oe_menu_counter'); + this.menu_click($(ev.currentTarget).data('menu'), needaction); }, }); @@ -718,69 +856,30 @@ instance.web.UserMenu = instance.web.Widget.extend({ start: function() { var self = this; this._super.apply(this, arguments); - $('html').bind('click', function() { - self.$element.find('.oe_dropdown_options').hide(); - }); - this.$element.find('.oe_dropdown_toggle').click(function() { - self.$element.find('.oe_dropdown_options').toggle(); - return false; - }); - this.$element.on('click', '.oe_dropdown_options li a[data-menu]', function() { + this.$el.on('click', '.oe_dropdown_menu li a[data-menu]', function(ev) { + ev.preventDefault(); var f = self['on_menu_' + $(this).data('menu')]; if (f) { f($(this)); } - self.$element.find('.oe_dropdown_options').hide(); - return false; }); }, - change_password :function() { - var self = this; - this.dialog = new instance.web.Dialog(this, { - title: _t("Change Password"), - width : 'auto' - }).open(); - this.dialog.$element.html(QWeb.render("UserMenu.password", self)); - this.dialog.$element.find("form[name=change_password_form]").validate({ - submitHandler: function (form) { - self.rpc("/web/session/change_password",{ - 'fields': $(form).serializeArray() - }, function(result) { - if (result.error) { - self.display_error(result); - return; - } else { - instance.webclient.on_logout(); - } - }); - } - }); - }, - display_error: function (error) { - return instance.web.dialog($('<div>'), { - modal: true, - title: error.title, - buttons: [ - {text: _("Ok"), click: function() { $(this).dialog("close"); }} - ] - }).html(error.error); - }, do_update: function () { var self = this; var fct = function() { - var $avatar = self.$element.find('.oe_topbar_avatar'); + var $avatar = self.$el.find('.oe_topbar_avatar'); $avatar.attr('src', $avatar.data('default-src')); if (!self.session.uid) return; var func = new instance.web.Model("res.users").get_func("read"); return func(self.session.uid, ["name", "company_id"]).pipe(function(res) { var topbar_name = res.name; - if(instance.connection.debug) - topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.connection.db); + if(instance.session.debug) + topbar_name = _.str.sprintf("%s (%s)", topbar_name, instance.session.db); if(res.company_id[0] > 1) topbar_name = _.str.sprintf("%s (%s)", topbar_name, res.company_id[1]); - self.$element.find('.oe_topbar_name').text(topbar_name); - var avatar_src = _.str.sprintf('%s/web/binary/image?session_id=%s&model=res.users&field=avatar&id=%s', self.session.prefix, self.session.session_id, self.session.uid); + self.$el.find('.oe_topbar_name').text(topbar_name); + var avatar_src = _.str.sprintf('%s/web/binary/image?session_id=%s&model=res.users&field=image_small&id=%s', self.session.prefix, self.session.session_id, self.session.uid); $avatar.attr('src', avatar_src); }); }; @@ -792,44 +891,12 @@ instance.web.UserMenu = instance.web.Widget.extend({ }, on_menu_settings: function() { var self = this; - var action_manager = new instance.web.ActionManager(this); - var dataset = new instance.web.DataSet (this,'res.users',this.context); - dataset.call ('action_get','',function (result){ - self.rpc('/web/action/load', {action_id:result}, function(result){ - action_manager.do_action(_.extend(result['result'], { - target: 'inline', - res_id: self.session.uid, - res_model: 'res.users', - flags: { - action_buttons: false, - search_view: false, - sidebar: false, - views_switcher: false, - pager: false - } - })); + if (!this.getParent().has_uncommitted_changes()) { + self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }, function(result) { + result.result.res_id = instance.session.uid; + self.getParent().action_manager.do_action(result.result); }); - }); - this.dialog = new instance.web.Dialog(this,{ - title: _t("Preferences"), - width: '700px', - buttons: [ - {text: _t("Change password"), click: function(){ self.change_password(); }}, - {text: _t("Cancel"), click: function(){ $(this).dialog('destroy'); }}, - {text: _t("Save"), click: function(){ - var inner_viewmanager = action_manager.inner_viewmanager; - inner_viewmanager.views[inner_viewmanager.active_view].controller.do_save() - .then(function() { - self.dialog.destroy(); - // needs to refresh interface in case language changed - window.location.reload(); - }); - } - } - ] - }).open(); - action_manager.appendTo(this.dialog.$element); - action_manager.renderElement(this.dialog); + } }, on_menu_about: function() { var self = this; @@ -841,45 +908,111 @@ instance.web.UserMenu = instance.web.Widget.extend({ window.location.href, 'debug'); }); instance.web.dialog($help, {autoOpen: true, - modal: true, width: 960, title: _t("About")}); + modal: true, width: 507, height: 290, resizable: false, title: _t("About")}); }); }, }); -instance.web.WebClient = instance.web.Widget.extend({ - init: function(parent) { +instance.web.Client = instance.web.Widget.extend({ + init: function(parent, origin) { + instance.client = instance.webclient = this; + this._super(parent); + this.origin = origin; + }, + start: function() { + var self = this; + return instance.session.session_bind(this.origin).pipe(function() { + var $e = $(QWeb.render(self._template, {})); + self.replaceElement($e); + self.bind_events(); + return self.show_common(); + }); + }, + bind_events: function() { + var self = this; + this.$el.on('mouseenter', '.oe_systray > div:not([data-tipsy=true])', function() { + $(this).attr('data-tipsy', 'true').tipsy().trigger('mouseenter'); + }); + this.$el.on('click', '.oe_dropdown_toggle', function(ev) { + ev.preventDefault(); + var $toggle = $(this); + var $menu = $toggle.siblings('.oe_dropdown_menu'); + $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu'); + var state = $menu.is('.oe_opened'); + setTimeout(function() { + // Do not alter propagation + $toggle.add($menu).toggleClass('oe_opened', !state); + if (!state) { + // Move $menu if outside window's edge + var doc_width = $(document).width(); + var offset = $menu.offset(); + var menu_width = $menu.width(); + var x = doc_width - offset.left - menu_width - 2; + if (x < 0) { + $menu.offset({ left: offset.left + x }).width(menu_width); + } + } + }, 0); + }); + instance.web.bus.on('click', this, function(ev) { + $.fn.tipsy.clear(); + if (!$(ev.target).is('input[type=file]')) { + self.$el.find('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened'); + } + }); + }, + show_common: function() { var self = this; + this.crashmanager = new instance.web.CrashManager(); + instance.session.on_rpc_error.add(this.crashmanager.on_rpc_error); + self.notification = new instance.web.Notification(this); + self.notification.appendTo(self.$el); + self.loading = new instance.web.Loading(self); + self.loading.appendTo(self.$el); + self.action_manager = new instance.web.ActionManager(self); + self.action_manager.appendTo(self.$('.oe_application')); + }, + toggle_bars: function(value) { + this.$('tr:has(td.oe_topbar),.oe_leftbar').toggle(value); + }, + has_uncommitted_changes: function() { + return false; + }, +}); + +instance.web.WebClient = instance.web.Client.extend({ + _template: 'WebClient', + init: function(parent) { this._super(parent); - instance.webclient = this; - this.querystring = '?' + jQuery.param.querystring(); this._current_state = null; }, start: function() { var self = this; - this.$element.addClass("openerp openerp-web-client-container"); - if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) { - $("body").addClass("kitten-mode-activated"); - self.$element.delegate('img.oe-record-edit-link-img', 'hover', function(e) { - self.$element.toggleClass('clark-gable'); + return $.when(this._super()).pipe(function() { + self.$el.on('click', '.oe_logo', function() { + self.action_manager.do_action('home'); }); - } - this.session.session_bind().then(function() { - self.destroy_content(); - self.show_common(); + if (jQuery.param !== undefined && jQuery.deparam(jQuery.param.querystring()).kitten !== undefined) { + $("body").addClass("kitten-mode-activated"); + if ($.blockUI) { + $.blockUI.defaults.message = '<img src="http://www.amigrave.com/kitten.gif">'; + } + } if (!self.session.session_is_valid()) { self.show_login(); } else { self.show_application(); } }); - this.$element.on('mouseenter', '.oe_systray > div:not([data-tipsy=true])', function() { - $(this).attr('data-tipsy', 'true').tipsy().trigger('mouseenter'); - }); + }, + set_title: function(title) { + title = _.str.clean(title); + var sep = _.isEmpty(title) ? '' : ' - '; + document.title = title + sep + 'OpenERP'; }, show_common: function() { var self = this; - this.crashmanager = new instance.web.CrashManager(); - instance.connection.on_rpc_error.add(this.crashmanager.on_rpc_error); + this._super(); window.onerror = function (message, file, line) { self.crashmanager.on_traceback({ type: _t("Client Error"), @@ -887,49 +1020,50 @@ instance.web.WebClient = instance.web.Widget.extend({ data: {debug: file + ':' + line} }); }; - self.notification = new instance.web.Notification(this); - self.notification.appendTo(self.$element); - self.loading = new instance.web.Loading(self); - self.loading.appendTo(self.$element); - self.login = new instance.web.Login(self); - self.login.on("login",self,self.show_application); - self.$table = $(QWeb.render("WebClient", {})); - self.action_manager = new instance.web.ActionManager(self); - self.action_manager.appendTo(self.$table.find('.oe_application')); }, show_login: function() { - var self = this; - self.login.appendTo(self.$element); + this.toggle_bars(false); + + var action = { + 'type': 'ir.actions.client', + 'tag': 'login' + }; + var state = $.bbq.getState(true); + if (state.action === "login") { + action.params = state; + } + + this.action_manager.do_action(action); + this.action_manager.inner_widget.on('login_successful', this, function() { + this.do_push_state(state); + this._current_state = null; // ensure the state will be loaded + this.show_application(); // will load the state we just pushed + }); }, show_application: function() { var self = this; - self.$element.append(self.$table); - self.login.$element.hide(); + self.toggle_bars(true); self.menu = new instance.web.Menu(self); - self.menu.replace(this.$element.find('.oe_menu_placeholder')); + self.menu.replace(this.$el.find('.oe_menu_placeholder')); self.menu.on('menu_click', this, this.on_menu_action); self.user_menu = new instance.web.UserMenu(self); - self.user_menu.replace(this.$element.find('.oe_user_menu_placeholder')); + self.user_menu.replace(this.$el.find('.oe_user_menu_placeholder')); self.user_menu.on_menu_logout.add(this.proxy('on_logout')); self.user_menu.on_action.add(this.proxy('on_menu_action')); self.user_menu.do_update(); self.bind_hashchange(); - var version_label = _t("OpenERP - Unsupported/Community Version"); - if (!self.session.openerp_entreprise) { - self.$element.find('.oe_footer_powered').append(_.str.sprintf('<span> - <a href="http://www.openerp.com/support-or-publisher-warranty-contract" target="_blank">%s</a></span>', version_label)); - document.title = version_label; - } + self.set_title(); }, destroy_content: function() { _.each(_.clone(this.getChildren()), function(el) { el.destroy(); }); - this.$element.children().remove(); + this.$el.children().remove(); }, do_reload: function() { var self = this; return this.session.session_reload().pipe(function () { - instance.connection.load_modules(true).pipe( + instance.session.load_modules(true).pipe( self.menu.proxy('do_reload')); }); }, @@ -943,28 +1077,28 @@ instance.web.WebClient = instance.web.Widget.extend({ }, on_logout: function() { var self = this; - this.session.session_logout().then(function () { - $(window).unbind('hashchange', self.on_hashchange); - self.do_push_state({}); - //would be cool to be able to do this, but I think it will make addons do strange things - //this.show_login(); - window.location.reload(); - }); + if (!this.has_uncommitted_changes()) { + this.session.session_logout().then(function () { + $(window).unbind('hashchange', self.on_hashchange); + self.do_push_state({}); + window.location.reload(); + }); + } }, bind_hashchange: function() { var self = this; $(window).bind('hashchange', this.on_hashchange); var state = $.bbq.getState(true); - if (! _.isEmpty(state)) { - $(window).trigger('hashchange'); - } else { + if (_.isEmpty(state) || state.action == "login") { self.menu.has_been_loaded.then(function() { - var first_menu_id = self.menu.$element.find("a:first").data("menu"); + var first_menu_id = self.menu.$el.find("a:first").data("menu"); if(first_menu_id) { self.menu.menu_click(first_menu_id); } }); + } else { + $(window).trigger('hashchange'); } }, on_hashchange: function(event) { @@ -984,12 +1118,24 @@ instance.web.WebClient = instance.web.Widget.extend({ this._current_state = state; }, do_push_state: function(state) { + this.set_title(state.title); + delete state.title; var url = '#' + $.param(state); this._current_state = _.clone(state); $.bbq.pushState(url); }, - on_menu_action: function(action) { - this.action_manager.do_action(action); + on_menu_action: function(options) { + var self = this; + return this.rpc("/web/action/load", { action_id: options.action_id }) + .pipe(function (result) { + var action = result.result; + if (options.needaction) { + action.context.search_default_needaction_pending = true; + } + return $.when(self.action_manager.do_action(action, null, true)).fail(function() { + self.menu.open_menu(options.previous_menu_id); + }); + }); }, do_action: function(action) { var self = this; @@ -1001,38 +1147,55 @@ instance.web.WebClient = instance.web.Widget.extend({ } }, set_content_full_screen: function(fullscreen) { - if (fullscreen) - $(".oe_webclient", this.$element).addClass("oe_content_full_screen"); - else - $(".oe_webclient", this.$element).removeClass("oe_content_full_screen"); - } + if (fullscreen) { + $(".oe_webclient", this.$el).addClass("oe_content_full_screen"); + $("body").css({'overflow-y':'hidden'}); + } else { + $(".oe_webclient", this.$el).removeClass("oe_content_full_screen"); + $("body").css({'overflow-y':'scroll'}); + } + }, + has_uncommitted_changes: function() { + var $e = $.Event('clear_uncommitted_changes'); + instance.web.bus.trigger('clear_uncommitted_changes', $e); + if ($e.isDefaultPrevented()) { + return true; + } else { + return this._super.apply(this, arguments); + } + }, }); -instance.web.EmbeddedClient = instance.web.Widget.extend({ - template: 'EmptyComponent', - init: function(parent, action_id, options) { - this._super(parent); - // TODO take the xmlid of a action instead of its id +instance.web.EmbeddedClient = instance.web.Client.extend({ + _template: 'EmbedClient', + init: function(parent, origin, dbname, login, key, action_id, options) { + this._super(parent, origin); + + this.dbname = dbname; + this.login = login; + this.key = key; this.action_id = action_id; this.options = options || {}; - this.am = new instance.web.ActionManager(this); }, start: function() { var self = this; - this.am.appendTo(this.$element.addClass('openerp')); - return this.rpc("/web/action/load", { action_id: this.action_id }, function(result) { - var action = result.result; - action.flags = _.extend({ - //views_switcher : false, - search_view : false, - action_buttons : false, - sidebar : false - //pager : false - }, self.options, action.flags || {}); + return $.when(this._super()).pipe(function() { + return instance.session.session_authenticate(self.dbname, self.login, self.key, true).pipe(function() { + return self.rpc("/web/action/load", { action_id: self.action_id }, function(result) { + var action = result.result; + action.flags = _.extend({ + //views_switcher : false, + search_view : false, + action_buttons : false, + sidebar : false + //pager : false + }, self.options, action.flags || {}); - self.am.do_action(action); + self.action_manager.do_action(action); + }); + }); }); - } + }, }); instance.web.embed = function (origin, dbname, login, key, action, options) { @@ -1046,13 +1209,8 @@ instance.web.embed = function (origin, dbname, login, key, action, options) { var sc = document.getElementsByTagName('script'); currentScript = sc[sc.length-1]; } - instance.connection.session_bind(origin).then(function () { - instance.connection.session_authenticate(dbname, login, key, true).then(function () { - var client = new instance.web.EmbeddedClient(null, action, options); - client.insertAfter(currentScript); - }); - }); - + var client = new instance.web.EmbeddedClient(null, origin, dbname, login, key, action, options); + client.insertAfter(currentScript); }; }; diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js index 94c748f839c..62ac48f0c61 100644 --- a/addons/web/static/src/js/corelib.js +++ b/addons/web/static/src/js/corelib.js @@ -99,7 +99,6 @@ openerp.web.corelib = function(instance) { for (var name in prop) { // Check if we're overwriting an existing function prototype[name] = typeof prop[name] == "function" && - typeof _super[name] == "function" && fnTest.test(prop[name]) ? (function(name, fn) { return function() { @@ -379,7 +378,8 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, { instance.web.EventDispatcherMixin.init.call(this); this.__getterSetterInternalMap = {}; }, - set: function(map) { + set: function(map, options) { + options = options || {}; var self = this; var changed = false; _.each(map, function(val, key) { @@ -388,10 +388,11 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, { return; changed = true; self.__getterSetterInternalMap[key] = val; - self.trigger("change:" + key, self, { - oldValue: tmp, - newValue: val - }); + if (! options.silent) + self.trigger("change:" + key, self, { + oldValue: tmp, + newValue: val + }); }); if (changed) self.trigger("change", self); @@ -490,24 +491,20 @@ instance.web.CallbackEnabledMixin = _.extend({}, instance.web.PropertiesMixin, { * * The semantics of this precisely replace closing over the method call. * - * @param {String} method_name name of the method to invoke + * @param {String|Function} method function or name of the method to invoke * @returns {Function} proxied method */ - proxy: function (method_name) { + proxy: function (method) { var self = this; return function () { - return self[method_name].apply(self, arguments); + var fn = (typeof method === 'string') ? self[method] : method; + return fn.apply(self, arguments); } } }); instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { /** - * Tag name when creating a default $element. - * @type string - */ - tagName: 'div', - /** * Constructs the widget and sets its parent if a parent is given. * * @constructs instance.web.Widget @@ -516,14 +513,9 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance. * When that widget is destroyed by calling destroy(), the current instance will be * destroyed too. Can be null. - * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want - * to bind the current Widget to an already existing part of the DOM, which is not compatible - * with the DOM insertion methods provided by the current implementation of Widget. So - * for new components this argument should not be provided any more. */ init: function(parent) { instance.web.CallbackEnabledMixin.init.call(this); - this.$element = $(document.createElement(this.tagName)); this.setParent(parent); }, /** @@ -533,8 +525,8 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { _.each(this.getChildren(), function(el) { el.destroy(); }); - if(this.$element != null) { - this.$element.remove(); + if(this.$el) { + this.$el.remove(); } instance.web.PropertiesMixin.destroy.call(this); }, @@ -546,7 +538,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { appendTo: function(target) { var self = this; return this.__widgetRenderAndInsert(function(t) { - self.$element.appendTo(t); + self.$el.appendTo(t); }, target); }, /** @@ -557,7 +549,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { prependTo: function(target) { var self = this; return this.__widgetRenderAndInsert(function(t) { - self.$element.prependTo(t); + self.$el.prependTo(t); }, target); }, /** @@ -568,7 +560,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { insertAfter: function(target) { var self = this; return this.__widgetRenderAndInsert(function(t) { - self.$element.insertAfter(t); + self.$el.insertAfter(t); }, target); }, /** @@ -579,7 +571,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { insertBefore: function(target) { var self = this; return this.__widgetRenderAndInsert(function(t) { - self.$element.insertBefore(t); + self.$el.insertBefore(t); }, target); }, /** @@ -589,7 +581,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { */ replace: function(target) { return this.__widgetRenderAndInsert(_.bind(function(t) { - this.$element.replaceAll(t); + this.$el.replaceAll(t); }, this), target); }, __widgetRenderAndInsert: function(insertion, target) { @@ -612,6 +604,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { * @returns {jQuery.Deferred} */ start: function() { + return $.when(); } }); @@ -647,8 +640,8 @@ instance.web.CallbackEnabled = instance.web.Class.extend(instance.web.CallbackEn * // stuff that you want to init before the rendering * }, * start: function() { - * // stuff you want to make after the rendering, `this.$element` holds a correct value - * this.$element.find(".my_button").click(/* an example of event binding * /); + * // stuff you want to make after the rendering, `this.$el` holds a correct value + * this.$el.find(".my_button").click(/* an example of event binding * /); * * // if you have some asynchronous operations, it's a good idea to return * // a promise in start() @@ -672,6 +665,12 @@ instance.web.CallbackEnabled = instance.web.Class.extend(instance.web.CallbackEn * That will kill the widget in a clean way and erase its content from the dom. */ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { + // Backbone-ish API + tagName: 'div', + id: null, + className: null, + attributes: {}, + events: {}, /** * The name of the QWeb template that will be used for rendering. Must be * redefined in subclasses or the default render() method can not be used. @@ -688,14 +687,12 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance. * When that widget is destroyed by calling destroy(), the current instance will be * destroyed too. Can be null. - * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want - * to bind the current Widget to an already existing part of the DOM, which is not compatible - * with the DOM insertion methods provided by the current implementation of Widget. So - * for new components this argument should not be provided any more. */ init: function(parent) { instance.web.WidgetMixin.init.call(this,parent); - this.session = instance.connection; + // FIXME: this should not be + this.setElement(this._make_descriptive()); + this.session = instance.session; }, /** * Renders the element. The default implementation renders the widget using QWeb, @@ -703,20 +700,120 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { * key that references `this`. */ renderElement: function() { - var rendered = null; - if (this.template) - rendered = instance.web.qweb.render(this.template, {widget: this}); - if (_.str.trim(rendered)) { - var elem = $(rendered); - this.$element.replaceWith(elem); - this.$element = elem; + var $el; + if (this.template) { + $el = $(_.str.trim(instance.web.qweb.render( + this.template, {widget: this}))); + } else { + $el = this._make_descriptive(); } + this.replaceElement($el); }, + /** - * Shortcut for $element.find() like backbone + * Re-sets the widget's root element and replaces the old root element + * (if any) by the new one in the DOM. + * + * @param {HTMLElement | jQuery} $el + * @returns {*} this */ - "$": function() { - return this.$element.find.apply(this.$element,arguments); + replaceElement: function ($el) { + var $oldel = this.$el; + this.setElement($el); + if ($oldel && !$oldel.is(this.$el)) { + $oldel.replaceWith(this.$el); + } + return this; + }, + /** + * Re-sets the widget's root element (el/$el/$el). + * + * Includes: + * * re-delegating events + * * re-binding sub-elements + * * if the widget already had a root element, replacing the pre-existing + * element in the DOM + * + * @param {HTMLElement | jQuery} element new root element for the widget + * @return {*} this + */ + setElement: function (element) { + // NB: completely useless, as WidgetMixin#init creates a $el + // always + if (this.$el) { + this.undelegateEvents(); + } + + this.$el = (element instanceof $) ? element : $(element); + this.el = this.$el[0]; + + this.delegateEvents(); + + return this; + }, + /** + * Utility function to build small DOM elements. + * + * @param {String} tagName name of the DOM element to create + * @param {Object} [attributes] map of DOM attributes to set on the element + * @param {String} [content] HTML content to set on the element + * @return {Element} + */ + make: function (tagName, attributes, content) { + var el = document.createElement(tagName); + if (!_.isEmpty(attributes)) { + $(el).attr(attributes); + } + if (content) { + $(el).html(content); + } + return el; + }, + /** + * Makes a potential root element from the declarative builder of the + * widget + * + * @return {jQuery} + * @private + */ + _make_descriptive: function () { + var attrs = _.extend({}, this.attributes || {}); + if (this.id) { attrs.id = this.id; } + if (this.className) { attrs['class'] = this.className; } + return $(this.make(this.tagName, attrs)); + }, + delegateEvents: function () { + var events = this.events; + if (_.isEmpty(events)) { return; } + + for(var key in events) { + if (!events.hasOwnProperty(key)) { continue; } + + var method = this.proxy(events[key]); + + var match = /^(\S+)(\s+(.*))?$/.exec(key); + var event = match[1]; + var selector = match[3]; + + event += '.widget_events'; + if (!selector) { + this.$el.on(event, method); + } else { + this.$el.on(event, selector, method); + } + } + }, + undelegateEvents: function () { + this.$el.off('.widget_events'); + }, + /** + * Shortcut for ``this.$el.find(selector)`` + * + * @param {String} selector CSS selector, rooted in $el + * @returns {jQuery} selector match + */ + $: function(selector) { + return this.$el.find(selector); }, /** * Informs the action manager to do an action. This supposes that @@ -744,7 +841,7 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { rpc: function(url, data, success, error) { var def = $.Deferred().then(success, error); var self = this; - instance.connection.rpc(url, data). then(function() { + instance.session.rpc(url, data). then(function() { if (!self.isDestroyed()) def.resolve.apply(def, arguments); }, function() { @@ -766,7 +863,7 @@ instance.web.Registry = instance.web.Class.extend({ * * An object path is simply a dotted name from the instance root to the * object pointed to (e.g. ``"instance.web.Session"`` for an OpenERP - * connection object). + * session object). * * @constructs instance.web.Registry * @param {Object} mapping a mapping of keys to object-paths @@ -875,6 +972,11 @@ instance.web.Registry = instance.web.Class.extend({ }); instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ + triggers: { + 'request': 'Request sent', + 'response': 'Response received', + 'error': 'HTTP Error response or timeout received', + }, /** * @constructs instance.web.JsonRPC * @extends instance.web.CallbackEnabled @@ -1038,7 +1140,8 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ uid: new py.float(this.uid), datetime: datetime, time: time, - relativedelta: relativedelta + relativedelta: relativedelta, + current_date: date.today.__call__().strftime(['%Y-%m-%d']) }; }, /** @@ -1212,12 +1315,12 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ id: _.uniqueId('r') }; var deferred = $.Deferred(); - this.on_rpc_request(); + this.trigger('request', url, payload); var aborter = params.aborter; delete params.aborter; var request = this.rpc_function(url, payload).then( function (response, textStatus, jqXHR) { - self.on_rpc_response(); + self.trigger('response', response); if (!response.error) { if (url.url === '/web/session/eval_domain_and_context') { self.test_eval(params, response.result); @@ -1225,18 +1328,12 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ deferred.resolve(response["result"], textStatus, jqXHR); } else if (response.error.data.type === "session_invalid") { self.uid = false; - // TODO deprecate or use a deferred on login.do_ask_login() - self.on_session_invalid(function() { - self.rpc(url, payload.params, - function() { deferred.resolve.apply(deferred, arguments); }, - function() { deferred.reject.apply(deferred, arguments); }); - }); } else { deferred.reject(response.error, $.Event()); } }, function(jqXHR, textStatus, errorThrown) { - self.on_rpc_response(); + self.trigger('error'); var error = { code: -32098, message: "XmlHttpRequestError " + errorThrown, @@ -1279,7 +1376,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ processData: false }, url); if (this.synch) - ajax.async = false; + ajax.async = false; return $.ajax(ajax); }, rpc_jsonp: function(url, payload) { @@ -1298,7 +1395,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ data: data }, url); if (this.synch) - ajax.async = false; + ajax.async = false; var payload_str = JSON.stringify(payload); var payload_url = $.param({r:payload_str}); if(payload_url.length < 2000) { @@ -1308,19 +1405,19 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ } else { // Indirect jsonp request var ifid = _.uniqueId('oe_rpc_iframe'); - var display = options.openerp.debug ? 'block' : 'none'; + var display = self.debug ? 'block' : 'none'; var $iframe = $(_.str.sprintf("<iframe src='javascript:false;' name='%s' id='%s' style='display:%s'></iframe>", ifid, ifid, display)); var $form = $('<form>') .attr('method', 'POST') .attr('target', ifid) .attr('enctype', "multipart/form-data") - .attr('action', ajax.url + '?' + $.param(data)) + .attr('action', ajax.url + '?jsonp=1&' + $.param(data)) .append($('<input type="hidden" name="r" />').attr('value', payload_str)) .hide() .appendTo($('body')); var cleanUp = function() { if ($iframe) { - $iframe.unbind("load").attr("src", "javascript:false;").remove(); + $iframe.unbind("load").remove(); } $form.remove(); }; @@ -1344,10 +1441,6 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ return deferred; } }, - on_rpc_request: function() { - }, - on_rpc_response: function() { - }, on_rpc_error: function(error) { }, get_url: function (file) { diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js index 4defde54a3c..ea160d6faee 100644 --- a/addons/web/static/src/js/coresetup.js +++ b/addons/web/static/src/js/coresetup.js @@ -11,33 +11,6 @@ if (!console.debug) { openerp.web.coresetup = function(instance) { -/** - * @deprecated use :class:`instance.web.Widget` - */ -instance.web.OldWidget = instance.web.Widget.extend({ - init: function(parent, element_id) { - this._super(parent); - this.element_id = element_id; - this.element_id = this.element_id || _.uniqueId('widget-'); - var tmp = document.getElementById(this.element_id); - this.$element = tmp ? $(tmp) : $(document.createElement(this.tagName)); - }, - renderElement: function() { - var rendered = this.render(); - if (rendered) { - var elem = $(rendered); - this.$element.replaceWith(elem); - this.$element = elem; - } - return this; - }, - render: function (additional) { - if (this.template) - return instance.web.qweb.render(this.template, _.extend({widget: this}, additional || {})); - return null; - } -}); - /** Session openerp specific RPC class */ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Session# */{ init: function() { @@ -62,7 +35,6 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess this.username = false; this.user_context= {}; this.db = false; - this.openerp_entreprise = false; this.module_list = instance._modules.slice(); this.module_loaded = {}; _(this.module_list).each(function (mod) { @@ -105,8 +77,7 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess db: result.db, username: result.login, uid: result.uid, - user_context: result.context, - openerp_entreprise: result.openerp_entreprise + user_context: result.context }); }); }, @@ -130,8 +101,7 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess db: result.db, username: result.login, uid: result.uid, - user_context: result.context, - openerp_entreprise: result.openerp_entreprise + user_context: result.context }); if (!_volatile) { self.set_cookie('session_id', self.session_id); @@ -143,20 +113,6 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess this.set_cookie('session_id', ''); return this.rpc("/web/session/destroy", {}); }, - on_session_valid: function() { - }, - /** - * Called when a rpc call fail due to an invalid session. - * By default, it's a noop - */ - on_session_invalid: function(retry_callback) { - }, - /** - * Fetches a cookie stored by an openerp session - * - * @private - * @param name the cookie's name - */ get_cookie: function (name) { if (!this.name) { return null; } var nameEQ = this.name + '|' + name + '='; @@ -190,9 +146,8 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess /** * Load additional web addons of that instance and init them * - * @param {Boolean} [no_session_valid_signal=false] prevents load_module from triggering ``on_session_valid``. */ - load_modules: function(no_session_valid_signal) { + load_modules: function() { var self = this; return this.rpc('/web/session/modules', {}).pipe(function(result) { var lang = self.user_context.lang, @@ -201,31 +156,31 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess var to_load = _.difference(result, self.module_list).join(','); self.module_list = all_modules; - var loaded = $.Deferred().resolve().promise(); - if (to_load.length) { + var loaded = self.rpc('/web/webclient/translations', params).then(function(trans) { + instance.web._t.database.set_bundle(trans); + }); + var file_list = ["/web/static/lib/datejs/globalization/" + lang.replace("_", "-") + ".js"]; + if(to_load.length) { loaded = $.when( - self.rpc('/web/webclient/csslist', {mods: to_load}, self.do_load_css), + loaded, + self.rpc('/web/webclient/csslist', {mods: to_load}).then(self.do_load_css), self.rpc('/web/webclient/qweblist', {mods: to_load}).pipe(self.do_load_qweb), - self.rpc('/web/webclient/translations', params).pipe(function(trans) { - instance.web._t.database.set_bundle(trans); - var file_list = ["/web/static/lib/datejs/globalization/" + lang.replace("_", "-") + ".js"]; - return self.rpc('/web/webclient/jslist', {mods: to_load}).pipe(function(files) { - return self.do_load_js(file_list.concat(files)); - }).then(function () { - if (!Date.CultureInfo.pmDesignator) { - // If no am/pm designator is specified but the openerp - // datetime format uses %i, date.js won't be able to - // correctly format a date. See bug#938497. - Date.CultureInfo.amDesignator = 'AM'; - Date.CultureInfo.pmDesignator = 'PM'; - } - }); - })) + self.rpc('/web/webclient/jslist', {mods: to_load}).then(function(files) { + file_list = file_list.concat(files); + }) + ); } - return loaded.then(function() { + return loaded.pipe(function () { + return self.do_load_js(file_list); + }).then(function() { self.on_modules_loaded(); - if (!no_session_valid_signal) { - self.on_session_valid(); + self.trigger('module_loaded'); + if (!Date.CultureInfo.pmDesignator) { + // If no am/pm designator is specified but the openerp + // datetime format uses %i, date.js won't be able to + // correctly format a date. See bug#938497. + Date.CultureInfo.amDesignator = 'AM'; + Date.CultureInfo.pmDesignator = 'PM'; } }); }); @@ -333,11 +288,14 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess .appendTo(document.body) .load(function () { try { - if (options.error) { - options.error(JSON.parse( - this.contentDocument.body.childNodes[1].textContent - )); - } + if (options.error) { + if (!this.contentDocument.body.childNodes[1]) { + options.error(this.contentDocument.body.childNodes); + } + else { + options.error(JSON.parse(this.contentDocument.body.childNodes[1].textContent)); + } + } } finally { complete(); } @@ -390,16 +348,41 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess timer = setTimeout(waitLoop, CHECK_INTERVAL); }, synchronized_mode: function(to_execute) { - var synch = this.synch; - this.synch = true; - try { - return to_execute(); - } finally { - this.synch = synch; - } + var synch = this.synch; + this.synch = true; + try { + return to_execute(); + } finally { + this.synch = synch; + } } }); + +/** + * Event Bus used to bind events scoped in the current instance + */ +instance.web.Bus = instance.web.Class.extend(instance.web.EventDispatcherMixin, { + init: function() { + instance.web.EventDispatcherMixin.init.call(this, parent); + var self = this; + // TODO fme: allow user to bind keys for some global actions. + // check gtk bindings + // http://unixpapa.com/js/key.html + _.each('click,dblclick,keydown,keypress,keyup'.split(','), function(evtype) { + $('html').on(evtype, function(ev) { + self.trigger(evtype, ev); + }); + }); + _.each('resize,scroll'.split(','), function(evtype) { + $(window).on(evtype, function(ev) { + self.trigger(evtype, ev); + }); + }); + } +}) +instance.web.bus = new instance.web.Bus(); + /** OpenERP Translations */ instance.web.TranslationDataBase = instance.web.Class.extend(/** @lends instance.web.TranslationDataBase# */{ /** @@ -454,64 +437,6 @@ instance.web.TranslationDataBase = instance.web.Class.extend(/** @lends instance } }); -/** Configure default qweb */ -instance.web._t = new instance.web.TranslationDataBase().build_translation_function(); -/** - * Lazy translation function, only performs the translation when actually - * printed (e.g. inserted into a template) - * - * Useful when defining translatable strings in code evaluated before the - * translation database is loaded, as class attributes or at the top-level of - * an OpenERP Web module - * - * @param {String} s string to translate - * @returns {Object} lazy translation object - */ -instance.web._lt = function (s) { - return {toString: function () { return instance.web._t(s); }} -}; -instance.web.qweb = new QWeb2.Engine(); -instance.web.qweb.debug = ($.deparam($.param.querystring()).debug != undefined); -instance.web.qweb.default_dict = { - '_' : _, - '_t' : instance.web._t -}; -instance.web.qweb.preprocess_node = function() { - // Note that 'this' is the Qweb Node - switch (this.node.nodeType) { - case 3: - case 4: - // Text and CDATAs - var translation = this.node.parentNode.attributes['t-translation']; - if (translation && translation.value === 'off') { - return; - } - var ts = _.str.trim(this.node.data); - if (ts.length === 0) { - return; - } - var tr = instance.web._t(ts); - if (tr !== ts) { - this.node.data = tr; - } - break; - case 1: - // Element - var attr, attrs = ['label', 'title', 'alt']; - while (attr = attrs.pop()) { - if (this.attributes[attr]) { - this.attributes[attr] = instance.web._t(this.attributes[attr]); - } - } - } -}; - -/** Configure blockui */ -if ($.blockUI) { - $.blockUI.defaults.baseZ = 1100; - $.blockUI.defaults.message = '<img src="/web/static/src/img/throbber2.gif">'; -} - /** Custom jQuery plugins */ $.fn.getAttributes = function() { var o = {}; @@ -541,10 +466,6 @@ $.Mutex = (function() { return Mutex; })(); -/** Setup default session */ -instance.connection = new instance.web.Session(); -instance.web.qweb.default_dict['__debug__'] = instance.connection.debug; - $.async_when = function() { var async = false; var def = $.Deferred(); @@ -574,38 +495,180 @@ $.async_when = function() { // special tweak for the web client var old_async_when = $.async_when; $.async_when = function() { - if (instance.connection.synch) - return $.when.apply(this, arguments); - else - return old_async_when.apply(this, arguments); + if (instance.session.synch) + return $.when.apply(this, arguments); + else + return old_async_when.apply(this, arguments); }; +/** Setup default session */ +instance.session = new instance.web.Session(); + +/** Configure default qweb */ +instance.web._t = new instance.web.TranslationDataBase().build_translation_function(); /** - * Registry for all the client actions key: tag value: widget + * Lazy translation function, only performs the translation when actually + * printed (e.g. inserted into a template) + * + * Useful when defining translatable strings in code evaluated before the + * translation database is loaded, as class attributes or at the top-level of + * an OpenERP Web module + * + * @param {String} s string to translate + * @returns {Object} lazy translation object */ -instance.web.client_actions = new instance.web.Registry(); +instance.web._lt = function (s) { + return {toString: function () { return instance.web._t(s); }} +}; +instance.web.qweb = new QWeb2.Engine(); +instance.web.qweb.default_dict['__debug__'] = instance.session.debug; // Which one ? +instance.web.qweb.debug = instance.session.debug; +instance.web.qweb.default_dict = { + '_' : _, + '_t' : instance.web._t +}; +instance.web.qweb.preprocess_node = function() { + // Note that 'this' is the Qweb Node + switch (this.node.nodeType) { + case 3: + case 4: + // Text and CDATAs + var translation = this.node.parentNode.attributes['t-translation']; + if (translation && translation.value === 'off') { + return; + } + var ts = _.str.trim(this.node.data); + if (ts.length === 0) { + return; + } + var tr = instance.web._t(ts); + if (tr !== ts) { + this.node.data = tr; + } + break; + case 1: + // Element + var attr, attrs = ['label', 'title', 'alt', 'placeholder']; + while (attr = attrs.pop()) { + if (this.attributes[attr]) { + this.attributes[attr] = instance.web._t(this.attributes[attr]); + } + } + } +}; -/** - * Client action to reload the whole interface. - * If params has an entry 'menu_id', it opens the given menu entry. +/** Setup jQuery timeago */ +var _t = instance.web._t; +/* + * Strings in timeago are "composed" with prefixes, words and suffixes. This + * makes their detection by our translating system impossible. Use all literal + * strings we're using with a translation mark here so the extractor can do its + * job. */ -instance.web.client_actions.add("reload", "instance.web.Reload"); +{ + _t('less than a minute ago'); + _t('about a minute ago'); + _t('%d minutes ago'); + _t('about an hour ago'); + _t('%d hours ago'); + _t('a day ago'); + _t('%d days ago'); + _t('about a month ago'); + _t('%d months ago'); + _t('about a year ago'); + _t('%d years ago'); +} -instance.web.Reload = instance.web.Widget.extend({ - init: function(parent, params) { - this._super(parent); - this.menu_id = (params && params.menu_id) || false; - }, +instance.session.on('module_loaded', this, function () { + // provide timeago.js with our own translator method + $.timeago.settings.translator = instance.web._t; +}); + +/** Setup blockui */ +if ($.blockUI) { + $.blockUI.defaults.baseZ = 1100; + $.blockUI.defaults.message = '<div class="openerp oe_blockui_spin_container" style="background-color: transparent;">'; + $.blockUI.defaults.css.border = '0'; + $.blockUI.defaults.css["background-color"] = ''; +} + +var messages_by_seconds = function() { + return [ + [0, _t("Loading...")], + [20, _t("Still loading...")], + [60, _t("Still loading...<br />Please be patient.")], + [120, _t("Don't leave yet,<br />it's still loading...")], + [300, _t("You may not believe it,<br />but the application is actually loading...")], + [420, _t("Take a minute to get a coffee,<br />because it's loading...")], + [600, _t("It's loading...<br />By the way, did you tried the kitten mode?")], + [3600, _t("Maybe you should consider pressing F5...")], + ]; +}; + +instance.web.Throbber = instance.web.Widget.extend({ + template: "Throbber", start: function() { - if (this.menu_id) { - // open the given menu id - var url_without_fragment = window.location.toString().split("#", 1)[0]; - window.location = url_without_fragment + "#menu_id=" + this.menu_id; - } else { - window.location.reload(); - } - } + var opts = { + lines: 13, // The number of lines to draw + length: 7, // The length of each line + width: 4, // The line thickness + radius: 10, // The radius of the inner circle + rotate: 0, // The rotation offset + color: '#FFF', // #rgb or #rrggbb + speed: 1, // Rounds per second + trail: 60, // Afterglow percentage + shadow: false, // Whether to render a shadow + hwaccel: false, // Whether to use hardware acceleration + className: 'spinner', // The CSS class to assign to the spinner + zIndex: 2e9, // The z-index (defaults to 2000000000) + top: 'auto', // Top position relative to parent in px + left: 'auto' // Left position relative to parent in px + }; + this.spin = new Spinner(opts).spin(this.$el[0]); + this.start_time = new Date().getTime(); + this.act_message(); + }, + act_message: function() { + var self = this; + setTimeout(function() { + if (self.isDestroyed()) + return; + var seconds = (new Date().getTime() - self.start_time) / 1000; + var mes; + _.each(messages_by_seconds(), function(el) { + if (seconds >= el[0]) + mes = el[1]; + }); + self.$(".oe_throbber_message").html(mes); + self.act_message(); + }, 1000); + }, + destroy: function() { + if (this.spin) + this.spin.stop(); + this._super(); + }, }); +instance.web.Throbber.throbbers = []; + +instance.web.blockUI = function() { + var tmp = $.blockUI.apply($, arguments); + var throbber = new instance.web.Throbber(); + instance.web.Throbber.throbbers.push(throbber); + throbber.appendTo($(".oe_blockui_spin_container")); + return tmp; +} +instance.web.unblockUI = function() { + _.each(instance.web.Throbber.throbbers, function(el) { + el.destroy(); + }); + return $.unblockUI.apply($, arguments); +} + +/** + * Registry for all the client actions key: tag value: widget + */ +instance.web.client_actions = new instance.web.Registry(); }; diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index fdb15d598af..f1f8532821f 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -58,7 +58,7 @@ instance.web.Query = instance.web.Class.extend({ }, _execute: function () { var self = this; - return instance.connection.rpc('/web/dataset/search_read', { + return instance.session.rpc('/web/dataset/search_read', { model: this._model.name, fields: this._fields || false, domain: this._model.domain(this._filter), @@ -107,7 +107,7 @@ instance.web.Query = instance.web.Class.extend({ * Performs a groups read according to the provided grouping criterion * * @param {String|Array<String>} grouping - * @returns {jQuery.Deferred<Array<openerp.web.data.Group>> | null} + * @returns {jQuery.Deferred<Array<openerp.web.QueryGroup>> | null} */ group_by: function (grouping) { if (grouping === undefined) { @@ -130,7 +130,7 @@ instance.web.Query = instance.web.Class.extend({ orderby: instance.web.serialize_sort(this._order_by) || false }).pipe(function (results) { return _(results).map(function (result) { - return new instance.web.data.Group( + return new instance.web.QueryGroup( self._model.name, grouping[0], result); }); }); @@ -194,7 +194,54 @@ instance.web.Query = instance.web.Class.extend({ } }); -instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ +instance.web.QueryGroup = instance.web.Class.extend({ + init: function (model, grouping_field, read_group_group) { + // In cases where group_by_no_leaf and no group_by, the result of + // read_group has aggregate fields but no __context or __domain. + // Create default (empty) values for those so that things don't break + var fixed_group = _.extend( + {__context: {group_by: []}, __domain: []}, + read_group_group); + + var aggregates = {}; + _(fixed_group).each(function (value, key) { + if (key.indexOf('__') === 0 + || key === grouping_field + || key === grouping_field + '_count') { + return; + } + aggregates[key] = value || 0; + }); + + this.model = new instance.web.Model( + model, fixed_group.__context, fixed_group.__domain); + + var group_size = fixed_group[grouping_field + '_count'] || fixed_group.__count || 0; + var leaf_group = fixed_group.__context.group_by.length === 0; + this.attributes = { + folded: !!(fixed_group.__fold), + grouped_on: grouping_field, + // if terminal group (or no group) and group_by_no_leaf => use group.__count + length: group_size, + value: fixed_group[grouping_field], + // A group is open-able if it's not a leaf in group_by_no_leaf mode + has_children: !(leaf_group && fixed_group.__context['group_by_no_leaf']), + + aggregates: aggregates + }; + }, + get: function (key) { + return this.attributes[key]; + }, + subgroups: function () { + return this.model.query().group_by(this.model.context().group_by); + }, + query: function () { + return this.model.query.apply(this.model, arguments); + } +}); + +instance.web.Model = instance.web.Class.extend({ /** * @constructs instance.web.Model * @extends instance.web.Class @@ -233,7 +280,7 @@ instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ kwargs = args; args = []; } - return instance.connection.rpc('/web/dataset/call_kw', { + return instance.session.rpc('/web/dataset/call_kw', { model: this.name, method: method, args: args, @@ -256,7 +303,7 @@ instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ * @param {String} signal signal to trigger on the workflow */ exec_workflow: function (id, signal) { - return instance.connection.rpc('/web/dataset/exec_workflow', { + return instance.session.rpc('/web/dataset/exec_workflow', { model: this.name, id: id, signal: signal @@ -282,7 +329,7 @@ instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ */ context: function (context) { return new instance.web.CompoundContext( - instance.connection.user_context, this._context, context || {}); + instance.session.user_context, this._context, context || {}); }, /** * Button action caller, needs to perform cleanup if an action is returned @@ -292,7 +339,7 @@ instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ * FIXME: remove when evaluator integrated */ call_button: function (method, args) { - return instance.connection.rpc('/web/dataset/call_button', { + return instance.session.rpc('/web/dataset/call_button', { model: this.name, method: method, domain_id: null, @@ -302,7 +349,7 @@ instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ }, }); -instance.web.Traverser = instance.web.Class.extend(/** @lends openerp.web.Traverser# */{ +instance.web.Traverser = instance.web.Class.extend({ /** * @constructs instance.web.Traverser * @extends instance.web.Class @@ -381,65 +428,7 @@ instance.web.Traverser = instance.web.Class.extend(/** @lends openerp.web.Traver }); -/** - * Utility objects, should never need to be instantiated from outside of this - * module - * - * @namespace - */ -instance.web.data = { - Group: instance.web.Class.extend(/** @lends openerp.web.data.Group# */{ - /** - * @constructs instance.web.data.Group - * @extends instance.web.Class - */ - init: function (model, grouping_field, read_group_group) { - // In cases where group_by_no_leaf and no group_by, the result of - // read_group has aggregate fields but no __context or __domain. - // Create default (empty) values for those so that things don't break - var fixed_group = _.extend( - {__context: {group_by: []}, __domain: []}, - read_group_group); - - var aggregates = {}; - _(fixed_group).each(function (value, key) { - if (key.indexOf('__') === 0 - || key === grouping_field - || key === grouping_field + '_count') { - return; - } - aggregates[key] = value || 0; - }); - - this.model = new instance.web.Model( - model, fixed_group.__context, fixed_group.__domain); - - var group_size = fixed_group[grouping_field + '_count'] || fixed_group.__count || 0; - var leaf_group = fixed_group.__context.group_by.length === 0; - this.attributes = { - grouped_on: grouping_field, - // if terminal group (or no group) and group_by_no_leaf => use group.__count - length: group_size, - value: fixed_group[grouping_field], - // A group is open-able if it's not a leaf in group_by_no_leaf mode - has_children: !(leaf_group && fixed_group.__context['group_by_no_leaf']), - - aggregates: aggregates - }; - }, - get: function (key) { - return this.attributes[key]; - }, - subgroups: function () { - return this.model.query().group_by(this.model.context().group_by); - }, - query: function () { - return this.model.query.apply(this.model, arguments); - } - }) -}; - -instance.web.DataGroup = instance.web.OldWidget.extend( /** @lends openerp.web.DataGroup# */{ +instance.web.DataGroup = instance.web.CallbackEnabled.extend({ /** * Management interface between views and grouped collections of OpenERP * records. @@ -451,9 +440,9 @@ instance.web.DataGroup = instance.web.OldWidget.extend( /** @lends openerp.web. * content of the current grouping level. * * @constructs instance.web.DataGroup - * @extends instance.web.OldWidget + * @extends instance.web.CallbackEnabled * - * @param {instance.web.OldWidget} parent widget + * @param {instance.web.CallbackEnabled} parent widget * @param {String} model name of the model managed by this DataGroup * @param {Array} domain search domain for this DataGroup * @param {Object} context context of the DataGroup's searches @@ -471,49 +460,47 @@ instance.web.DataGroup = instance.web.OldWidget.extend( /** @lends openerp.web. }, list: function (fields, ifGroups, ifRecords) { var self = this; - $.when(this.model.query(fields) - .order_by(this.sort) - .group_by(this.group_by)).then(function (groups) { - if (!groups) { - ifRecords(_.extend( - new instance.web.DataSetSearch( - self, self.model.name, - self.model.context(), - self.model.domain()), - {_sort: self.sort})); + var query = this.model.query(fields).order_by(this.sort).group_by(this.group_by); + $.when(query).then(function (querygroups) { + // leaf node + if (!querygroups) { + var ds = new instance.web.DataSetSearch(self, self.model.name, self.model.context(), self.model.domain()); + ds._sort = self.sort; + ifRecords(ds); return; } - ifGroups(_(groups).map(function (group) { + // internal node + var child_datagroups = _(querygroups).map(function (group) { var child_context = _.extend( {}, self.model.context(), group.model.context()); - return _.extend( - new instance.web.DataGroup( - self, self.model.name, group.model.domain(), - child_context, group.model._context.group_by, - self.level + 1), - { - __context: child_context, - __domain: group.model.domain(), - grouped_on: group.get('grouped_on'), - length: group.get('length'), - value: group.get('value'), - openable: group.get('has_children'), - aggregates: group.get('aggregates') - }, {sort: self.sort}); - })); + var child_dg = new instance.web.DataGroup( + self, self.model.name, group.model.domain(), + child_context, group.model._context.group_by, + self.level + 1); + child_dg.sort = self.sort; + // copy querygroup properties + child_dg.__context = child_context; + child_dg.__domain = group.model.domain(); + child_dg.folded = group.get('folded'); + child_dg.grouped_on = group.get('grouped_on'); + child_dg.length = group.get('length'); + child_dg.value = group.get('value'); + child_dg.openable = group.get('has_children'); + child_dg.aggregates = group.get('aggregates'); + return child_dg; + }); + ifGroups(child_datagroups); }); } }); -instance.web.ContainerDataGroup = instance.web.DataGroup.extend({ }); -instance.web.GrouplessDataGroup = instance.web.DataGroup.extend({ }); -instance.web.StaticDataGroup = instance.web.GrouplessDataGroup.extend( /** @lends openerp.web.StaticDataGroup# */ { +instance.web.StaticDataGroup = instance.web.DataGroup.extend({ /** - * A specialization of groupless data groups, relying on a single static + * A specialization of data groups, relying on a single static * dataset as its records provider. * * @constructs instance.web.StaticDataGroup - * @extends instance.web.GrouplessDataGroup + * @extends instance.web.DataGroup * @param {openep.web.DataSetStatic} dataset a static dataset backing the groups */ init: function (dataset) { @@ -524,13 +511,13 @@ instance.web.StaticDataGroup = instance.web.GrouplessDataGroup.extend( /** @lend } }); -instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.DataSet# */{ +instance.web.DataSet = instance.web.CallbackEnabled.extend({ /** * DateaManagement interface between views and the collection of selected * OpenERP records (represents the view's state?) * * @constructs instance.web.DataSet - * @extends instance.web.OldWidget + * @extends instance.web.CallbackEnabled * * @param {String} model the OpenERP model this dataset will manage */ @@ -648,11 +635,9 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da * @param {Function} error_callback function called in case of creation error * @returns {$.Deferred} */ - create: function(data, callback, error_callback) { - return this._model.call('create', - [data], {context: this._model.context()}) - .pipe(function (r) { return {result: r}; }) - .then(callback, error_callback); + create: function(data) { + return this._model.call('create', [data], {context: this._model.context()}) + .pipe(function (r) { return {result: r}; }); }, /** * Saves the provided data in an existing db record @@ -677,10 +662,8 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da * @param {Function} callback function called with operation result * @param {Function} error_callback function called in case of deletion error */ - unlink: function(ids, callback, error_callback) { - return this._model.call('unlink', - [ids], {context: this._model.context()}) - .then(callback, error_callback); + unlink: function(ids) { + return this._model.call('unlink', [ids], {context: this._model.context()}); }, /** * Calls an arbitrary RPC method @@ -705,14 +688,14 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da * @param {Function} error_callback * @returns {$.Deferred} */ - call_and_eval: function (method, args, domain_index, context_index, callback, error_callback) { - return this.rpc('/web/dataset/call', { + call_and_eval: function (method, args, domain_index, context_index) { + return instance.session.rpc('/web/dataset/call', { model: this.model, method: method, domain_id: domain_index == undefined ? null : domain_index, context_id: context_index == undefined ? null : context_index, args: args || [] - }, callback, error_callback); + }); }, /** * Calls a button method, usually returning some sort of action @@ -723,9 +706,8 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da * @param {Function} error_callback * @returns {$.Deferred} */ - call_button: function (method, args, callback, error_callback) { - return this._model.call_button(method, args) - .then(callback, error_callback); + call_button: function (method, args) { + return this._model.call_button(method, args); }, /** * Fetches the "readable name" for records, based on intrinsic rules @@ -734,10 +716,8 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da * @param {Function} callback * @returns {$.Deferred} */ - name_get: function(ids, callback) { - return this._model.call('name_get', - [ids], {context: this._model.context()}) - .then(callback); + name_get: function(ids) { + return this._model.call('name_get', [ids], {context: this._model.context()}); }, /** * @@ -748,28 +728,25 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da * @param {Function} callback function to call with name_search result * @returns {$.Deferred} */ - name_search: function (name, domain, operator, limit, callback) { + name_search: function (name, domain, operator, limit) { return this._model.call('name_search', { name: name || '', args: domain || false, operator: operator || 'ilike', context: this._model.context(), limit: limit || 0 - }).then(callback); + }); }, /** * @param name * @param callback */ - name_create: function(name, callback) { - return this._model.call('name_create', - [name], {context: this._model.context()}) - .then(callback); + name_create: function(name) { + return this._model.call('name_create', [name], {context: this._model.context()}); }, - exec_workflow: function (id, signal, callback) { + exec_workflow: function (id, signal) { return this._model.exec_workflow(id, signal) - .pipe(function (result) { return { result: result }; }) - .then(callback); + .pipe(function (result) { return { result: result }; }); }, get_context: function(request_context) { return this._model.context(request_context); @@ -803,7 +780,23 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da return this.ids.length; }, alter_ids: function(n_ids) { - this.ids = n_ids; + this.ids = n_ids; + }, + /** + * Resequence records. + * + * @param {Array} ids identifiers of the records to resequence + * @returns {$.Deferred} + */ + resequence: function (ids, options) { + options = options || {}; + return instance.session.rpc('/web/dataset/resequence', { + model: this.model, + ids: ids, + context: this._model.context(options.context), + }).pipe(function (results) { + return results; + }); }, }); instance.web.DataSetStatic = instance.web.DataSet.extend({ @@ -836,7 +829,7 @@ instance.web.DataSetStatic = instance.web.DataSet.extend({ this.set_ids(_.without.apply(null, [this.ids].concat(ids))); } }); -instance.web.DataSetSearch = instance.web.DataSet.extend(/** @lends openerp.web.DataSetSearch */{ +instance.web.DataSetSearch = instance.web.DataSet.extend({ /** * @constructs instance.web.DataSetSearch * @extends instance.web.DataSet @@ -886,7 +879,7 @@ instance.web.DataSetSearch = instance.web.DataSet.extend(/** @lends openerp.web }, unlink: function(ids, callback, error_callback) { var self = this; - return this._super(ids, function(result) { + return this._super(ids).then(function(result) { self.ids = _(self.ids).difference(ids); if (self._length) { self._length -= 1; @@ -895,9 +888,7 @@ instance.web.DataSetSearch = instance.web.DataSet.extend(/** @lends openerp.web self.index = self.index <= self.ids.length - 1 ? self.index : (self.ids.length > 0 ? self.ids.length -1 : 0); } - if (callback) - callback(result); - }, error_callback); + }); }, size: function () { if (this._length !== undefined) { @@ -920,14 +911,12 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ on_default_get: function(res) { this.last_default_get = res; }, - create: function(data, callback, error_callback) { + create: function(data) { var cached = {id:_.uniqueId(this.virtual_id_prefix), values: data, defaults: this.last_default_get}; this.to_create.push(_.extend(_.clone(cached), {values: _.clone(cached.values)})); this.cache.push(cached); - var prom = $.Deferred().then(callback); - prom.resolve({result: cached.id}); - return prom.promise(); + return $.Deferred().resolve({result: cached.id}).promise(); }, write: function (id, data, options, callback) { var self = this; @@ -1042,7 +1031,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ self.cache.push({id: id, values: record}); } else { // I assume cache value is prioritary - cached.values = _.defaults(_.clone(cached.values), record); + cached.values = _.defaults(_.clone(cached.values), record); } }); return_records(); @@ -1053,7 +1042,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ } return completion.promise(); }, - call_button: function (method, args, callback, error_callback) { + call_button: function (method, args) { var id = args[0][0], index; for(var i=0, len=this.cache.length; i<len; ++i) { var record = this.cache[i]; @@ -1064,10 +1053,10 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ break; } } - return this._super(method, args, callback, error_callback); + return this._super(method, args); }, alter_ids: function(n_ids) { - this._super(n_ids); + this._super(n_ids); this.on_change(); }, }); @@ -1096,9 +1085,9 @@ instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({ return this._super.apply(this, arguments); } }, - create: function(data, callback, error_callback) { + create: function(data) { if (this.create_function) { - return this.create_function(data, this._super).then(callback, error_callback); + return this.create_function(data, this._super); } else { return this._super.apply(this, arguments); } @@ -1110,9 +1099,9 @@ instance.web.ProxyDataSet = instance.web.DataSetSearch.extend({ return this._super.apply(this, arguments); } }, - unlink: function(ids, callback, error_callback) { + unlink: function(ids) { if (this.unlink_function) { - return this.unlink_function(ids, this._super).then(callback, error_callback); + return this.unlink_function(ids, this._super); } else { return this._super.apply(this, arguments); } @@ -1165,7 +1154,7 @@ instance.web.CompoundDomain = instance.web.Class.extend({ } }); -instance.web.DropMisordered = instance.web.Class.extend(/** @lends openerp.web.DropMisordered# */{ +instance.web.DropMisordered = instance.web.Class.extend({ /** * @constructs instance.web.DropMisordered * @extends instance.web.Class diff --git a/addons/web/static/src/js/data_export.js b/addons/web/static/src/js/data_export.js index ebe82ddb4b4..29aea6362f7 100644 --- a/addons/web/static/src/js/data_export.js +++ b/addons/web/static/src/js/data_export.js @@ -21,31 +21,31 @@ instance.web.DataExport = instance.web.Dialog.extend({ ], close: function(event, ui){ self.close();} }); - self.$element.removeClass('ui-dialog-content ui-widget-content'); - self.$element.find('#add_field').click(function() { + self.$el.removeClass('ui-dialog-content ui-widget-content'); + self.$el.find('#add_field').click(function() { if ($('#field-tree-structure tr.ui-selected')) { - var fld = self.$element.find('#field-tree-structure tr.ui-selected').find('a'); + var fld = self.$el.find('#field-tree-structure tr.ui-selected').find('a'); for (var i = 0; i < fld.length; i++) { var id = $(fld[i]).attr('id').split('-')[1]; var string = $(fld[i]).attr('string'); self.add_field(id, string); } - self.$element.find('#field-tree-structure tr').removeClass('ui-selected'); + self.$el.find('#field-tree-structure tr').removeClass('ui-selected'); } }); - self.$element.find('#remove_field').click(function() { - self.$element.find('#fields_list option:selected').remove(); + self.$el.find('#remove_field').click(function() { + self.$el.find('#fields_list option:selected').remove(); }); - self.$element.find('#remove_all_field').click(function() { - self.$element.find('#fields_list').empty(); + self.$el.find('#remove_all_field').click(function() { + self.$el.find('#fields_list').empty(); }); - this.$element.find('#export_new_list').click(this.on_show_save_list); + this.$el.find('#export_new_list').click(this.on_show_save_list); var got_fields = new $.Deferred(); - this.$element.find('#import_compat').change(function() { - self.$element.find('#fields_list').empty(); - self.$element.find('#field-tree-structure').remove(); - var import_comp = self.$element.find("#import_compat").val(); + this.$el.find('#import_compat').change(function() { + self.$el.find('#fields_list').empty(); + self.$el.find('#field-tree-structure').remove(); + var import_comp = self.$el.find("#import_compat").val(); self.rpc("/web/export/get_fields", { model: self.dataset.model, import_compat: Boolean(import_comp) @@ -61,7 +61,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ this.show_exports_list()); }, do_setup_export_formats: function (formats) { - var $fmts = this.$element.find('#export_format'); + var $fmts = this.$el.find('#export_format'); _(formats).each(function (format) { var opt = new Option(format.label, format.tag); if (format.error) { @@ -76,8 +76,8 @@ instance.web.DataExport = instance.web.Dialog.extend({ }, show_exports_list: function() { var self = this; - if (self.$element.find('#saved_export_list').is(':hidden')) { - self.$element.find('#ExistsExportList').show(); + if (self.$el.find('#saved_export_list').is(':hidden')) { + self.$el.find('#ExistsExportList').show(); return; } return this.exports.read_slice(['name'], { @@ -86,35 +86,35 @@ instance.web.DataExport = instance.web.Dialog.extend({ if (!export_list.length) { return; } - self.$element.find('#ExistsExportList').append(QWeb.render('Exists.ExportList', {'existing_exports': export_list})); - self.$element.find('#saved_export_list').change(function() { - self.$element.find('#fields_list option').remove(); - var export_id = self.$element.find('#saved_export_list option:selected').val(); + self.$el.find('#ExistsExportList').append(QWeb.render('Exists.ExportList', {'existing_exports': export_list})); + self.$el.find('#saved_export_list').change(function() { + self.$el.find('#fields_list option').remove(); + var export_id = self.$el.find('#saved_export_list option:selected').val(); if (export_id) { self.rpc('/web/export/namelist', {'model': self.dataset.model, export_id: parseInt(export_id)}, self.do_load_export_field); } }); - self.$element.find('#delete_export_list').click(function() { - var select_exp = self.$element.find('#saved_export_list option:selected'); + self.$el.find('#delete_export_list').click(function() { + var select_exp = self.$el.find('#saved_export_list option:selected'); if (select_exp.val()) { self.exports.unlink([parseInt(select_exp.val(), 10)]); select_exp.remove(); - if (self.$element.find('#saved_export_list option').length <= 1) { - self.$element.find('#ExistsExportList').hide(); + if (self.$el.find('#saved_export_list option').length <= 1) { + self.$el.find('#ExistsExportList').hide(); } } }); }); }, do_load_export_field: function(field_list) { - var export_node = this.$element.find("#fields_list"); + var export_node = this.$el.find("#fields_list"); _(field_list).each(function (field) { export_node.append(new Option(field.label, field.name)); }); }, on_show_save_list: function() { var self = this; - var current_node = self.$element.find("#savenewlist"); + var current_node = self.$el.find("#savenewlist"); if (!(current_node.find("label")).length) { current_node.append(QWeb.render('ExportNewList')); current_node.find("#add_export_list").click(function() { @@ -150,14 +150,14 @@ instance.web.DataExport = instance.web.Dialog.extend({ if (!export_list_id.result) { return; } - self.$element.find("#saved_export_list").append( + self.$el.find("#saved_export_list").append( new Option(value, export_list_id.result)); - if (self.$element.find("#saved_export_list").is(":hidden")) { + if (self.$el.find("#saved_export_list").is(":hidden")) { self.show_exports_list(); } }); this.on_show_save_list(); - this.$element.find("#fields_list option").remove(); + this.$el.find("#fields_list option").remove(); }, on_click: function(id, record) { var self = this; @@ -173,7 +173,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ } if (!record.loaded) { - var import_comp = self.$element.find("#import_compat").val(); + var import_comp = self.$el.find("#import_compat").val(); self.rpc("/web/export/get_fields", { model: model, prefix: prefix, @@ -191,44 +191,44 @@ instance.web.DataExport = instance.web.Dialog.extend({ }, on_show_data: function(result, after) { var self = this; - var imp_cmpt = Boolean(self.$element.find("#import_compat").val()); + var imp_cmpt = Boolean(self.$el.find("#import_compat").val()); if (after) { - var current_tr = self.$element.find("tr[id='treerow-" + after + "']"); + var current_tr = self.$el.find("tr[id='treerow-" + after + "']"); current_tr.addClass('open'); current_tr.find('img').attr('src','/web/static/src/img/collapse.gif'); current_tr.after(QWeb.render('ExportTreeView-Secondary.children', {'fields': result})); } else { - self.$element.find('#left_field_panel').append(QWeb.render('ExportTreeView-Secondary', {'fields': result})); + self.$el.find('#left_field_panel').append(QWeb.render('ExportTreeView-Secondary', {'fields': result})); } _.each(result, function(record) { self.records[record.id] = record.value; if (record.required) { - var required_fld = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + var required_fld = self.$el.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); required_fld.addClass("oe_export_requiredfield"); } - self.$element.find("img[id='parentimg-" + record.id +"']").click(function() { + self.$el.find("img[id='parentimg-" + record.id +"']").click(function() { self.on_click(this.id, record); }); - self.$element.find("tr[id='treerow-" + record.id + "']").click(function(e) { + self.$el.find("tr[id='treerow-" + record.id + "']").click(function(e) { if (e.shiftKey) { var frst_click, scnd_click = ''; if (self.row_index == 0) { self.row_index = this.rowIndex; - frst_click = self.$element.find("tr[id^='treerow-']")[self.row_index-1]; + frst_click = self.$el.find("tr[id^='treerow-']")[self.row_index-1]; $(frst_click).addClass("ui-selected"); } else { if (this.rowIndex >=self.row_index) { for (var i = (self.row_index-1); i < this.rowIndex; i++) { - scnd_click = self.$element.find("tr[id^='treerow-']")[i]; + scnd_click = self.$el.find("tr[id^='treerow-']")[i]; if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { $(scnd_click).addClass("ui-selected"); } } } else { for (var i = (self.row_index-1); i >= (this.rowIndex-1); i--) { - scnd_click = self.$element.find("tr[id^='treerow-']")[i]; + scnd_click = self.$el.find("tr[id^='treerow-']")[i]; if (!$(scnd_click).find('#tree-column').hasClass("oe_export_readonlyfield")) { $(scnd_click).addClass("ui-selected"); } @@ -238,10 +238,10 @@ instance.web.DataExport = instance.web.Dialog.extend({ } self.row_index = this.rowIndex; - self.$element.find("tr[id='treerow-" + record.id + "']").keyup(function() { + self.$el.find("tr[id='treerow-" + record.id + "']").keyup(function() { self.row_index = 0; }); - var o2m_selection = self.$element.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); + var o2m_selection = self.$el.find("tr[id='treerow-" + record.id + "']").find('#tree-column'); if ($(o2m_selection).hasClass("oe_export_readonlyfield")) { return false; } @@ -252,14 +252,14 @@ instance.web.DataExport = instance.web.Dialog.extend({ $(this).addClass('ui-selected').find('a').focus(); } } else if (!e.shiftKey) { - self.$element.find("tr.ui-selected") + self.$el.find("tr.ui-selected") .removeClass("ui-selected").find('a').blur(); $(this).addClass("ui-selected").find('a').focus(); } return false; }); - self.$element.find("tr[id='treerow-" + record.id + "']").keydown(function(e) { + self.$el.find("tr[id='treerow-" + record.id + "']").keydown(function(e) { var keyCode = e.keyCode || e.which; var arrow = {left: 37, up: 38, right: 39, down: 40 }; switch (keyCode) { @@ -297,19 +297,19 @@ instance.web.DataExport = instance.web.Dialog.extend({ break; } }); - self.$element.find("tr[id='treerow-" + record.id + "']").dblclick(function() { - var $o2m_selection = self.$element.find("tr[id^='treerow-" + record.id + "']").find('#tree-column'); + self.$el.find("tr[id='treerow-" + record.id + "']").dblclick(function() { + var $o2m_selection = self.$el.find("tr[id^='treerow-" + record.id + "']").find('#tree-column'); if (!$o2m_selection.hasClass("oe_export_readonlyfield")) { self.add_field(record.id, $(this).find("a").attr("string")); } }); }); - self.$element.find('#fields_list').mouseover(function(event) { + self.$el.find('#fields_list').mouseover(function(event) { if (event.relatedTarget) { if (event.relatedTarget.attributes['id'] && event.relatedTarget.attributes['string']) { var field_id = event.relatedTarget.attributes["id"]["value"]; if (field_id && field_id.split("-")[0] === 'export') { - if (!self.$element.find("tr[id='treerow-" + field_id.split("-")[1] + "']").find('#tree-column').hasClass("oe_export_readonlyfield")) { + if (!self.$el.find("tr[id='treerow-" + field_id.split("-")[1] + "']").find('#tree-column').hasClass("oe_export_readonlyfield")) { self.add_field(field_id.split("-")[1], event.relatedTarget.attributes["string"]["value"]); } } @@ -319,7 +319,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ }, showcontent: function(id) { // show & hide the contents - var $this = this.$element.find("tr[id='treerow-" + id + "']"); + var $this = this.$el.find("tr[id='treerow-" + id + "']"); var is_open = $this.hasClass('open'); $this.toggleClass('open'); @@ -329,7 +329,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ } else { first_child.attr('src', '/web/static/src/img/collapse.gif'); } - var child_field = this.$element.find("tr[id^='treerow-" + id +"/']"); + var child_field = this.$el.find("tr[id^='treerow-" + id +"/']"); var child_len = (id.split("/")).length + 1; for (var i = 0; i < child_field.length; i++) { var $child = $(child_field[i]); @@ -345,15 +345,15 @@ instance.web.DataExport = instance.web.Dialog.extend({ } }, add_field: function(field_id, string) { - var field_list = this.$element.find('#fields_list'); - if (this.$element.find("#fields_list option[value='" + field_id + "']") - && !this.$element.find("#fields_list option[value='" + field_id + "']").length) { + var field_list = this.$el.find('#fields_list'); + if (this.$el.find("#fields_list option[value='" + field_id + "']") + && !this.$el.find("#fields_list option[value='" + field_id + "']").length) { field_list.append(new Option(string, field_id)); } }, get_fields: function() { var export_field = []; - this.$element.find("#fields_list option").each(function() { + this.$el.find("#fields_list option").each(function() { export_field.push($(this).val()); }); if (!export_field.length) { @@ -363,7 +363,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ }, on_click_export_data: function() { var self = this; - var exported_fields = this.$element.find('#fields_list option').map(function () { + var exported_fields = this.$el.find('#fields_list option').map(function () { // DOM property is textContent, but IE8 only knows innerText return {name: self.records[this.value] || this.value, label: this.textContent || this.innerText}; @@ -375,8 +375,8 @@ instance.web.DataExport = instance.web.Dialog.extend({ } exported_fields.unshift({name: 'id', label: 'External ID'}); - var export_format = this.$element.find("#export_format").val(); - $.blockUI(); + var export_format = this.$el.find("#export_format").val(); + instance.web.blockUI(); this.session.get_file({ url: '/web/export/' + export_format, data: {data: JSON.stringify({ @@ -385,13 +385,13 @@ instance.web.DataExport = instance.web.Dialog.extend({ ids: this.dataset.ids, domain: this.dataset.domain, import_compat: Boolean( - this.$element.find("#import_compat").val()) + this.$el.find("#import_compat").val()) })}, - complete: $.unblockUI + complete: instance.web.unblockUI }); }, close: function() { - this.$element.remove(); + this.$el.remove(); this._super(); } }); diff --git a/addons/web/static/src/js/data_import.js b/addons/web/static/src/js/data_import.js deleted file mode 100644 index 30f9323f723..00000000000 --- a/addons/web/static/src/js/data_import.js +++ /dev/null @@ -1,366 +0,0 @@ -openerp.web.data_import = function(instance) { -var QWeb = instance.web.qweb, - _t = instance.web._t; -/** - * Safari does not deal well at all with raw JSON data being returned. As a - * result, we're going to cheat by using a pseudo-jsonp: instead of getting - * JSON data in the iframe, we're getting a ``script`` tag which consists of a - * function call and the returned data (the json dump). - * - * The function is an auto-generated name bound to ``window``, which calls - * back into the callback provided here. - * - * @param {Object} form the form element (DOM or jQuery) to use in the call - * @param {Object} attributes jquery.form attributes object - * @param {Function} callback function to call with the returned data - */ -function jsonp(form, attributes, callback) { - attributes = attributes || {}; - var options = {jsonp: _.uniqueId('import_callback_')}; - window[options.jsonp] = function () { - delete window[options.jsonp]; - callback.apply(null, arguments); - }; - if ('data' in attributes) { - _.extend(attributes.data, options); - } else { - _.extend(attributes, {data: options}); - } - $(form).ajaxSubmit(attributes); -} - -instance.web.DataImport = instance.web.Dialog.extend({ - template: 'ImportDataView', - dialog_title: {toString: function () { return _t("Import Data"); }}, - init: function(parent, dataset){ - var self = this; - this._super(parent, {}); - this.model = parent.model; - this.fields = []; - this.all_fields = []; - this.fields_with_defaults = []; - this.required_fields = null; - - var convert_fields = function (root, prefix) { - prefix = prefix || ''; - _(root.fields).each(function (f) { - self.all_fields.push(prefix + f.name); - if (f.fields) { - convert_fields(f, prefix + f.id + '/'); - } - }); - }; - this.ready = $.Deferred.queue().then(function () { - self.required_fields = _(self.fields).chain() - .filter(function (field) { - return field.required && - !_.include(self.fields_with_defaults, field.id); }) - .pluck('name') - .value(); - convert_fields(self); - self.all_fields.sort(); - }); - }, - start: function() { - var self = this; - this._super(); - this.open({ - buttons: [ - {text: _t("Close"), click: function() { self.destroy(); }}, - {text: _t("Import File"), click: function() { self.do_import(); }, 'class': 'oe-dialog-import-button'} - ], - close: function(event, ui) { - self.destroy(); - } - }); - this.toggle_import_button(false); - this.$element.find('#csvfile').change(this.on_autodetect_data); - this.$element.find('fieldset').change(this.on_autodetect_data); - this.$element.delegate('fieldset legend', 'click', function() { - $(this).parent().toggleClass('oe-closed'); - }); - this.ready.push(new instance.web.DataSet(this, this.model).call( - 'fields_get', [], function (fields) { - self.graft_fields(fields); - self.ready.push(new instance.web.DataSet(self, self.model) - .default_get(_.pluck(self.fields, 'id')).then(function (fields) { - _.each(fields, function(val, key) { - if (val) { - self.fields_with_defaults.push(key); - } - }); - }) - ) - })); - }, - graft_fields: function (fields, parent, level) { - parent = parent || this; - level = level || 0; - - var self = this; - if (level === 0) { - parent.fields.push({ - id: 'id', - name: 'id', - string: _t('External ID'), - required: false - }); - } - _(fields).each(function (field, field_name) { - // Ignore spec for id field - // Don't import function fields (function and related) - if (field_name === 'id') { - return; - } - // Skip if there's no state which could disable @readonly, - // if a field is ever always readonly we can't import it - if (field.readonly) { - // no states at all - if (_.isEmpty(field.states)) { return; } - // no state altering @readonly - if (!_.any(field.states, function (modifiers) { - return _(modifiers).chain().pluck(0).contains('readonly').value(); - })) { return; } - } - - var f = { - id: field_name, - name: field_name, - string: field.string, - required: field.required - }; - - switch (field.type) { - case 'many2many': - case 'many2one': - f.name += '/id'; - break; - case 'one2many': - f.name += '/id'; - f.fields = []; - // only fetch sub-fields to a depth of 2 levels - if (level < 2) { - self.ready.push(new instance.web.DataSet(self, field.relation).call( - 'fields_get', [], function (fields) { - self.graft_fields(fields, f, level+1); - })); - } - break; - } - parent.fields.push(f); - }); - }, - toggle_import_button: function (newstate) { - instance.web.dialog(this.$element, 'widget') - .find('.oe-dialog-import-button') - .button('option', 'disabled', !newstate); - }, - do_import: function() { - if(!this.$element.find('#csvfile').val()) { return; } - var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10); - var with_headers = this.$element.find('#file_has_headers').prop('checked'); - if (!lines_to_skip && with_headers) { - lines_to_skip = 1; - } - var indices = [], fields = []; - this.$element.find(".sel_fields").each(function(index, element) { - var val = element.value; - if (!val) { - return; - } - indices.push(index); - fields.push(val); - }); - - jsonp(this.$element.find('#import_data'), { - url: '/web/import/import_data', - data: { - model: this.model, - meta: JSON.stringify({ - skip: lines_to_skip, - indices: indices, - fields: fields - }) - } - }, this.on_import_results); - }, - on_autodetect_data: function() { - if(!this.$element.find('#csvfile').val()) { return; } - jsonp(this.$element.find('#import_data'), { - url: '/web/import/detect_data' - }, this.on_import_results); - }, - on_import_results: function(results) { - this.$element.find('#result').empty(); - var headers, result_node = this.$element.find("#result"); - - if (results['error']) { - result_node.append(QWeb.render('ImportView.error', { - 'error': results['error']})); - this.$element.find('fieldset').removeClass('oe-closed'); - return; - } - if (results['success']) { - if (this.getParent().getParent().active_view == "list") { - this.getParent().reload_content(); - } - this.destroy(); - return; - } - - if (results['records']) { - var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10), - with_headers = this.$element.find('#file_has_headers').prop('checked'); - headers = with_headers ? results.records[0] : null; - - result_node.append(QWeb.render('ImportView.result', { - 'headers': headers, - 'records': lines_to_skip ? results.records.slice(lines_to_skip) - : with_headers ? results.records.slice(1) - : results.records - })); - this.$element.find('fieldset').addClass('oe-closed'); - } - this.$element.find('form').removeClass('oe-import-no-result'); - - this.$element.delegate('.oe-m2o-drop-down-button', 'click', function () { - $(this).prev('input').focus(); - }); - - var self = this; - this.ready.then(function () { - var $fields = self.$element.find('.sel_fields').bind('blur', function () { - if (this.value && !_(self.all_fields).contains(this.value)) { - this.value = ''; - } - }).autocomplete({ - minLength: 0, - source: self.all_fields, - change: self.on_check_field_values - }).focus(function () { - $(this).autocomplete('search'); - }); - // Column auto-detection - _(headers).each(function (header, index) { - var field_name = self.match_column_to_field(header); - if (field_name) { - $fields.eq(index).val(field_name); - } - }); - self.on_check_field_values(); - }); - }, - /** - * Returns the name of the field (nested) matching the provided column name - * - * @param {String} name column name to look for - * @param {Array} [fields] fields to look into for the provided name - * @returns {String|undefined} - */ - match_column_to_field: function (name, fields) { - fields = fields || this.fields; - var f; - f = _(fields).detect(function (field) { - // TODO: levenshtein between header and field.string - return field.name === name - || field.string.toLowerCase() === name.toLowerCase(); - }); - if (f) { return f.name; } - - // if ``name`` is a path (o2m), we need to recurse through its .fields - var index = name.indexOf('/'); - if (index === -1) { return undefined; } - // Get the first path section, try to find the matching field - var column_name = name.substring(0, index); - f = _(fields).detect(function (field) { - // field.name for o2m is $foo/id, so we want to match on id - return field.id === column_name - || field.string.toLowerCase() === column_name.toLowerCase() - }); - if (!f) { return undefined; } - - // if we found a matching field for the first path section, recurse in - // its own .fields to try and get the rest of the path matched - var rest = this.match_column_to_field( - name.substring(index+1), f.fields); - if (!rest) { return undefined; } - return f.id + '/' + rest; - }, - /** - * Looks through all the field selections, and tries to find if two - * (or more) columns were matched to the same model field. - * - * Returns a map of the multiply-mapped fields to an array of offending - * columns (not actually columns, but the inputs containing the same field - * names). - * - * Also has the side-effect of marking the discovered inputs with the class - * ``duplicate_fld``. - * - * @returns {Object<String, Array<String>>} map of duplicate field matches to same-valued inputs - */ - find_duplicate_fields: function() { - // Maps values to DOM nodes, in order to discover duplicates - var values = {}, duplicates = {}; - this.$element.find(".sel_fields").each(function(index, element) { - var value = element.value; - var $element = $(element).removeClass('duplicate_fld'); - if (!value) { return; } - - if (!(value in values)) { - values[value] = element; - } else { - var same_valued_field = values[value]; - if (value in duplicates) { - duplicates[value].push(element); - } else { - duplicates[value] = [same_valued_field, element]; - } - $element.add(same_valued_field).addClass('duplicate_fld'); - } - }); - return duplicates; - }, - on_check_field_values: function () { - this.$element.find("#message, #msg").remove(); - - var required_valid = this.check_required(); - - var duplicates = this.find_duplicate_fields(); - if (_.isEmpty(duplicates)) { - this.toggle_import_button(required_valid); - } else { - var $err = $('<div id="msg" style="color: red;">Destination fields should only be selected once, some fields are selected more than once:</div>').insertBefore(this.$element.find('#result')); - var $dupes = $('<dl>').appendTo($err); - _(duplicates).each(function(elements, value) { - $('<dt>').text(value).appendTo($dupes); - _(elements).each(function(element) { - var cell = $(element).closest('td'); - $('<dd>').text(cell.parent().children().index(cell)).appendTo($dupes); - }); - }); - this.toggle_import_button(false); - } - - }, - check_required: function() { - if (!this.required_fields.length) { return true; } - - var selected_fields = _(this.$element.find('.sel_fields').get()).chain() - .pluck('value') - .compact() - .value(); - - var missing_fields = _.difference(this.required_fields, selected_fields); - if (missing_fields.length) { - this.$element.find("#result").before('<div id="message" style="color:red">*Required Fields are not selected : ' + missing_fields + '.</div>'); - return false; - } - return true; - }, - destroy: function() { - this.$element.remove(); - this._super(); - } -}); -}; diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js index 0f1c71643be..3e5e6fecd5e 100644 --- a/addons/web/static/src/js/formats.js +++ b/addons/web/static/src/js/formats.js @@ -118,7 +118,9 @@ instance.web.format_value = function (value, descriptor, value_if_empty) { return instance.web.insert_thousand_seps( _.str.sprintf('%d', value)); case 'float': - var precision = descriptor.digits ? descriptor.digits[1] : 2; + var digits = descriptor.digits ? descriptor.digits : [69,2]; + digits = typeof digits === "string" ? py.eval(digits) : digits; + var precision = digits[1]; var formatted = _.str.sprintf('%.' + precision + 'f', value).split('.'); formatted[0] = instance.web.insert_thousand_seps(formatted[0]); return formatted.join(l10n.decimal_point); @@ -136,6 +138,9 @@ instance.web.format_value = function (value, descriptor, value_if_empty) { return value[1]; case 'one2many': case 'many2many': + if (typeof value === 'string') { + return value; + } return _.str.sprintf(_t("(%d records)"), value.length); case 'datetime': if (typeof(value) == "string") @@ -269,82 +274,4 @@ instance.web.auto_date_to_str = function(value, type) { } }; -/** - * Formats a provided cell based on its field type. Most of the field types - * return a correctly formatted value, but some tags and fields are - * special-cased in their handling: - * - * * buttons will return an actual ``<button>`` tag with a bunch of error handling - * - * * boolean fields will return a checkbox input, potentially disabled - * - * * binary fields will return a link to download the binary data as a file - * - * @param {Object} row_data record whose values should be displayed in the cell - * @param {Object} column column descriptor - * @param {"button"|"field"} column.tag base control type - * @param {String} column.type widget type for a field control - * @param {String} [column.string] button label - * @param {String} [column.icon] button icon - * @param {Object} [options] - * @param {String} [options.value_if_empty=''] what to display if the field's value is ``false`` - * @param {Boolean} [options.process_modifiers=true] should the modifiers be computed ? - * @param {String} [options.model] current record's model - * @param {Number} [options.id] current record's id - * - */ -instance.web.format_cell = function (row_data, column, options) { - options = options || {}; - var attrs = {}; - if (options.process_modifiers !== false) { - attrs = column.modifiers_for(row_data); - } - if (attrs.invisible) { return ''; } - - if (column.tag === 'button') { - return _.template('<button type="button" title="<%-title%>" <%=additional_attributes%> >' + - '<img src="<%-prefix%>/web/static/src/img/icons/<%-icon%>.png" alt="<%-alt%>"/>' + - '</button>', { - title: column.string || '', - additional_attributes: isNaN(row_data["id"].value) && instance.web.BufferedDataSet.virtual_id_regex.test(row_data["id"].value) ? - 'disabled="disabled" class="oe-listview-button-disabled"' : '', - prefix: instance.connection.prefix, - icon: column.icon, - alt: column.string || '' - }); - } - if (!row_data[column.id]) { - return options.value_if_empty === undefined ? '' : options.value_if_empty; - } - - switch (column.widget || column.type) { - case "boolean": - return _.str.sprintf('<input type="checkbox" %s disabled="disabled"/>', - row_data[column.id].value ? 'checked="checked"' : ''); - case "binary": - var text = _t("Download"), - download_url = _.str.sprintf('/web/binary/saveas?session_id=%s&model=%s&field=%s&id=%d', instance.connection.session_id, options.model, column.id, options.id); - if (column.filename) { - download_url += '&filename_field=' + column.filename; - if (row_data[column.filename]) { - text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value( - row_data[column.filename].value, {type: 'char'})); - } - } - return _.template('<a href="<%-href%>"><%-text%></a> (%<-size%>)', { - text: text, - href: download_url, - size: row_data[column.id].value - }); - case 'progressbar': - return _.template( - '<progress value="<%-value%>" max="100"><%-value%>%</progress>', { - value: _.str.sprintf("%.0f", row_data[column.id].value || 0) - }); - } - - return _.escape(instance.web.format_value( - row_data[column.id].value, column, options.value_if_empty)); -} - }; diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index f9e737471ae..01bc0c76c87 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -126,21 +126,21 @@ my.InputView = instance.web.Widget.extend({ template: 'SearchView.InputView', start: function () { var p = this._super.apply(this, arguments); - this.$element.on('focus', this.proxy('onFocus')); - this.$element.on('blur', this.proxy('onBlur')); - this.$element.on('keydown', this.proxy('onKeydown')); + this.$el.on('focus', this.proxy('onFocus')); + this.$el.on('blur', this.proxy('onBlur')); + this.$el.on('keydown', this.proxy('onKeydown')); return p; }, onFocus: function () { this.trigger('focused', this); }, onBlur: function () { - this.$element.text(''); + this.$el.text(''); this.trigger('blurred', this); }, getSelection: function () { // get Text node - var root = this.$element[0].childNodes[0]; + var root = this.$el[0].childNodes[0]; if (!root || !root.textContent) { // if input does not have a child node, or the child node is an // empty string, then the selection can only be (0, 0) @@ -202,7 +202,7 @@ my.InputView = instance.web.Widget.extend({ break; case $.ui.keyCode.RIGHT: sel = this.getSelection(); - var len = this.$element.text().length; + var len = this.$el.text().length; if (sel.start !== len || sel.start !== sel.end) { e.stopPropagation(); } @@ -223,17 +223,17 @@ my.FacetView = instance.web.Widget.extend({ }, start: function () { var self = this; - this.$element.on('focus', function () { self.trigger('focused', self); }); - this.$element.on('blur', function () { self.trigger('blurred', self); }); - this.$element.on('click', function (e) { + this.$el.on('focus', function () { self.trigger('focused', self); }); + this.$el.on('blur', function () { self.trigger('blurred', self); }); + this.$el.on('click', function (e) { if ($(e.target).is('.oe_facet_remove')) { self.model.destroy(); return false; } - self.$element.focus(); + self.$el.focus(); e.stopPropagation(); }); - this.$element.on('keydown', function (e) { + this.$el.on('keydown', function (e) { var keys = $.ui.keyCode; switch (e.which) { case keys.BACKSPACE: @@ -242,7 +242,7 @@ my.FacetView = instance.web.Widget.extend({ return false; } }); - var $e = self.$element.find('> span:last-child'); + var $e = self.$el.find('> span:last-child'); var q = $.when(this._super()); return q.pipe(function () { var values = self.model.values.map(function (value) { @@ -253,7 +253,7 @@ my.FacetView = instance.web.Widget.extend({ }); }, model_changed: function () { - this.$element.text(this.$element.text() + '*'); + this.$el.text(this.$el.text() + '*'); } }); my.FacetValueView = instance.web.Widget.extend({ @@ -268,7 +268,7 @@ my.FacetValueView = instance.web.Widget.extend({ this._super(); }, model_changed: function () { - this.$element.text(this.$element.text() + '*'); + this.$el.text(this.$el.text() + '*'); } }); @@ -313,7 +313,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea .on('add change reset remove', this.proxy('renderFacets')); if (this.hidden) { - this.$element.hide(); + this.$el.hide(); } if (this.headless) { this.ready.resolve(); @@ -330,7 +330,13 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea }); } - this.$element.on('keydown', + // Launch a search on clicking the oe_searchview_search button + this.$el.on('click', 'button.oe_searchview_search', function (e) { + e.stopImmediatePropagation(); + self.do_search(); + }); + + this.$el.on('keydown', '.oe_searchview_input, .oe_searchview_facet', function (e) { switch(e.which) { case $.ui.keyCode.LEFT: @@ -344,26 +350,31 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea } }); - this.$element.on('click', '.oe_searchview_clear', function (e) { + this.$el.on('click', '.oe_searchview_clear', function (e) { e.stopImmediatePropagation(); self.query.reset(); }); - this.$element.on('click', '.oe_searchview_unfold_drawer', function (e) { + this.$el.on('click', '.oe_searchview_unfold_drawer', function (e) { e.stopImmediatePropagation(); - self.$element.toggleClass('oe_searchview_open_drawer'); + self.$el.toggleClass('oe_searchview_open_drawer'); + }); + instance.web.bus.on('click', this, function(ev) { + if ($(ev.target).parents('.oe_searchview').length === 0) { + self.$el.removeClass('oe_searchview_open_drawer'); + } }); // Focus last input if the view itself is clicked (empty section of // facets element) - this.$element.on('click', function (e) { - if (e.target === self.$element.find('.oe_searchview_facets')[0]) { - self.$element.find('.oe_searchview_input:last').focus(); + this.$el.on('click', function (e) { + if (e.target === self.$el.find('.oe_searchview_facets')[0]) { + self.$el.find('.oe_searchview_input:last').focus(); } }); // when the completion list opens/refreshes, automatically select the // first completion item so if the user just hits [RETURN] or [TAB] it // automatically selects it - this.$element.on('autocompleteopen', function () { - var menu = self.$element.data('autocomplete').menu; + this.$el.on('autocompleteopen', function () { + var menu = self.$el.data('autocomplete').menu; menu.activate( $.Event({ type: "mouseenter" }), menu.element.children().first()); @@ -372,15 +383,15 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea return $.when(p, this.ready); }, show: function () { - this.$element.show(); + this.$el.show(); }, hide: function () { - this.$element.hide(); + this.$el.hide(); }, subviewForRoot: function (subview_root) { return _(this.input_subviews).detect(function (subview) { - return subview.$element[0] === subview_root; + return subview.$el[0] === subview_root; }); }, siblingSubview: function (subview, direction, wrap_around) { @@ -395,12 +406,12 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea focusPreceding: function (subview_root) { return this.siblingSubview( this.subviewForRoot(subview_root), -1, true) - .$element.focus(); + .$el.focus(); }, focusFollowing: function (subview_root) { return this.siblingSubview( this.subviewForRoot(subview_root), +1, true) - .$element.focus(); + .$el.focus(); }, /** @@ -419,19 +430,19 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea // autocomplete only correctly handles being initialized on the actual // editable element (and only an element with a @value in 1.8 e.g. - // input or textarea), cheat by setting val() on $element - this.$element.on('keydown', function () { + // input or textarea), cheat by setting val() on $el + this.$el.on('keydown', function () { // keydown is triggered *before* the element's value is set, so // delay this. Pray that setTimeout are executed in FIFO (if they // have the same delay) as autocomplete uses the exact same trick. // FIXME: brittle as fuck setTimeout(function () { - self.$element.val(self.currentInputValue()); + self.$el.val(self.currentInputValue()); }, 0); }); - this.$element.autocomplete({ + this.$el.autocomplete({ source: this.proxy('complete_global_search'), select: this.proxy('select_completion'), focus: function (e) { e.preventDefault(); }, @@ -468,7 +479,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea * div[contenteditable].oe_searchview_input) */ currentInputValue: function () { - return this.$element.find('div.oe_searchview_input:focus').text(); + return this.$el.find('div.oe_searchview_input:focus').text(); }, /** * Provide auto-completion result for req.term (an array to `resp`) @@ -498,14 +509,23 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea var input_index = _(this.input_subviews).indexOf( this.subviewForRoot( - this.$element.find('div.oe_searchview_input:focus')[0])); + this.$el.find('div.oe_searchview_input:focus')[0])); this.query.add(ui.item.facet, {at: input_index / 2}); }, childFocused: function () { - this.$element.addClass('oe_focused'); + this.$el.addClass('oe_focused'); }, childBlurred: function () { - this.$element.removeClass('oe_focused'); + var val = this.$el.val(); + this.$el.val(''); + var complete = this.$el.data('autocomplete'); + if ((val && complete.term === undefined) || complete.previous !== undefined) { + throw new Error("new jquery.ui version altering implementation" + + " details relied on"); + } + delete complete.term; + this.$el.removeClass('oe_focused') + .trigger('blur'); }, /** * @@ -518,7 +538,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea // _2: undefined if event=change, otherwise model var self = this; var started = []; - var $e = this.$element.find('div.oe_searchview_facets'); + var $e = this.$el.find('div.oe_searchview_facets'); _.invoke(this.input_subviews, 'destroy'); this.input_subviews = []; @@ -549,7 +569,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea input_to_focus = self.input_subviews[(options.at + 1) * 2]; } - input_to_focus.$element.focus(); + input_to_focus.$el.focus(); }); }, @@ -615,6 +635,16 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea return null; } }, + + add_common_inputs: function() { + // add Filters to this.inputs, need view.controls filled + (new instance.web.search.Filters(this)); + // add custom filters to this.inputs + (new instance.web.search.CustomFilters(this)); + // add Advanced to this.inputs + (new instance.web.search.Advanced(this)); + }, + on_loaded: function(data) { var self = this; this.fields_view = data.fields_view; @@ -629,18 +659,13 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea data.fields_view['arch'].children, data.fields_view.fields); - // add Filters to this.inputs, need view.controls filled - (new instance.web.search.Filters(this)); - // add custom filters to this.inputs - (new instance.web.search.CustomFilters(this)); - // add Advanced to this.inputs - (new instance.web.search.Advanced(this)); + this.add_common_inputs(); // build drawer var drawer_started = $.when.apply( null, _(this.select_for_drawer()).invoke( - 'appendTo', this.$element.find('.oe_searchview_drawer'))); - + 'appendTo', this.$el.find('.oe_searchview_drawer'))); + // load defaults var defaults_fetched = $.when.apply(null, _(this.inputs).invoke( 'facet_for_defaults', this.defaults)).then(function () { @@ -655,15 +680,12 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea */ on_filters_management: function(e) { var self = this; - var select = this.$element.find(".oe_search-view-filters-management"); + var select = this.$el.find(".oe_search-view-filters-management"); var val = select.val(); switch(val) { case 'advanced_filter': this.extended_search.on_activate(); break; - case 'add_to_dashboard': - this.on_add_to_dashboard(); - break; case '': this.do_clear(); } @@ -692,53 +714,6 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea select.val(''); } }, - on_add_to_dashboard: function() { - this.$element.find(".oe_search-view-filters-management")[0].selectedIndex = 0; - var self = this, - menu = instance.webclient.menu, - $dialog = $(QWeb.render("SearchView.add_to_dashboard", { - dashboards : menu.data.data.children, - selected_menu_id : menu.$element.find('a.active').data('menu') - })); - $dialog.find('input').val(this.fields_view.name); - instance.web.dialog($dialog, { - modal: true, - title: _t("Add to Dashboard"), - buttons: [ - {text: _t("Cancel"), click: function() { - $(this).dialog("close"); - }}, - {text: _t("OK"), click: function() { - $(this).dialog("close"); - var menu_id = $(this).find("select").val(), - title = $(this).find("input").val(), - data = self.build_search_data(), - context = new instance.web.CompoundContext(), - domain = new instance.web.CompoundDomain(); - _.each(data.contexts, function(x) { - context.add(x); - }); - _.each(data.domains, function(x) { - domain.add(x); - }); - self.rpc('/web/searchview/add_to_dashboard', { - menu_id: menu_id, - action_id: self.getParent().action.id, - context_to_save: context, - domain: domain, - view_mode: self.getParent().active_view, - name: title - }, function(r) { - if (r === false) { - self.do_warn("Could not add filter to dashboard"); - } else { - self.do_notify("Filter added to dashboard", ''); - } - }); - }} - ] - }); - }, /** * Extract search data from the view's facets. * @@ -886,13 +861,13 @@ instance.web.search.Invalid = instance.web.Class.extend( /** @lends instance.web ); } }); -instance.web.search.Widget = instance.web.OldWidget.extend( /** @lends instance.web.search.Widget# */{ +instance.web.search.Widget = instance.web.Widget.extend( /** @lends instance.web.search.Widget# */{ template: null, /** * Root class of all search widgets * * @constructs instance.web.search.Widget - * @extends instance.web.OldWidget + * @extends instance.web.Widget * * @param view the ancestor view of this widget */ @@ -1014,7 +989,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in this.view.query.on('add remove change reset', this.proxy('search_change')); }, start: function () { - this.$element.on('click', 'li', this.proxy('toggle_filter')); + this.$el.on('click', 'li', this.proxy('toggle_filter')); return $.when(null); }, /** @@ -1023,7 +998,7 @@ instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends in */ search_change: function () { var self = this; - var $filters = this.$element.find('> li').removeClass('oe_selected'); + var $filters = this.$el.find('> li').removeClass('oe_selected'); var facet = this.view.query.find(_.bind(this.match_facet, this)); if (!facet) { return; } facet.values.each(function (v) { @@ -1234,7 +1209,7 @@ instance.web.search.Field = instance.web.search.Input.extend( /** @lends instanc var self = this; // A field needs a context to send when active var context = this.attrs.context; - if (!context || !facet.values.length) { + if (_.isEmpty(context) || !facet.values.length) { return; } var contexts = facet.values.map(function (facetValue) { @@ -1324,17 +1299,17 @@ instance.web.search.CharField = instance.web.search.Field.extend( /** @lends ins }); instance.web.search.NumberField = instance.web.search.Field.extend(/** @lends instance.web.search.NumberField# */{ value_from: function () { - if (!this.$element.val()) { + if (!this.$el.val()) { return null; } - var val = this.parse(this.$element.val()), - check = Number(this.$element.val()); + var val = this.parse(this.$el.val()), + check = Number(this.$el.val()); if (isNaN(val) || val !== check) { - this.$element.addClass('error'); + this.$el.addClass('error'); throw new instance.web.search.Invalid( - this.attrs.name, this.$element.val(), this.error_message); + this.attrs.name, this.$el.val(), this.error_message); } - this.$element.removeClass('error'); + this.$el.removeClass('error'); return val; } }); @@ -1515,9 +1490,16 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ facet_for: function (value) { var self = this; if (value instanceof Array) { - return $.when(facet_from(this, value)); + if (value.length === 2 && _.isString(value[1])) { + return $.when(facet_from(this, value)); + } + assert(value.length <= 1, + _("M2O search fields do not currently handle multiple default values")); + // there are many cases of {search_default_$m2ofield: [id]}, need + // to handle this as if it were a single value. + value = value[0]; } - return this.model.call('name_get', [value], {}).pipe(function (names) { + return this.model.call('name_get', [value]).pipe(function (names) { if (_(names).isEmpty()) { return null; } return facet_from(self, names[0]); }) @@ -1530,6 +1512,15 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ return [[name, '=', facetValue.get('value')]]; } return this._super(name, operator, facetValue); + }, + get_context: function (facet) { + var values = facet.values; + if (_.isEmpty(this.attrs.context) && values.length === 1) { + var c = {}; + c['default_' + this.attrs.name] = values.at(0).get('value'); + return c; + } + return this._super(facet); } }); @@ -1540,15 +1531,26 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ var self = this; this.model = new instance.web.Model('ir.filters'); this.filters = {}; - this.$element.on('submit', 'form', this.proxy('save_current')); - this.$element.on('click', 'h4', function () { - self.$element.toggleClass('oe_opened'); + this.view.query + .on('remove', function (facet) { + if (!facet.get('is_custom_filter')) { + return; + } + self.clear_selection(); + }) + .on('reset', this.proxy('clear_selection')); + this.$el.on('submit', 'form', this.proxy('save_current')); + this.$el.on('click', 'h4', function () { + self.$el.toggleClass('oe_opened'); }); // FIXME: local eval of domain and context to get rid of special endpoint return this.rpc('/web/searchview/get_filters', { model: this.view.model }).pipe(this.proxy('set_filters')); }, + clear_selection: function () { + this.$el.find('li.oe_selected').removeClass('oe_selected'); + }, append_filter: function (filter) { var self = this; var key = _.str.sprintf('(%s)%s', filter.user_id, filter.name); @@ -1559,7 +1561,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ } else { var id = filter.id; $filter = this.filters[key] = $('<li></li>') - .appendTo(this.$element.find('.oe_searchview_custom_list')) + .appendTo(this.$el.find('.oe_searchview_custom_list')) .addClass(filter.user_id ? 'oe_searchview_custom_private' : 'oe_searchview_custom_public') .text(filter.name); @@ -1571,7 +1573,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ $filter.remove(); }); }) - .prependTo($filter); + .appendTo($filter); } $filter.unbind('click').click(function () { @@ -1583,8 +1585,10 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ get_groupby: function () { return [filter.context]; }, get_domain: function () { return filter.domain; } }, + is_custom_filter: true, values: [{label: filter.name, value: null}] }]); + $filter.addClass('oe_selected'); }); }, set_filters: function (filters) { @@ -1592,8 +1596,8 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ }, save_current: function () { var self = this; - var $name = this.$element.find('input:first'); - var private_filter = !this.$element.find('input:last').prop('checked'); + var $name = this.$el.find('input:first'); + var private_filter = !this.$el.find('input:last').prop('checked'); var search = this.view.build_search_data(); this.rpc('/web/session/eval_domain_and_context', { @@ -1606,7 +1610,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ } var filter = { name: $name.val(), - user_id: private_filter ? instance.connection.uid : false, + user_id: private_filter ? instance.session.uid : false, model_id: self.view.model, context: results.context, domain: results.domain @@ -1615,7 +1619,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ return self.model.call('create_or_replace', [filter]).then(function (id) { filter.id = id; self.append_filter(filter); - self.$element + self.$el .removeClass('oe_opened') .find('form')[0].reset(); }); @@ -1660,8 +1664,8 @@ instance.web.search.Filters = instance.web.search.Input.extend({ } return $.when( - this.render_column(col1, $('<div>').appendTo(this.$element)), - this.render_column(col2, $('<div>').appendTo(this.$element))); + this.render_column(col1, $('<div>').appendTo(this.$el)), + this.render_column(col2, $('<div>').appendTo(this.$el))); }, render_column: function (column, $el) { return $.when.apply(null, _(column).map(function (group) { @@ -1671,15 +1675,16 @@ instance.web.search.Filters = instance.web.search.Input.extend({ })); } }); + instance.web.search.Advanced = instance.web.search.Input.extend({ template: 'SearchView.advanced', _in_drawer: true, start: function () { var self = this; - this.$element + this.$el .on('keypress keydown keyup', function (e) { e.stopPropagation(); }) .on('click', 'h4', function () { - self.$element.toggleClass('oe_opened'); + self.$el.toggleClass('oe_opened'); }).on('click', 'button.oe_add_condition', function () { self.append_proposition(); }).on('submit', 'form', function (e) { @@ -1698,7 +1703,7 @@ instance.web.search.Advanced = instance.web.search.Input.extend({ }, append_proposition: function () { return (new instance.web.search.ExtendedSearchProposition(this, this.fields)) - .appendTo(this.$element.find('ul')); + .appendTo(this.$el.find('ul')); }, commit_search: function () { var self = this; @@ -1725,15 +1730,15 @@ instance.web.search.Advanced = instance.web.search.Input.extend({ // add new empty proposition this.append_proposition(); // TODO: API on searchview - this.view.$element.removeClass('oe_searchview_open_drawer'); + this.view.$el.removeClass('oe_searchview_open_drawer'); } }); -instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/** @lends instance.web.search.ExtendedSearchProposition# */{ +instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @lends instance.web.search.ExtendedSearchProposition# */{ template: 'SearchView.extended_search.proposition', /** * @constructs instance.web.search.ExtendedSearchProposition - * @extends instance.web.OldWidget + * @extends instance.web.Widget * * @param parent * @param fields @@ -1749,16 +1754,16 @@ instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/* }, start: function () { var _this = this; - this.$element.find(".searchview_extended_prop_field").change(function() { + this.$el.find(".searchview_extended_prop_field").change(function() { _this.changed(); }); - this.$element.find('.searchview_extended_delete_prop').click(function () { + this.$el.find('.searchview_extended_delete_prop').click(function () { _this.destroy(); }); this.changed(); }, changed: function() { - var nval = this.$element.find(".searchview_extended_prop_field").val(); + var nval = this.$el.find(".searchview_extended_prop_field").val(); if(this.attrs.selected == null || nval != this.attrs.selected.name) { this.select_field(_.detect(this.fields, function(x) {return x.name == nval;})); } @@ -1773,7 +1778,7 @@ instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/* if(this.attrs.selected != null) { this.value.destroy(); this.value = null; - this.$element.find('.searchview_extended_prop_op').html(''); + this.$el.find('.searchview_extended_prop_op').html(''); } this.attrs.selected = field; if(field == null) { @@ -1789,9 +1794,9 @@ instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/* _.each(this.value.operators, function(operator) { $('<option>', {value: operator.value}) .text(String(operator.text)) - .appendTo(self.$element.find('.searchview_extended_prop_op')); + .appendTo(self.$el.find('.searchview_extended_prop_op')); }); - var $value_loc = this.$element.find('.searchview_extended_prop_value').empty(); + var $value_loc = this.$el.find('.searchview_extended_prop_value').empty(); this.value.appendTo($value_loc); }, @@ -1799,7 +1804,7 @@ instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/* if ( this.attrs.selected == null) return null; var field = this.attrs.selected; - var op = this.$element.find('.searchview_extended_prop_op')[0]; + var op = this.$el.find('.searchview_extended_prop_op')[0]; var operator = op.options[op.selectedIndex]; return { label: _.str.sprintf(_t('%(field)s %(operator)s "%(value)s"'), { @@ -1843,7 +1848,7 @@ instance.web.search.ExtendedSearchProposition.Char = instance.web.search.Extende {value: "!=", text: _lt("is not equal to")} ], get_value: function() { - return this.$element.val(); + return this.$el.val(); } }); instance.web.search.ExtendedSearchProposition.DateTime = instance.web.search.ExtendedSearchProposition.Field.extend({ @@ -1867,7 +1872,7 @@ instance.web.search.ExtendedSearchProposition.DateTime = instance.web.search.Ext start: function() { var ready = this._super(); this.datewidget = new (this.widget())(this); - this.datewidget.appendTo(this.$element); + this.datewidget.appendTo(this.$el); return ready; } }); @@ -1885,11 +1890,11 @@ instance.web.search.ExtendedSearchProposition.Integer = instance.web.search.Exte {value: "<=", text: _lt("less or equal than")} ], toString: function () { - return this.$element.val(); + return this.$el.val(); }, get_value: function() { try { - return instance.web.parse_value(this.$element.val(), {'widget': 'integer'}); + return instance.web.parse_value(this.$el.val(), {'widget': 'integer'}); } catch (e) { return ""; } @@ -1909,11 +1914,11 @@ instance.web.search.ExtendedSearchProposition.Float = instance.web.search.Extend {value: "<=", text: _lt("less or equal than")} ], toString: function () { - return this.$element.val(); + return this.$el.val(); }, get_value: function() { try { - return instance.web.parse_value(this.$element.val(), {'widget': 'float'}); + return instance.web.parse_value(this.$el.val(), {'widget': 'float'}); } catch (e) { return ""; } @@ -1926,12 +1931,12 @@ instance.web.search.ExtendedSearchProposition.Selection = instance.web.search.Ex {value: "!=", text: _lt("is not")} ], toString: function () { - var select = this.$element[0]; + var select = this.$el[0]; var option = select.options[select.selectedIndex]; return option.label || option.text; }, get_value: function() { - return this.$element.val(); + return this.$el.val(); } }); instance.web.search.ExtendedSearchProposition.Boolean = instance.web.search.ExtendedSearchProposition.Field.extend({ diff --git a/addons/web/static/src/js/test_support.js b/addons/web/static/src/js/test_support.js index d46366326b7..8e1317ba2b0 100644 --- a/addons/web/static/src/js/test_support.js +++ b/addons/web/static/src/js/test_support.js @@ -1,18 +1,17 @@ openerp.test_support = { - setup_connection: function (connection) { + setup_session: function (session) { var origin = location.protocol+"//"+location.host; - _.extend(connection, { + _.extend(session, { origin: origin, prefix: origin, server: origin, // keep chs happy //openerp.web.qweb.default_dict['_s'] = this.origin; - rpc_function: connection.rpc_json, + rpc_function: session.rpc_json, session_id: false, uid: false, username: false, user_context: {}, db: false, - openerp_entreprise: false, // this.module_list = openerp._modules.slice(); // this.module_loaded = {}; // _(this.module_list).each(function (mod) { @@ -22,7 +21,7 @@ openerp.test_support = { shortcuts: [], active_id: null }); - return connection.session_reload(); + return session.session_reload(); }, module: function (title, tested_core, nonliterals) { var conf = QUnit.config.openerp = {}; @@ -31,10 +30,10 @@ openerp.test_support = { QUnit.stop(); var oe = conf.openerp = window.openerp.init(); window.openerp.web[tested_core](oe); - var done = openerp.test_support.setup_connection(oe.connection); + var done = openerp.test_support.setup_session(oe.session); if (nonliterals) { done = done.pipe(function () { - return oe.connection.rpc('/tests/add_nonliterals', { + return oe.session.rpc('/tests/add_nonliterals', { domains: nonliterals.domains || [], contexts: nonliterals.contexts || [] }).then(function (r) { diff --git a/addons/web/static/src/js/view_editor.js b/addons/web/static/src/js/view_editor.js deleted file mode 100644 index 9fdd4f0bca1..00000000000 --- a/addons/web/static/src/js/view_editor.js +++ /dev/null @@ -1,1207 +0,0 @@ -openerp.web.view_editor = function(instance) { -var _t = instance.web._t; -var QWeb = instance.web.qweb; -instance.web.ViewEditor = instance.web.OldWidget.extend({ - init: function(parent, element_id, dataset, view, options) { - this._super(parent); - this.element_id = element_id; - this.parent = parent; - this.dataset = new instance.web.DataSetSearch(this, 'ir.ui.view', null, null), - this.model = dataset.model; - this.xml_element_id = 0; - this.property = instance.web.ViewEditor.property_widget; - this.one_object = false; - }, - start: function() { - this.init_view_editor(); - }, - init_view_editor: function() { - var self = this, - action_title = _.str.sprintf(_t("Manage Views (%s)"), this.model); - var action = { - name: action_title, - context: this.session.user_context, - domain: [["model", "=", this.model]], - res_model: 'ir.ui.view', - views: [[false, 'list']], - type: 'ir.actions.act_window', - target: "current", - limit: this.dataset.limit || 80, - auto_search : true, - flags: { - sidebar: false, - deletable: false, - views_switcher: false, - action_buttons: false, - search_view: false, - pager: false, - radio: true, - select_view_id: self.parent.fields_view.view_id - } - }; - this.view_edit_dialog = new instance.web.Dialog(this, { - title: action_title, - width: 850, - buttons: [ - {text: _t("Create"), click: function() { self.on_create_view(); }}, - {text: _t("Edit"), click: function() { self.xml_element_id = 0; self.get_arch(); }}, - {text: _t("Remove"), click: function() { self.do_delete_view(); }}, - {text: _t("Close"), click: function() { self.view_edit_dialog.close(); window.location.reload(); }} - ] - }).open(); - this.view_edit_dialog.on_close.add_last(function(){window.location.reload();}); - this.main_view_id = this.parent.fields_view.view_id; - this.action_manager = new instance.web.ActionManager(this); - $.when(this.action_manager.do_action(action)).then(function() { - var viewmanager = self.action_manager.inner_viewmanager, - controller = viewmanager.views[viewmanager.active_view].controller; - self.action_manager.appendTo(self.view_edit_dialog.$element); - self.action_manager.renderElement(self.view_edit_dialog); - controller.on_loaded.add_last(function(){ - $(controller.groups).bind({ - 'selected': function(e, ids, records) { - self.main_view_id = ids[0]; - } - }) - }); - }); - }, - on_create_view: function() { - var self = this; - this.create_view_dialog = new instance.web.Dialog(this, { - title: _.str.sprintf(_t("Create a view (%s)"), self.model), - buttons: [ - {text: _t("Save"), click: function () { - var view_values = {}; - var warn = false; - _.each(self.create_view_widget, function(widget) { - if (widget.is_invalid) { - warn = true; - return false; - } - if (widget.dirty && !widget.is_invalid) { - view_values[widget.name] = widget.get_value(); - } - }); - if (warn) { - self.on_valid_create_view(self.create_view_widget); - } else { - $.when(self.do_save_view(view_values)).then(function() { - self.create_view_dialog.close(); - var controller = self.action_manager.inner_viewmanager.views[self.action_manager.inner_viewmanager.active_view].controller; - controller.reload_content(); - }); - } - }}, - {text: _t("Cancel"), click: function () { self.create_view_dialog.close(); }} - ] - }).open(); - var view_widget = [{'name': 'view_name', 'string':'View Name', 'type': 'char', 'required': true, 'value' : this.model + '.custom_' + Math.round(Math.random() * 1000)}, - {'name': 'view_type', 'string': 'View Type', 'type': 'selection', 'required': true, 'value': 'Form', 'selection': [['',''],['tree', 'Tree'],['form', 'Form'],['graph', 'Graph'],['calendar', 'Calender']]}, - {'name': 'proirity', 'string': 'Priority', 'type': 'float', 'required': true, 'value':'16'}]; - this.create_view_dialog.$element.append('<table id="create_view" style="width:400px" class="oe_form"></table>'); - this.create_view_widget = []; - _.each(view_widget, function(widget) { - var type_widget = new (self.property.get_any([widget.type])) (self.create_view_dialog, widget); - self.create_view_dialog.$element.find('table[id=create_view]').append('<tr><td width="100px" align="right">' + widget.string + ':</td>' + type_widget.render()+'</tr>'); - var value = null; - if (widget.value) { - value = widget.value; - type_widget.dirty = true; - } - type_widget.start(); - type_widget.set_value(value); - self.create_view_widget.push(type_widget); - }); - }, - do_save_view: function(values) { - def = $.Deferred(); - var field_dataset = new instance.web.DataSetSearch(this, this.model, null, null); - var model_dataset = new instance.web.DataSetSearch(this, 'ir.model', null, null); - var view_string = "", field_name = false, self = this; - field_dataset.call( 'fields_get', [], function(fields) { - _.each(['name', 'x_name'], function(value) { - if (_.include(_.keys(fields), value)) { - field_name = value; - return false; - } - }); - if (field_name) { - model_dataset.read_slice(['name','field_id'], {"domain": [['model','=',self.model]]}).then(function(records) { - if (records) {view_string = records[0].name;} - var arch = _.str.sprintf("<?xml version='1.0'?>\n<%s string='%s'>\n\t<field name='%s'/>\n</%s>", values.view_type, view_string, field_name, values.view_type); - var vals = {'model': self.model, 'name': values.view_name, 'priority': values.priority, 'type': values.view_type, 'arch': arch}; - self.dataset.create(vals, function(suc) { - def.resolve(); - }); - }); - } - }); - return def.promise(); - }, - on_valid_create_view: function(widgets) { - var msg = "<ul>"; - _.each(widgets, function(widget) { - if (widget.is_invalid) { - msg += "<li>" + widget.name + "</li>"; - } - }); - msg += "</ul>"; - this.do_warn("The following fields are invalid :", msg); - }, - add_node_name : function(node) { - if(node.tagName.toLowerCase() == "button" || node.tagName.toLowerCase() == "field"){ - return (node.getAttribute('name'))? - _.str.sprintf( "<%s name='%s'>",node.tagName.toLowerCase(), node.getAttribute('name')): - _.str.sprintf( "<%s>",node.tagName.toLowerCase()); - }else if(node.tagName.toLowerCase() == "group"){ - return (node.getAttribute('string'))? - _.str.sprintf( "<%s>",node.getAttribute('string')): - _.str.sprintf( "<%s>",node.tagName.toLowerCase()); - }else{ - return (node.getAttribute('string'))? - _.str.sprintf( "<%s string='%s'>",node.tagName.toLowerCase(), node.getAttribute('string')): - _.str.sprintf( "<%s>",node.tagName.toLowerCase()); - } - }, - do_delete_view: function() { - var self = this; - if (confirm(_t("Do you really want to remove this view?"))) { - var controller = this.action_manager.inner_viewmanager.views[this.action_manager.inner_viewmanager.active_view].controller; - this.dataset.unlink([this.main_view_id]).then(function() { - controller.reload_content(); - self.main_view_id = self.parent.fields_view.view_id; - }); - } - }, - create_View_Node: function(node){ - ViewNode = { - 'level': ($(node).parents()).length + 1, - 'id': this.xml_element_id += 1, - 'att_list': [], - 'name': this.add_node_name(node), - 'child_id': [] - }; - ViewNode.att_list.push(node.tagName.toLowerCase()); - _.each(node.attributes, function(att) { - ViewNode.att_list.push([att.nodeName, att.nodeValue]); - }); - return ViewNode; - }, - append_child_object: function(main_object, parent_id, child_obj_list) { - var self = this; - if (main_object.id == parent_id) { - main_object.child_id = child_obj_list; - return main_object; - } else { - _.each(main_object.child_id, function(child_object) { - self.append_child_object(child_object, parent_id, child_obj_list); - }); - } - }, - convert_arch_to_obj: function(xml_Node, main_object, parent_id) { - var self = this; - var child_obj_list = []; - _.each(xml_Node, function(element) { - child_obj_list.push(self.create_View_Node(element)); - }); - this.append_child_object(main_object, parent_id, child_obj_list); - var obj_xml_list = _.zip(xml_Node, child_obj_list); - _.each(obj_xml_list, function(node) { - var children = _.filter(node[0].childNodes, function(child) { - return child.nodeType == 1; - }); - if (children) { - self.convert_arch_to_obj(children, main_object, node[1].id); - } - }); - return main_object; - }, - parse_xml: function(arch, view_id) { - main_object = { - 'level': 0, - 'id': this.xml_element_id +=1, - 'att_list': [], - 'name': _.str.sprintf("<view view_id = %s>", view_id), - 'child_id': [] - }; - var xml_arch = QWeb.load_xml(arch); - return [this.convert_arch_to_obj(xml_arch.childNodes, main_object, this.xml_element_id)]; - }, - get_arch: function() { - var self = this; - var view_arch_list = []; - this.dataset.read_ids([parseInt(self.main_view_id)], ['arch', 'type','priority']).then(function(arch) { - if (arch.length) { - var arch_object = self.parse_xml(arch[0].arch, self.main_view_id); - self.main_view_type = arch[0].type == 'tree'? 'list': arch[0].type; - view_arch_list.push({"view_id": self.main_view_id, "arch": arch[0].arch,"priority":arch[0].priority}); - self.dataset.read_slice([], {domain: [['inherit_id','=', parseInt(self.main_view_id)]]}).then(function(result) { - _.each(result, function(res) { - view_arch_list.push({"view_id": res.id, "arch": res.arch,"priority":res.priority}); - self.inherit_view(arch_object, res); - }); - return self.edit_view({"main_object": arch_object, - "parent_child_id": self.parent_child_list(arch_object, []), - "arch": view_arch_list}); - }); - } else { - self.do_warn("Please select view in list :"); - } - }); - }, - parent_child_list : function(one_object, parent_list) { - var self = this; - _.each(one_object , function(element) { - if (element.child_id.length != 0) { - parent_list.push({"key": element.id, "value": _.pluck(element.child_id, 'id')}); - self.parent_child_list(element.child_id, parent_list); - } - }); - return parent_list; - }, - inherit_view : function(arch_object, result) { - var self = this, xml_list = [], xml_arch = QWeb.load_xml(result.arch); - if (xml_arch.childNodes[0].tagName == "data") { - xml_list = _.filter(xml_arch.childNodes[0].childNodes, function(child) { - return child.nodeType == 1; - }); - } else { - xml_list.push( xml_arch.childNodes[0]); - } - _.each(xml_list, function(xml) { - var expr_to_list = [], xpath_arch_object = self.parse_xml(QWeb.tools.xml_node_to_string(xml), result.id); - if (xml.tagName == "xpath") { - var part_expr = _.without(xml.getAttribute('expr').split("/"), ""); - _.each(part_expr, function(part) { - expr_to_list.push(_.without($.trim(part.replace(/[^a-zA-Z 0-9 _]+/g,'!')).split("!"), "")); - }); - } else { - var temp = _.reject(xpath_arch_object[0].child_id[0].att_list, function(list) { - return list instanceof Array? _.include(list, "position"): false; - }); - expr_to_list = [_.flatten(temp)]; - } - self.inherit_apply(expr_to_list, arch_object ,xpath_arch_object); - }); - }, - inherit_apply: function(expr_list ,arch_object ,xpath_arch_object) { - var self = this; - if (xpath_arch_object.length) { - var check = expr_list[0], obj = false; - switch (check.length) { - case 2: - if (parseInt(check[1])) { - //for field[3] - var temp_list = _.select(arch_object, function(element) { - return _.include(_.flatten(element.att_list), check[0]); - }); - obj = arch_object[_.indexOf(arch_object, temp_list[parseInt(check[1]) - 1])]; - } else { - //for notebook[last()] - obj = _.detect(arch_object, function(element) { - return _.include(_.flatten(element.att_list), check[0]); - }); - } - break; - case 3: - //for field[@name='type'] - obj = _.detect(arch_object, function(element){ - if ((_.intersection(_.flatten(element.att_list), _.uniq(check))).length == _.uniq(check).length) { - return element; - } - }); - break; - case 1: - //for /form/notebook - var temp_list = _.select(arch_object, function(element) { - return _.include(_.flatten(element.att_list), check[0]); - }); - if (temp_list.length != 0) { - expr_list.length == 1 ? obj = temp_list[0] : expr_list.shift(); - } - break; - } - if (obj) { - expr_list.shift(); - if (expr_list.length) { - self.inherit_apply(expr_list, obj.child_id, xpath_arch_object); - } else { - self.increase_level(xpath_arch_object[0], obj.level + 1); - obj.child_id.push(xpath_arch_object[0]); - xpath_arch_object.pop(); - } - } else { - _.each(arch_object, function(element) { - self.inherit_apply(expr_list, element.child_id, xpath_arch_object); - }); - } - } - }, - increase_level: function(val, level) { - var self = this; - val.level = level; - _.each(val.child_id, function(val, key) { - self.increase_level(val, level + 1); - }); - }, - do_select_row: function(row_id) { - this.edit_xml_dialog.$element.find("tr[id^='viewedit-']").removeClass('ui-selected'); - this.edit_xml_dialog.$element.find("tr[id=viewedit-" + row_id + "]").addClass('ui-selected'); - }, - do_parent_img_hide_show: function(img) { - if ($(img).attr('src') == '/web/static/src/img/collapse.gif') { - $(img).attr('src', '/web/static/src/img/expand.gif'); - this.on_expand(img); - } else { - $(img).attr('src', '/web/static/src/img/collapse.gif'); - this.on_collapse(img); - } - }, - edit_view: function(one_object) { - var self = this; - this.one_object = one_object; - this.edit_xml_dialog = new instance.web.Dialog(this, { - title: _.str.sprintf(_t("View Editor %d - %s"), self.main_view_id, self.model), - height: '90%', - buttons: [ - {text: _t("Inherited View"), click: function(){ - var selected_row = self.edit_xml_dialog.$element.find('.ui-selected'); - if (selected_row.length) { - if(selected_row.find('a').text().search("field") != -1){ - if (confirm(_t("Do you really wants to create an inherited view here?"))) { - self.inherited_view(selected_row); - } - }else{ - alert("Can't Update View"); - } - }else{ - alert("Select an element"); - } - }}, - {text: _t("Preview"), click: function() { - var action = { - context: self.session.user_context, - res_model: self.model, - views: [[self.main_view_id, self.main_view_type]], - type: 'ir.actions.act_window', - target: "new", - auto_search: true, - flags: { - sidebar: false, - views_switcher: false, - action_buttons: false - } - }; - var action_manager = new instance.web.ActionManager(self); - action_manager.do_action(action); - }}, - {text: _t("Close"), click: function(){ - self.action_manager.inner_viewmanager.views[self.action_manager.inner_viewmanager.active_view].controller.reload_content(); - self.edit_xml_dialog.close(); - }} - ] - }).open(); - var no_property_att = []; - _.each(_PROPERTIES, function(val, key) { - if (! val.length) no_property_att.push(key); - }); - this.edit_xml_dialog.$element.html(QWeb.render('view_editor', {'data': one_object['main_object'], 'no_properties': no_property_att})); - this.edit_xml_dialog.$element.find("tr[id^='viewedit-']").click(function() { - self.do_select_row(this.id.split('-')[1]); - }); - this.edit_xml_dialog.$element.find("img[id^='parentimg-']").click(function() { - self.do_parent_img_hide_show(this); - }); - this.edit_xml_dialog.$element.find("img[id^='side-']").click(function() { - self.on_select_img(this); - }); - }, - inherited_view: function(selected_row){ - var self = this; - var row_id = parseInt((selected_row.attr('id')).split('-')[1]); - var obj = self.get_object_by_id(row_id,self.one_object['main_object'], [])[0]; - var view_name = this.model + '.inherit_' + Math.round(Math.random() * 1000); - var view_find = selected_row; - var view_id; - var min_level = parseInt(selected_row.attr('level')); - while (1) { - view_find = view_find.prev(); - if (view_find.length == 0 || - self.edit_xml_dialog.$element.find(view_find).find('a').text().search("view_id") != -1 && - parseInt(view_find.attr('level')) < min_level ) { - view_id = parseInt($(view_find).find('a').text().replace(/[^0-9]+/g, '')); - break; - } - if (view_find.attr('level') < min_level) { - min_level = parseInt(view_find.attr('level')); - } - } - var val = _.detect(obj.att_list, function(val) {return val[0] == "name";}); - var priority = _.detect(self.one_object['arch'], function(val) {return val.view_id == view_id;}); - var arch = _.str.sprintf("<?xml version='1.0'?>\n\t <field name='%s' position='after'> </field>", val[1]); - var vals = {'model': self.model, 'name': view_name, 'priority': priority.priority + 1, 'type': "form", 'arch': arch,'inherit_id':self.main_view_id}; - this.dataset.create(vals, function(suc) { - var arch_to_obj = self.parse_xml(arch,suc.result); - obj.child_id.push(arch_to_obj[0]); - self.one_object['parent_child_id'] = self.parent_child_list(self.one_object['main_object'],[]); - self.one_object['arch'].push({'view_id':suc.result,"arch":arch,'priority': priority.priority + 1}); - self.increase_level(arch_to_obj[0],obj.level+1); - self.render_inherited_view(selected_row,arch_to_obj[0]); - }); - }, - render_inherited_view: function(selected_row,obj){ - var self = this,row_id = parseInt((selected_row.attr('id')).split('-')[1]); - var clone = this.create_clone(selected_row.clone(),obj); - if (selected_row.find("img[id^='parentimg-']").length == 0) { - ($(selected_row.find('a').parent()).siblings('td')) - .append($('<img width="16" height="16"></img>').attr('src', '/web/static/src/img/collapse.gif'). - attr('id','parentimg-'+ row_id).click(function(){ - self.do_parent_img_hide_show(this); - })); - } - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+row_id+"']").after(clone.removeClass('ui-selected')); - _.each(obj.child_id,function(obj){self.render_inherited_view(clone,obj);}); - }, - on_select_img: function(element_img) { - var self = this; - var side = $(element_img).closest("tr[id^='viewedit-']"); - this.one_object.clicked_tr_id = parseInt((side.attr('id')).split('-')[1]); - this.one_object.clicked_tr_level = parseInt(side.attr('level')); - var img = side.find("img[id='parentimg-" + this.one_object.clicked_tr_id + "']").attr('src'); - var view_id = 0, view_xml_id = 0, view_find = side; - //for view id finding - var min_level = this.one_object.clicked_tr_id; - if (($(side).find('a').text()).search("view_id") != -1) { - view_id = parseInt(($(view_find).find('a').text()).replace(/[^0-9]+/g, '')); - view_xml_id = (view_find.attr('id')).split('-')[1]; - this.one_object.clicked_tr_id += 1; - this.one_object.clicked_tr_level += 1; - }else{ - while (1) { - view_find = view_find.prev(); - if (view_find.length == 0 || - (self.edit_xml_dialog.$element.find(view_find).find('a').text()).search("view_id") != -1 - && parseInt(view_find.attr('level')) < min_level ) { - view_id = parseInt(($(view_find).find('a').text()).replace(/[^0-9]+/g, '')); - view_xml_id = parseInt((view_find.attr('id')).split('-')[1]); - break; - } - if (view_find.attr('level') < min_level) { - min_level = parseInt(view_find.attr('level')); - } - } - } - this.one_object.clicked_tr_view = [view_id, view_xml_id]; - switch (element_img.id) { - case "side-add": - self.do_node_add(side); - break; - case "side-remove": - if (confirm(_t("Do you really want to remove this node?"))) { - self.do_save_update_arch("remove_node"); - } - break; - case "side-edit": - self.do_node_edit(side); - break; - case "side-up": - self.do_node_up(side, img); - break; - case "side-down": - self.do_node_down(side, img); - break; - } - }, - do_node_add: function(side){ - var self = this; - var tr = self.get_object_by_id(this.one_object.clicked_tr_id, this.one_object['main_object'], [])[0].att_list[0]; - var parent_tr = ($(side).prevAll("tr[level=" + String(this.one_object.clicked_tr_level - 1) + "]"))[0]; - var field_dataset = new instance.web.DataSetSearch(this, this.model, null, null); - parent_tr = $(parent_tr).find('a').text(); - field_dataset.call( 'fields_get', [], function(result) { - var fields = _.keys(result); - fields.push(" "),fields.sort(); - var property_to_check = []; - _.each([tr, parent_tr],function(element) { - property_to_check.push( - _.detect(_.keys(_CHILDREN),function(res){ - return _.str.include(element, res); - })); - }); - self.on_add_node(property_to_check, fields); - }); - }, - do_node_edit: function(side) { - var self = this; - var result = self.get_object_by_id(this.one_object.clicked_tr_id, this.one_object['main_object'], []); - if (result.length && result[0] && result[0].att_list) { - var properties = _PROPERTIES[result[0].att_list[0]]; - self.on_edit_node(properties); - } - }, - do_node_down: function(cur_tr, img) { - var self = this, next_tr, last_tr, tr_to_move = []; - tr_to_move.push(cur_tr); - if (img) { - while (1) { - next_tr = cur_tr.next(); - if ( parseInt(next_tr.attr('level')) <= this.one_object.clicked_tr_level || next_tr.length == 0) { - last_tr = next_tr; - break; - } else { - tr_to_move.push(next_tr); - cur_tr = next_tr; - } - } - } else { - last_tr = cur_tr.next(); - } - if ((self.edit_xml_dialog.$element.find(last_tr).find('a').text()).search("view_id") != -1) { - return false; - } - if (last_tr.length != 0 && parseInt(last_tr.attr('level')) == this.one_object.clicked_tr_level) { - var last_tr_id = (last_tr.attr('id')).split('-')[1]; - img = last_tr.find("img[id='parentimg-" + last_tr_id + "']").attr('src'); - if (img) { - self.edit_xml_dialog.$element.find("img[id='parentimg-" + last_tr_id + "']"). - attr('src', '/web/static/src/img/expand.gif'); - while (1) { - var next_tr = last_tr.next(); - if (next_tr.attr('level') <= this.one_object.clicked_tr_level || next_tr.length == 0) break; - next_tr.hide(); - last_tr = next_tr; - } - } - tr_to_move.reverse(); - _.each(tr_to_move, function(rec) { - $(last_tr).after(rec); - }); - self.do_save_update_arch("down"); - } - }, - do_node_up: function(cur_tr, img) { - var self = this, side = cur_tr, tr_to_move = []; - tr_to_move.push(side); - while (1) { - var prev_tr = cur_tr.prev(); - if (this.one_object.clicked_tr_level >= parseInt(prev_tr.attr('level')) || prev_tr.length == 0) { - last_tr = prev_tr; - break; - } - cur_tr = prev_tr; - } - if (img) { - self.edit_xml_dialog.$element.find("img[id='parentimg-" + this.one_object.clicked_tr_id + "']"). - attr('src', '/web/static/src/img/expand.gif'); - while (1) { - next_tr = side.next(); - if (parseInt(next_tr.attr('level')) <= this.one_object.clicked_tr_level || next_tr.length == 0) { - break; - } else { - next_tr.hide(); - tr_to_move.push(next_tr); - side = next_tr; - } - } - } - if (last_tr.length != 0 && parseInt(last_tr.attr('level')) == this.one_object.clicked_tr_level && - (self.edit_xml_dialog.$element.find(last_tr).find('a').text()).search("view_id") == -1) { - _.each(tr_to_move, function(rec) { - $(last_tr).before(rec); - }); - self.do_save_update_arch("up"); - } - }, - do_save_update_arch: function(move_direct, update_values) { - var self = this; - var arch = _.detect(self.one_object['arch'], function(element) - {return element.view_id == self.one_object.clicked_tr_view[0]}); - var obj = self.get_object_by_id(this.one_object.clicked_tr_view[1],this.one_object['main_object'], []); - //for finding xpath tag from inherit view - var xml_arch = QWeb.load_xml(arch.arch); - if (xml_arch.childNodes[0].tagName == "data") { - var check_list = _.flatten(obj[0].child_id[0].att_list); - var children = _.filter(xml_arch.childNodes[0].childNodes, function (child) { - return child.nodeType == 1; - }); - arch.arch = _.detect(children, function(xml_child) { - var temp_obj = self.create_View_Node(xml_child), - insert = _.intersection(_.flatten(temp_obj.att_list),_.uniq(check_list)); - if (insert.length == _.uniq(check_list).length ) {return xml_child;} - }); - xml_arch = QWeb.load_xml(arch.arch); - } - return self.do_save_xml(xml_arch.documentElement, obj[0].child_id[0],obj[0].child_id, move_direct, update_values,arch); - }, - get_object_by_id: function(id, one_object, result) { - var self = this; - if (result.length == 0 ) { - var check = _.detect(one_object , function(obj) { - return id == obj.id; - }); - if (check) {result.push(check);} - _.each(one_object, function(obj) { - self.get_object_by_id(id,obj.child_id, result); - }); - } - return result; - }, - create_clone: function(clone, new_node_obj){ - var self = this; - clone.find('a').text(new_node_obj.name); - ($(clone.find('a').parent()).siblings('td')).css( "padding-left", 20 * new_node_obj.level); - clone.attr("id", "viewedit-" + new_node_obj.id); - clone.attr("level", new_node_obj.level); - clone.find("img[id^='parentimg-']").remove(); - clone.bind("click",function(){ - self.do_select_row(this.id.split('-')[1]); - }); - clone.find("img[id^='side-']").click(function() { - self.on_select_img(this); - }); - return clone; - }, - do_save_xml: function(arch1, obj, child_list, move_direct, update_values, arch){ - var self = this, children_list = $(arch1).children(),list_obj_xml; - try{list_obj_xml = _.zip(children_list, obj.child_id);}catch(err){return;} - if (this.one_object.clicked_tr_id) { - if (obj.id == this.one_object.clicked_tr_id) { - var parent = false, index = _.indexOf(child_list, obj); - if (move_direct == "down") { - var next = $(arch1).next(); - $(next).after(arch1); - var re_insert_obj = child_list.splice(index, 1); - child_list.splice(index+1, 0, re_insert_obj[0]); - parent = $(arch1).parents(); - } else if (move_direct == "up") { - var prev = $(arch1).prev(); - $(prev).before(arch1); - var re_insert_obj = child_list.splice(index, 1); - child_list.splice(index-1, 0, re_insert_obj[0]); - parent = $(arch1).parents(); - } else if (move_direct == "update_node") { - _.each(update_values, function(val){ - if (val[1]) $(arch1)[0].setAttribute(val[0], val[1]); - else $(arch1)[0].removeAttribute(val[0]); - }); - var new_obj = self.create_View_Node(arch1); - new_obj.id = obj.id,new_obj.child_id = obj.child_id; - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+this.one_object.clicked_tr_id+"']"). - find('a').text(new_obj.name); - child_list.splice(index, 1, new_obj); - parent = $(arch1).parents(); - } else if(move_direct == "add_node") { - var tr_click = self.edit_xml_dialog.$element.find("tr[id='viewedit-"+self.one_object.clicked_tr_id+"']"), - temp_xml = QWeb.load_xml(update_values[0]), - object_xml = self.create_View_Node(temp_xml.childNodes[0]); - (update_values[1] == "Inside")? object_xml.level = obj.level + 1:object_xml.level = obj.level; - var clone = self.create_clone(tr_click.clone(),object_xml), - after_append = _.detect(self.one_object['parent_child_id'],function(ele){ - return self.one_object.clicked_tr_id == ele.key; - }); - after_append = (after_append)?_.last(after_append.value):self.one_object.clicked_tr_id; - switch (update_values[1]) { - case "After": - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+after_append+"']").after(clone); - $(arch1).after($(update_values[0])); - child_list.splice(index + 1, 0, object_xml); - break; - case "Before": - tr_click.before(clone); - $(arch1).before($(update_values[0])); - child_list.splice(index - 1, 0, object_xml); - break; - case "Inside": - if (tr_click.find("img[id^='parentimg-']").length == 0) { - ($(tr_click.find('a').parent()).siblings('td')) - .append($('<img width="16" height="16"></img>').attr('src', '/web/static/src/img/collapse.gif'). - attr('id','parentimg-'+ self.one_object.clicked_tr_id).click(function(){ - self.do_parent_img_hide_show(this); - })); - } - $(arch1).append($(update_values[0])); - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+after_append+"']").after(clone); - obj.child_id.push(object_xml); - break; - } - self.edit_xml_dialog.$element. - find("tr[id='viewedit-" + object_xml.id + "']").removeClass('ui-selected'); - parent = $(arch1).parents(); - } else if (move_direct == "remove_node") { - parent = $(arch1).parents(); - if (parent.length == 0 || (parent[0].tagName.toLowerCase() == "data")) { - self.one_object.clicked_tr_id = self.one_object.clicked_tr_id -1; - self.one_object.clicked_tr_level = self.one_object.clicked_tr_level - 1; - (parent.length == 0)?parent.push("remove_view"):false; - } - $(arch1).remove(); - child_list.splice(index,1); - var cur_tr = self.edit_xml_dialog.$element. - find("tr[id='viewedit-" + self.one_object.clicked_tr_id + "']"); - _.each(self.get_list_tr(cur_tr,self.one_object.clicked_tr_level), function(tr_element){ - tr_element.remove(); - }); - cur_tr.remove(); - var parent_img = _.detect(self.one_object['parent_child_id'],function(element){ - return _.include(element.value, self.one_object.clicked_tr_id); - }); - if(parent_img.value.length == 1){ - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+parent_img.key+"']"). - find("img[id^='parentimg-']").remove(); - } - self.one_object['parent_child_id'] = self.parent_child_list(self.one_object['main_object'],[]); - } - var convert_to_utf = (parent.length != 0)? parent[parent.length-1]: arch1; - if (convert_to_utf != "remove_view") { - convert_to_utf = QWeb.tools.xml_node_to_string(convert_to_utf); - convert_to_utf = convert_to_utf.replace('xmlns="http://www.w3.org/1999/xhtml"', ""); - convert_to_utf = '<?xml version="1.0"?>' + convert_to_utf; - arch.arch = convert_to_utf; - this.dataset.write(this.one_object.clicked_tr_view[0] ,{"arch":convert_to_utf}, function(r) {}); - } else { - this.dataset.unlink([this.one_object.clicked_tr_view[0]],function(res) {}); - } - if(move_direct == "add_node"){ - self.add_node_dialog.close(); - self.on_select_img(clone.find("img[id='side-edit']")[0]); - self.one_object['parent_child_id'] = self.parent_child_list(self.one_object['main_object'],[]); - } - } - if (obj.level <= this.one_object.clicked_tr_level) { - _.each(list_obj_xml, function(child_node) { - self.do_save_xml(child_node[0], child_node[1], obj.child_id, move_direct, update_values, arch); - }); - } - } - }, - on_expand: function(expand_img){ - var level = parseInt($(expand_img).closest("tr[id^='viewedit-']").attr('level')); - var cur_tr = $(expand_img).closest("tr[id^='viewedit-']"); - _.each(this.get_list_tr(cur_tr,level), function(tr_element){ - tr_element.hide(); - }); - }, - get_list_tr: function(cur_tr,level){ - tr_list = []; - while (1) { - var nxt_tr = cur_tr.next(); - if (parseInt(nxt_tr.attr('level')) > level) { - cur_tr = nxt_tr; - tr_list.push(nxt_tr); - } else return tr_list; - } - }, - on_collapse: function(collapse_img) { - var self = this, id = collapse_img.id.split('-')[1]; - var datas = _.detect(self.one_object['parent_child_id'] , function(res) { - return res.key == id; - }); - _.each(datas.value, function (rec) { - var tr = self.edit_xml_dialog.$element.find("tr[id='viewedit-" + rec + "']"); - tr.find("img[id='parentimg-" + rec + "']").attr('src', '/web/static/src/img/expand.gif'); - tr.show(); - }); - }, - on_edit_node: function(properties){ - var self = this; - this.edit_node_dialog = new instance.web.Dialog(this,{ - title: _t("Properties"), - width: 450, - buttons: [ - {text: _t("Update"), click: function () { - var warn = false, update_values = []; - _.each(self.edit_widget, function(widget) { - if (widget.is_invalid) { - warn = true; - return false; - } - if (widget.dirty && !widget.is_invalid) { - update_values.push([widget.name, widget.get_value()]); - } - }); - if (warn) { - self.on_valid_create_view(self.edit_widget); - } else { - self.do_save_update_arch("update_node", update_values); - self.edit_node_dialog.close(); - } - }}, - {text: _t("Cancel"), click: function () { self.edit_node_dialog.close(); }} - ] - }).open(); - var _PROPERTIES_ATTRIBUTES = { - 'name' : {'name':'name', 'string': 'Name', 'type': 'char'}, - 'string' : {'name':'string', 'string': 'String', 'type': 'char'}, - 'required' : {'name':'required', 'string': 'Required', 'type': 'boolean'}, - 'readonly' : {'name':'readonly', 'string': 'Readonly', 'type': 'boolean'}, - 'invisible' : {'name':'invisible', 'string': 'Invisible', 'type': 'boolean'}, - 'domain' : {'name':'domain', 'string': 'Domain', 'type': 'char'}, - 'context' : {'name':'context', 'string': 'Context', 'type': 'char'}, - 'limit' : {'name':'limit', 'string': 'Limit', 'type': 'float'}, - 'min_rows' : {'name':'min_rows', 'string': 'Minimum rows', 'type': 'float'}, - 'date_start' : {'name':'date_start', 'string': 'Start date', 'type': 'char'}, - 'date_delay' : {'name':'date_delay', 'string': 'Delay date', 'type': 'char'}, - 'day_length' : {'name':'day_length', 'string': 'Day length', 'type': 'char'}, - 'mode' : {'name':'mode', 'string': 'Mode', 'type': 'char'}, - 'align' : {'name':'align', 'string': 'Alignment ', 'type': 'selection', 'selection': [['', ''], ['0.0', 'Left'], ['0.5', 'Center'], ['1.0', 'Right']]}, - 'icon' : {'name':'icon', 'string': 'Icon', 'type': 'selection', 'selection': _ICONS}, - 'type' : {'name':'type', 'string': 'Type', 'type': 'selection', 'selection': [['', ''], ['action', 'Action'], ['object', 'Object'], ['workflow', 'Workflow'], ['server_action', 'Server Action']]}, - 'special' : {'name':'special', 'string': 'Special', 'type': 'selection', 'selection': [['',''],['save', 'Save Button'], ['cancel', 'Cancel Button'], ['open', 'Open Button']]}, - 'target' : {'name':'target', 'string': 'Target', 'type': 'selection', 'selection': [['', ''], ['new', 'New Window']]}, - 'confirm' : {'name':'confirm', 'string': 'Confirm', 'type': 'char'}, - 'style' : {'name':'style', 'string': 'Style', 'type': 'selection', 'selection':[["",""],["1", "1"],["1-1", "1-1"],["1-2", "1-2"],["2-1", "2-1"],["1-1-1", "1-1-1"]]}, - 'filename' : {'name':'filename', 'string': 'File Name', 'type': 'char'}, - 'width' : {'name':'width', 'string': 'Width', 'type': 'float'}, - 'height' : {'name':'height', 'string': 'Height', 'type': 'float'}, - 'attrs' : {'name':'attrs', 'string': 'Attrs', 'type': 'char'}, - 'col' : {'name':'col', 'string': 'col', 'type': 'float'}, - 'link' : {'name':'link', 'string': 'Link', 'type': 'char'}, - 'position' : {'name':'position', 'string': 'Position', 'type': 'selection', 'selection': [['',''],['after', 'After'],['before', 'Before'],['inside', 'Inside'],['replace', 'Replace']]}, - 'states' : {'name':'states', 'string': 'states', 'type': 'char'}, - 'eval' : {'name':'eval', 'string': 'Eval', 'type': 'char'}, - 'ref' : {'name':'ref', 'string': 'Ref', 'type': 'char'}, - 'on_change' : {'name':'on_change', 'string': 'On change', 'type': 'char'}, - 'nolabel' : {'name':'nolabel', 'string': 'No label', 'type': 'boolean'}, - 'completion' : {'name':'completion', 'string': 'Completion', 'type': 'boolean'}, - 'colspan' : {'name':'colspan', 'string': 'Colspan', 'type': 'float'}, - 'widget' : {'name':'widget', 'string': 'widget', 'type': 'selection'}, - 'colors' : {'name':'colors', 'string': 'Colors', 'type': 'char'}, - 'editable' : {'name':'editable', 'string': 'Editable', 'type': 'selection', 'selection': [["",""],["top","Top"],["bottom", "Bottom"]]}, - 'groups' : {'name':'groups', 'string': 'Groups', 'type': 'selection_multi'}, - 'fonts' : {'name':'fonts', 'string': 'fonts', 'type': 'char'}, - }; - var arch_val = self.get_object_by_id(this.one_object.clicked_tr_id,this.one_object['main_object'], []); - this.edit_node_dialog.$element.append('<table id="rec_table" style="width:400px" class="oe_form"></table>'); - this.edit_widget = []; - self.ready = $.when(self.on_groups(properties)).then(function () { - _PROPERTIES_ATTRIBUTES['groups']['selection'] = self.groups; - var values = _.keys( instance.web.form.widgets.map); - values.push(''); - values.sort(); - _PROPERTIES_ATTRIBUTES['widget']['selection'] = values; - var widgets = _.filter(_PROPERTIES_ATTRIBUTES, function (property) { return _.include(properties, property.name)}); - _.each(widgets, function(widget) { - var type_widget = new (self.property.get_any([widget.type])) (self.edit_node_dialog, widget); - var value = _.detect(arch_val[0]['att_list'],function(res) { - return res instanceof Array? _.include(res, widget.name): false; - }); - - value = value instanceof Array ? value[1] : value; - self.edit_node_dialog.$element.find('table[id=rec_table]').append('<tr><td align="right">' + widget.string + ':</td>' + type_widget.render() + '</tr>'); - type_widget.start(); - type_widget.set_value(value); - self.edit_widget.push(type_widget); - }); - }); - }, - //for getting groups - on_groups: function(properties){ - var self = this, - def = $.Deferred(); - if (!_.include(properties, 'groups')) { - self.groups = false; - def.resolve(); - } - var group_ids = [], group_names = {}, groups = []; - var res_groups = new instance.web.DataSetSearch(this,'res.groups', null, null), - model_data = new instance.web.DataSetSearch(self,'ir.model.data', null, null); - res_groups.read_slice([], {}).done(function (res_grp) { - _.each(res_grp, function (res) { - var key = res.id; - group_names[key]=res.full_name; - group_ids.push(res.id); - }); - model_data.read_slice([], {domain: [ - ['res_id', 'in', group_ids], - ['model', '=', 'res.groups'] - ]}).done(function (model_grp) { - _.each(model_grp, function (res_group) { - groups.push([res_group.module + "." + res_group.name, group_names[res_group.res_id]]); - }); - self.groups = groups; - def.resolve(); - }); - }); - return def.promise(); - }, - on_add_node: function(properties, fields){ - var self = this; - var render_list = [{'name': 'node_type','selection': _.keys(_CHILDREN).sort(), 'value': 'field', 'string': 'Node Type','type': 'selection'}, - {'name': 'field_value','selection': fields, 'value': false, 'string': '','type': 'selection'}, - {'name': 'position','selection': ['After','Before','Inside'], 'value': false, 'string': 'Position','type': 'selection'}]; - this.add_widget = []; - this.add_node_dialog = new instance.web.Dialog(this,{ - title: _t("Properties"), - width: 450, - buttons: [ - {text: _t("Update"), click: function() { - var check_add_node = true, values = {}; - _.each(self.add_widget, function(widget) { - values[widget.name] = widget.get_value() || false; - }); - (values.position == "Inside")? - check_add_node =(_.include(_CHILDREN[properties[0]],values.node_type))?true:false: - check_add_node =(_.include(_CHILDREN[properties[1]],values.node_type))?true:false; - if(values.node_type == "field" && check_add_node ) - {check_add_node = (values.field_value != " ")?true:false; - } - if(check_add_node){ - var tag = (values.node_type == "field")? - _.str.sprintf("<%s name='%s'> </%s>",values.node_type,values.field_value,values.node_type): - _.str.sprintf("<%s> </%s>",values.node_type,values.node_type); - self.do_save_update_arch("add_node", [tag, values.position]); - } else { - alert("Can't Update View"); - } - }}, - {text: _t("Cancel"), click: function() { self.add_node_dialog.close(); }} - ] - }).open(); - this.add_node_dialog.$element.append('<table id="rec_table" style="width:420px" class="oe_form"><tbody><tr></tbody></table>'); - var table_selector = self.add_node_dialog.$element.find('table[id=rec_table] tbody'); - _.each(render_list, function(node) { - type_widget = new (self.property.get_any([node.type])) (self.add_node_dialog, node); - if (node.name == "position") { - table_selector.append('</tr><tr><td align="right" width="100px">' + node.string + '</td>' + type_widget.render() + '</tr>'); - } else { - table_selector.append('<td align="right">' + node.string + '</td>' + type_widget.render() ); - if (node.name == "field_value") { - table_selector.append('<td id="new_field" align="right" width="100px"> <button>New Field</button></td>'); - } - } - type_widget.start(); - type_widget.set_value(node.value); - self.add_widget.push(type_widget); - }); - table_selector.find("td[id^=]").attr("width","100px"); - self.add_node_dialog.$element.find('#new_field').click(function() { - model_data = new instance.web.DataSetSearch(self,'ir.model', null, null); - model_data.read_slice([], {domain: [['model','=', self.model]]}).then(function(result) { - self.render_new_field(result[0]); - }); - }); - }, - render_new_field :function( result ) { - var self = this; - var action = { - context: {'default_model_id': result.id, 'manual': true, 'module' : result.modules}, - res_model: "ir.model.fields", - views: [[false, 'form']], - type: 'ir.actions.act_window', - target: "new", - flags: { - action_buttons: true - } - }; - var action_manager = new instance.web.ActionManager(self); - $.when(action_manager.do_action(action)).then(function() { - var controller = action_manager.dialog_viewmanager.views['form'].controller; - controller.on_button_cancel.add_last(function(){ - action_manager.destroy() - }); - controller.do_save.add_last(function(){ - action_manager.destroy(); - var value =controller.fields.name.get('value'); - self.add_node_dialog.$element.find('select[id=field_value]').append($("<option selected></option>").attr("value",value).text(value)); - _.detect(self.add_widget,function(widget){ - widget.name == "field_value"? widget.selection.push(value): false; - }); - }); - }); - } -}); -instance.web.ViewEditor.Field = instance.web.Class.extend({ - init: function(view, widget) { - this.$element = view.$element; - this.dirty = false; - this.name = widget.name; - this.selection = widget.selection || []; - this.required = widget.required || false; - this.string = widget.string || ""; - this.type = widget.type; - this.is_invalid = false; - }, - start: function () { - this.update_dom(); - }, - update_dom: function() { - this.$element.find("td[id=" + this.name + "]").toggleClass('invalid', this.is_invalid); - this.$element.find("td[id=" + this.name + "]").toggleClass('required', this.required); - }, - on_ui_change: function() { - this.validate(); - this.dirty = true; - this.update_dom(); - }, - validate: function() { - this.is_invalid = false; - try { - var value = instance.web.parse_value(this.get_value(), this, ''); - this.is_invalid = this.required && value === ''; - } catch(e) { - this.is_invalid = true; - } - }, - render: function() { - return _.str.sprintf("<td id = %s>%s</td>", this.name, QWeb.render(this.template, {widget: this})) - } -}); -instance.web.ViewEditor.FieldBoolean = instance.web.ViewEditor.Field.extend({ - template : "vieweditor_boolean", - start: function() { - var self = this; - this._super(); - this.$element.find("input[id="+ self.name+"]").change(function() { - self.on_ui_change(); - }); - }, - set_value: function(value) { - if (value) { - this.$element.find("input[id=" + this.name+ "]").attr('checked', true); - } - }, - get_value: function() { - return this.$element.find("input[id=" + this.name + "]").is(':checked')? "1" : null; - } -}); -instance.web.ViewEditor.FieldChar = instance.web.ViewEditor.Field.extend({ - template : "vieweditor_char", - start: function () { - var self = this; - this._super(); - this.$element.find("input[id="+ this.name+"]").css('width','100%').change(function() { - self.on_ui_change(); - }); - }, - set_value: function(value) { - this.$element.find("input[id=" + this.name + "]").val(value); - }, - get_value: function() { - return this.$element.find("input[id=" + this.name + "]").val(); - } -}); -instance.web.ViewEditor.FieldSelect = instance.web.ViewEditor.Field.extend({ - template : "vieweditor_selection", - start: function () { - var self = this; - this._super(); - this.$element.find("select[id=" + this.name + "]").css('width', '100%').change(function() { - self.on_ui_change(); - if (self.name == "node_type") { - if (self.get_value() == "field") { - self.$element.find('#new_field').show(); - self.$element.find("select[id=field_value]").show(); - } else { - self.$element.find('#new_field').hide(); - self.$element.find("select[id=field_value]").hide(); - } - } - }); - }, - set_value: function(value) { - var index = 0; - value = value === null? false: value; - for (var i = 0, ii = this.selection.length; i < ii; i++) { - if ((this.selection[i] instanceof Array && this.selection[i][0] === value) || this.selection[i] === value) index = i; - } - this.$element.find("select[id=" + this.name + "]")[0].selectedIndex = index; - }, - get_value: function() { - return this.$element.find("select[id=" + this.name + "]").val(); - } -}); -instance.web.ViewEditor.FieldSelectMulti = instance.web.ViewEditor.FieldSelect.extend({ - start: function () { - this._super(); - this.$element.find("select[id=" + this.name + "]").css('height', '100px').attr("multiple", true); - }, - set_value: function(value) { - var self = this; - self.$element.find("#groups option").attr("selected",false); - if (!value) return false; - _.each(this.selection, function(item) { - if (_.include(value.split(','), item[0])) { - self.$element.find("select[id="+self.name+"] option[value='" + item[0] +"']").attr("selected",1) - } - }); - } -}); -instance.web.ViewEditor.FieldFloat = instance.web.ViewEditor.FieldChar.extend({ -}); - -var _PROPERTIES = { - 'field' : ['name', 'string', 'required', 'readonly','invisible', 'domain', 'context', 'nolabel', 'completion', - 'colspan', 'widget', 'eval', 'ref', 'on_change', 'attrs', 'groups'], - 'form' : ['string', 'col', 'link'], - 'notebook' : ['colspan', 'position', 'groups'], - 'page' : ['string', 'states', 'attrs', 'groups'], - 'group' : ['string', 'col', 'colspan','invisible', 'states', 'attrs', 'groups'], - 'image' : ['filename', 'width', 'height', 'groups'], - 'separator' : ['string', 'colspan', 'groups'], - 'label': ['string', 'align', 'colspan', 'groups'], - 'button': ['name', 'string', 'icon', 'type', 'states', 'readonly', 'special', 'target', 'confirm', 'context', 'attrs', 'colspan', 'groups'], - 'newline' : [], - 'board': ['style'], - 'column' : [], - 'action' : ['name', 'string', 'colspan', 'groups'], - 'tree' : ['string', 'colors', 'editable', 'link', 'limit', 'min_rows', 'fonts'], - 'graph' : ['string', 'type'], - 'calendar' : ['string', 'date_start', 'date_stop', 'date_delay', 'day_length', 'color', 'mode'] -}; -var _CHILDREN = { - 'form': ['notebook', 'group', 'field', 'label', 'button','board', 'newline', 'separator'], - 'tree': ['field'], - 'graph': ['field'], - 'calendar': ['field'], - 'notebook': ['page'], - 'page': ['notebook', 'group', 'field', 'label', 'button', 'newline', 'separator'], - 'group': ['field', 'label', 'button', 'separator', 'newline','group'], - 'board': ['column'], - 'action': [], - 'field': ['form', 'tree', 'graph','field'], - 'label': [], - 'button' : [], - 'newline': [], - 'separator': [] -}; -var _ICONS = ['','STOCK_ABOUT', 'STOCK_ADD', 'STOCK_APPLY', 'STOCK_BOLD', - 'STOCK_CANCEL', 'STOCK_CDROM', 'STOCK_CLEAR', 'STOCK_CLOSE', 'STOCK_COLOR_PICKER', - 'STOCK_CONNECT', 'STOCK_CONVERT', 'STOCK_COPY', 'STOCK_CUT', 'STOCK_DELETE', - 'STOCK_DIALOG_AUTHENTICATION', 'STOCK_DIALOG_ERROR', 'STOCK_DIALOG_INFO', - 'STOCK_DIALOG_QUESTION', 'STOCK_DIALOG_WARNING', 'STOCK_DIRECTORY', 'STOCK_DISCONNECT', - 'STOCK_DND', 'STOCK_DND_MULTIPLE', 'STOCK_EDIT', 'STOCK_EXECUTE', 'STOCK_FILE', - 'STOCK_FIND', 'STOCK_FIND_AND_REPLACE', 'STOCK_FLOPPY', 'STOCK_GOTO_BOTTOM', - 'STOCK_GOTO_FIRST', 'STOCK_GOTO_LAST', 'STOCK_GOTO_TOP', 'STOCK_GO_BACK', - 'STOCK_GO_DOWN', 'STOCK_GO_FORWARD', 'STOCK_GO_UP', 'STOCK_HARDDISK', - 'STOCK_HELP', 'STOCK_HOME', 'STOCK_INDENT', 'STOCK_INDEX', 'STOCK_ITALIC', - 'STOCK_JUMP_TO', 'STOCK_JUSTIFY_CENTER', 'STOCK_JUSTIFY_FILL', - 'STOCK_JUSTIFY_LEFT', 'STOCK_JUSTIFY_RIGHT', 'STOCK_MEDIA_FORWARD', - 'STOCK_MEDIA_NEXT', 'STOCK_MEDIA_PAUSE', 'STOCK_MEDIA_PLAY', - 'STOCK_MEDIA_PREVIOUS', 'STOCK_MEDIA_RECORD', 'STOCK_MEDIA_REWIND', - 'STOCK_MEDIA_STOP', 'STOCK_MISSING_IMAGE', 'STOCK_NETWORK', 'STOCK_NEW', - 'STOCK_NO', 'STOCK_OK', 'STOCK_OPEN', 'STOCK_PASTE', 'STOCK_PREFERENCES', - 'STOCK_PRINT', 'STOCK_PRINT_PREVIEW', 'STOCK_PROPERTIES', 'STOCK_QUIT', - 'STOCK_REDO', 'STOCK_REFRESH', 'STOCK_REMOVE', 'STOCK_REVERT_TO_SAVED', - 'STOCK_SAVE', 'STOCK_SAVE_AS', 'STOCK_SELECT_COLOR', 'STOCK_SELECT_FONT', - 'STOCK_SORT_ASCENDING', 'STOCK_SORT_DESCENDING', 'STOCK_SPELL_CHECK', - 'STOCK_STOP', 'STOCK_STRIKETHROUGH', 'STOCK_UNDELETE', 'STOCK_UNDERLINE', - 'STOCK_UNDO', 'STOCK_UNINDENT', 'STOCK_YES', 'STOCK_ZOOM_100', - 'STOCK_ZOOM_FIT', 'STOCK_ZOOM_IN', 'STOCK_ZOOM_OUT', - 'terp-account', 'terp-crm', 'terp-mrp', 'terp-product', 'terp-purchase', - 'terp-sale', 'terp-tools', 'terp-administration', 'terp-hr', 'terp-partner', - 'terp-project', 'terp-report', 'terp-stock', 'terp-calendar', 'terp-graph' -]; -instance.web.ViewEditor.property_widget = new instance.web.Registry({ - 'boolean' : 'instance.web.ViewEditor.FieldBoolean', - 'selection_multi' : 'instance.web.ViewEditor.FieldSelectMulti', - 'selection' : 'instance.web.ViewEditor.FieldSelect', - 'char' : 'instance.web.ViewEditor.FieldChar', - 'float' : 'instance.web.ViewEditor.FieldFloat' -}); -}; diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 64381a66218..69e8db19fd3 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -9,13 +9,22 @@ instance.web.form = {}; /** * Interface implemented by the form view or any other object * able to provide the features necessary for the fields to work. - * + * * Properties: * - display_invalid_fields : if true, all fields where is_valid() return true should * be displayed as invalid. + * - actual_mode : the current mode of the field manager. Can be "view", "edit" or "create". * Events: * - view_content_has_changed : when the values of the fields have changed. When * this event is triggered all fields should reprocess their modifiers. + * - field_changed:<field_name> : when the value of a field change, an event is triggered + * named "field_changed:<field_name>" with <field_name> replaced by the name of the field. + * This event is not related to the on_change mechanism of OpenERP and is always called + * when the value of a field is setted or changed. This event is only triggered when the + * value of the field is syntactically valid, but it can be triggered when the value + * is sematically invalid (ie, when a required field is false). It is possible that an event + * about a precise field is never triggered even if that field exists in the view, in that + * case the value of the field is assumed to be false. */ instance.web.form.FieldManagerMixin = { /** @@ -23,17 +32,27 @@ instance.web.form.FieldManagerMixin = { */ get_field: function(field_name) {}, /** - * Called by the field when the translate button is clicked. + * Returns the current value of a field present in the view. See the get_value() method + * method in FieldInterface for further information. */ - open_translate_dialog: function(field) {}, + get_field_value: function(field_name) {}, /** - * Returns true when the view is in create mode. - */ - is_create_mode: function() {}, + Gives new values for the fields contained in the view. The new values could not be setted + right after the call to this method. Setting new values can trigger on_changes. + + @param (dict) values A dictonnary with key = field name and value = new value. + @return (Deferred) Is resolved after all the values are setted. + */ + set_values: function(values) {}, }; instance.web.views.add('form', 'instance.web.FormView'); -instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form.FieldManagerMixin, { +/** + * Properties: + * - actual_mode: always "view", "edit" or "create". Read-only property. Determines + * the mode used by the view. + */ +instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerMixin, { /** * Indicates that this view is not searchable, and thus that no search * view should be displayed (if there is one active). @@ -55,6 +74,7 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. * @property {instance.web.Registry} registry=instance.web.form.widgets widgets registry for this form view instance */ init: function(parent, dataset, view_id, options) { + var self = this; this._super(parent); this.set_default_options(options); this.dataset = dataset; @@ -68,23 +88,43 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. this.default_focus_button = null; this.fields_registry = instance.web.form.widgets; this.tags_registry = instance.web.form.tags; + this.widgets_registry = instance.web.form.custom_widgets; this.has_been_loaded = $.Deferred(); this.translatable_fields = []; _.defaults(this.options, { "not_interactible_on_create": false, "initial_mode": "view", + "disable_autofocus": false, }); this.is_initialized = $.Deferred(); this.mutating_mutex = new $.Mutex(); this.on_change_mutex = new $.Mutex(); this.reload_mutex = new $.Mutex(); - this.rendering_engine = new instance.web.form.FormRenderingEngineReadonly(this); + this.__clicked_inside = false; + this.__blur_timeout = null; + this.rendering_engine = new instance.web.form.FormRenderingEngine(this); this.qweb = null; // A QWeb instance will be created if the view is a QWeb template + self.set({actual_mode: self.options.initial_mode}); + this.has_been_loaded.then(function() { + self.on("change:actual_mode", self, self.check_actual_mode); + self.check_actual_mode(); + self.on("change:actual_mode", self, self.init_pager); + self.init_pager(); + }); + instance.web.bus.on('clear_uncommitted_changes', this, function(e) { + if (!this.can_be_discarded()) { + e.preventDefault(); + } + }); }, destroy: function() { _.each(this.get_widgets(), function(w) { + w.off('focused blurred'); w.destroy(); }); + if (this.$el) { + this.$el.off('.formBlur'); + } this._super(); }, on_loaded: function(data) { @@ -100,51 +140,62 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. this.rendering_engine.set_fields_registry(this.fields_registry); this.rendering_engine.set_tags_registry(this.tags_registry); + this.rendering_engine.set_widgets_registry(this.widgets_registry); if (!this.extract_qweb_template(data)) { this.rendering_engine.set_fields_view(data); - var $dest = this.$element.hasClass("oe_form_container") ? this.$element : this.$element.find('.oe_form_container'); + var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container'); this.rendering_engine.render_to($dest); } + this.$el.on('mousedown.formBlur', function () { + self.__clicked_inside = true; + }); this.$buttons = $(QWeb.render("FormView.buttons", {'widget':self})); if (this.options.$buttons) { this.$buttons.appendTo(this.options.$buttons); } else { - this.$element.find('.oe_form_buttons').replaceWith(this.$buttons); + this.$el.find('.oe_form_buttons').replaceWith(this.$buttons); } this.$buttons.on('click','.oe_form_button_create',this.on_button_create); this.$buttons.on('click','.oe_form_button_edit',this.on_button_edit); this.$buttons.on('click','.oe_form_button_save',this.on_button_save); this.$buttons.on('click','.oe_form_button_cancel',this.on_button_cancel); - this.$pager = $(QWeb.render("FormView.pager", {'widget':self})); - if (this.options.$pager) { - this.$pager.appendTo(this.options.$pager); - } else { - this.$element.find('.oe_form_pager').replaceWith(this.$pager); - } - this.$pager.on('click','a[data-pager-action]',function() { - var action = $(this).data('pager-action'); - self.on_pager_action(action); - }); - - this.$sidebar = this.options.$sidebar || this.$element.find('.oe_form_sidebar'); + this.$sidebar = this.options.$sidebar || this.$el.find('.oe_form_sidebar'); if (!this.sidebar && this.options.$sidebar) { this.sidebar = new instance.web.Sidebar(this); this.sidebar.appendTo(this.$sidebar); - if(this.fields_view.toolbar) { + if (this.fields_view.toolbar) { this.sidebar.add_toolbar(this.fields_view.toolbar); } - this.sidebar.add_items('other', [ - { label: _t('Delete'), callback: self.on_button_delete }, - { label: _t('Duplicate'), callback: self.on_button_duplicate }, - { label: _t('Set Default'), callback: function (item) { self.open_defaults_dialog(); } }, - ]); - } - this.on("change:mode", this, this.switch_mode); - this.set({mode: this.options.initial_mode}); + this.sidebar.add_items('other', _.compact([ + self.is_action_enabled('delete') && { label: _t('Delete'), callback: self.on_button_delete }, + self.is_action_enabled('create') && { label: _t('Duplicate'), callback: self.on_button_duplicate }, + { label: _t('Set Default'), callback: function (item) { self.open_defaults_dialog(); } } + ])); + } + this.has_been_loaded.resolve(); + + // Add bounce effect on button 'Edit' when click on readonly page view. + this.$el.find(".oe_form_group_row,.oe_form_field,label").on('click', function (e) { + if(self.get("actual_mode") == "view") { + var $button = self.options.$buttons.find(".oe_form_button_edit"); + $button.effect('bounce', {distance: 18, times: 5}, 150); + e.stopPropagation(); + instance.web.bus.trigger('click', e); + } + }); + //bounce effect on red button when click on statusbar. + this.$el.find(".oe_form_field_status:not(.oe_form_status_clickable)").on('click', function (e) { + if((self.get("actual_mode") == "view")) { + var $button = self.$el.find(".oe_highlight:not(.oe_form_invisible)").css({'float':'left','clear':'none'}); + $button.effect('bounce', {distance:18, times: 5}, 150); + e.stopPropagation(); + } + }); + return $.when(); }, extract_qweb_template: function(fvg) { @@ -195,8 +246,32 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. this.default_focus_field = null; this.default_focus_button = null; this.translatable_fields = []; - this.$element.find('.oe_form_container').empty(); + this.$el.find('.oe_form_container').empty(); }, + + widgetFocused: function() { + // Clear click flag if used to focus a widget + this.__clicked_inside = false; + if (this.__blur_timeout) { + clearTimeout(this.__blur_timeout); + this.__blur_timeout = null; + } + }, + widgetBlurred: function() { + if (this.__clicked_inside) { + // clicked in an other section of the form (than the currently + // focused widget) => just ignore the blurring entirely? + this.__clicked_inside = false; + return; + } + var self = this; + // clear timeout, if any + this.widgetFocused(); + this.__blur_timeout = setTimeout(function () { + self.trigger('blurred'); + }, 0); + }, + do_load_state: function(state, warm) { if (state.id && this.datarecord.id != state.id) { if (!this.dataset.get_id_index(state.id)) { @@ -208,43 +283,53 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. } } }, + /** + * + * @param {Object} [options] + * @param {Boolean} [mode=undefined] If specified, switch the form to specified mode. Can be "edit" or "view". + * @param {Boolean} [reload=true] whether the form should reload its content on show, or use the currently loaded record + * @return {$.Deferred} + */ do_show: function (options) { var self = this; options = options || {}; if (this.sidebar) { - this.sidebar.$element.show(); + this.sidebar.$el.show(); } if (this.$buttons) { this.$buttons.show(); - this.$buttons.find('.oe_form_button_save').removeClass('oe_form_button_save_dirty'); } - if (this.$pager) { - this.$pager.show(); - } - this.$element.show().css('visibility', 'hidden'); - this.$element.removeClass('oe_form_dirty'); - return this.has_been_loaded.pipe(function() { - var result; - if (self.dataset.index === null) { - // null index means we should start a new record - result = self.on_button_new(); - } else { - result = self.dataset.read_index(_.keys(self.fields_view.fields), { - context : { 'bin_size' : true } - }).pipe(self.on_record_loaded); - } - result.pipe(function() { - if (options.editable) { - self.set({mode: "edit"}); + this.$el.show().css({ + opacity: '0', + filter: 'alpha(opacity = 0)' + }); + this.$el.add(this.$buttons).removeClass('oe_form_dirty'); + + var shown = this.has_been_loaded; + if (options.reload !== false) { + shown = shown.pipe(function() { + if (self.dataset.index === null) { + // null index means we should start a new record + return self.on_button_new(); } - self.$element.css('visibility', 'visible'); + var fields = _.keys(self.fields_view.fields); + fields.push('display_name'); + return self.dataset.read_index(fields, { + context: { 'bin_size': true, 'future_display_name' : true } + }).pipe(self.on_record_loaded); + }); + } + return shown.pipe(function() { + self._actualize_mode(options.mode || self.options.initial_mode); + self.$el.css({ + opacity: '1', + filter: 'alpha(opacity = 100)' }); - return result; }); }, do_hide: function () { if (this.sidebar) { - this.sidebar.$element.hide(); + this.sidebar.$el.hide(); } if (this.$buttons) { this.$buttons.hide(); @@ -257,21 +342,26 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. on_record_loaded: function(record) { var self = this, set_values = []; if (!record) { + this.set({ 'title' : undefined }); this.do_warn("Form", "The record could not be found in the database.", true); return $.Deferred().reject(); } this.datarecord = record; + this._actualize_mode(); + this.set({ 'title' : record.id ? record.display_name : "New" }); if (this.qweb) { this.kill_current_form(); this.rendering_engine.set_fields_view(this.get_fvg_from_qweb(record)); - var $dest = this.$element.hasClass("oe_form_container") ? this.$element : this.$element.find('.oe_form_container'); + var $dest = this.$el.hasClass("oe_form_container") ? this.$el : this.$el.find('.oe_form_container'); this.rendering_engine.render_to($dest); } _(this.fields).each(function (field, f) { field._dirty_flag = false; + field._inhibit_on_change_flag = true; var result = field.set_value(self.datarecord[f] || false); + field._inhibit_on_change_flag = false; set_values.push(result); }); return $.when.apply(null, set_values).pipe(function() { @@ -292,22 +382,34 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. if (self.sidebar) { self.sidebar.do_attachement_update(self.dataset, self.datarecord.id); } - if (self.default_focus_field) { - self.default_focus_field.focus(); - } if (record.id) { self.do_push_state({id:record.id}); + } else { + self.do_push_state({}); } - self.$element.removeClass('oe_form_dirty'); - self.$buttons.find('.oe_form_button_save').removeClass('oe_form_button_save_dirty'); + self.$el.add(self.$buttons).removeClass('oe_form_dirty'); + self.autofocus(); }); }, + /** + * Loads and sets up the default values for the model as the current + * record + * + * @return {$.Deferred} + */ + load_defaults: function () { + var keys = _.keys(this.fields_view.fields); + if (keys.length) { + return this.dataset.default_get(keys) + .pipe(this.on_record_loaded); + } + return this.on_record_loaded({}); + }, on_form_changed: function() { this.trigger("view_content_has_changed"); }, do_notify_change: function() { - this.$element.addClass('oe_form_dirty'); - this.$buttons.find('.oe_form_button_save').addClass('oe_form_button_save_dirty'); + this.$el.add(this.$buttons).addClass('oe_form_dirty'); }, on_pager_action: function(action) { if (this.can_be_discarded()) { @@ -328,11 +430,31 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. this.reload(); } }, + init_pager: function() { + var self = this; + if (this.$pager) + this.$pager.remove(); + if (this.get("actual_mode") === "create") + return; + this.$pager = $(QWeb.render("FormView.pager", {'widget':self})).hide(); + if (this.options.$pager) { + this.$pager.appendTo(this.options.$pager); + } else { + this.$el.find('.oe_form_pager').replaceWith(this.$pager); + } + this.$pager.on('click','a[data-pager-action]',function() { + var action = $(this).data('pager-action'); + self.on_pager_action(action); + }); + this.do_update_pager(); + }, do_update_pager: function(hide_index) { - var index = hide_index ? '-' : this.dataset.index + 1; - this.$pager.find('button').prop('disabled', this.dataset.ids.length < 2).end() - .find('span.oe_pager_index').html(index).end() - .find('span.oe_pager_count').html(this.dataset.ids.length); + this.$pager.toggle(this.dataset.ids.length > 1); + if (hide_index) { + $(".oe_form_pager_state", this.$pager).html(""); + } else { + $(".oe_form_pager_state", this.$pager).html(_.str.sprintf(_t("%d / %d"), this.dataset.index + 1, this.dataset.ids.length)); + } }, parse_on_change: function (on_change, widget) { var self = this; @@ -473,33 +595,18 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. return self.on_processed_onchange(response, processed); } catch(e) { console.error(e); + instance.webclient.crashmanager.on_javascript_exception(e); return $.Deferred().reject(); } }); }, - on_processed_onchange: function(response, processed) { + on_processed_onchange: function(result, processed) { try { - var result = response; if (result.value) { - for (var f in result.value) { - if (!result.value.hasOwnProperty(f)) { continue; } - var field = this.fields[f]; - // If field is not defined in the view, just ignore it - if (field) { - var value_ = result.value[f]; - if (field.get_value() != value_) { - field.set_value(value_); - field._dirty_flag = true; - if (!_.contains(processed, field.name)) { - this.do_onchange(field, processed); - } - } - } - } - this.on_form_changed(); + this._internal_set_values(result.value, processed); } if (!_.isEmpty(result.warning)) { - instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), { + instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), { title:result.warning.title, modal: true, buttons: [ @@ -509,6 +616,9 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. } if (result.domain) { function edit_domain(node) { + if (typeof node !== "object") { + return; + } var new_domain = result.domain[node.attrs.name]; if (new_domain) { node.attrs.domain = new_domain; @@ -520,63 +630,129 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. return $.Deferred().resolve(); } catch(e) { console.error(e); + instance.webclient.crashmanager.on_javascript_exception(e); return $.Deferred().reject(); } }, - switch_mode: function() { + _internal_set_values: function(values, exclude) { + exclude = exclude || []; + for (var f in values) { + if (!values.hasOwnProperty(f)) { continue; } + var field = this.fields[f]; + // If field is not defined in the view, just ignore it + if (field) { + var value_ = values[f]; + if (field.get_value() != value_) { + field._inhibit_on_change_flag = true; + field.set_value(value_); + field._inhibit_on_change_flag = false; + field._dirty_flag = true; + if (!_.contains(exclude, field.name)) { + this.do_onchange(field, exclude); + } + } + } + } + this.on_form_changed(); + }, + set_values: function(values) { + var self = this; + return this.on_change_mutex.exec(function() { + self._internal_set_values(values); + }); + }, + /** + * Ask the view to switch to view mode if possible. The view may not do it + * if the current record is not yet saved. It will then stay in create mode. + */ + to_view_mode: function() { + this._actualize_mode("view"); + }, + /** + * Ask the view to switch to edit mode if possible. The view may not do it + * if the current record is not yet saved. It will then stay in create mode. + */ + to_edit_mode: function() { + this._actualize_mode("edit"); + }, + /** + * Ask the view to switch to a precise mode if possible. The view is free to + * not respect this command if the state of the dataset is not compatible with + * the new mode. For example, it is not possible to switch to edit mode if + * the current record is not yet saved in database. + * + * @param {string} [new_mode] Can be "edit", "view", "create" or undefined. If + * undefined the view will test the actual mode to check if it is still consistent + * with the dataset state. + */ + _actualize_mode: function(switch_to) { + var mode = switch_to || this.get("actual_mode"); + if (! this.datarecord.id) { + mode = "create"; + } else if (mode === "create") { + mode = "edit"; + } + this.set({actual_mode: mode}); + }, + check_actual_mode: function(source, options) { var self = this; - if(this.get("mode") == "view") { - self.$element.removeClass('oe_form_editable').addClass('oe_form_readonly'); + if(this.get("actual_mode") === "view") { + self.$el.removeClass('oe_form_editable').addClass('oe_form_readonly'); self.$buttons.find('.oe_form_buttons_edit').hide(); self.$buttons.find('.oe_form_buttons_view').show(); self.$sidebar.show(); - _.each(this.fields,function(field){ - field.set({"force_readonly": true}); - }); } else { - self.$element.removeClass('oe_form_readonly').addClass('oe_form_editable'); + self.$el.removeClass('oe_form_readonly').addClass('oe_form_editable'); self.$buttons.find('.oe_form_buttons_edit').show(); self.$buttons.find('.oe_form_buttons_view').hide(); self.$sidebar.hide(); - _.each(this.fields,function(field){ - field.set({"force_readonly": false}); - }); + this.autofocus(); + } + }, + autofocus: function() { + if (this.get("actual_mode") !== "view" && !this.options.disable_autofocus) { + var fields_order = this.fields_order.slice(0); + if (this.default_focus_field) { + fields_order.unshift(this.default_focus_field.name); + } + for (var i = 0; i < fields_order.length; i += 1) { + var field = this.fields[fields_order[i]]; + if (!field.get('effective_invisible') && !field.get('effective_readonly')) { + if (field.focus() !== false) { + break; + } + } + } } }, on_button_save: function() { var self = this; return this.do_save().then(function(result) { - self.set({mode: "view"}); + self.to_view_mode(); }); }, on_button_cancel: function(event) { if (this.can_be_discarded()) { - this.set({mode: "view"}); + if (this.get('actual_mode') === 'create') { + this.trigger('history_back'); + } else { + this.to_view_mode(); + this.on_record_loaded(this.datarecord); + } } return false; }, on_button_new: function() { var self = this; - this.set({mode: "edit"}); - var def = $.Deferred(); - $.when(this.has_been_loaded).then(function() { + this.to_edit_mode(); + return $.when(this.has_been_loaded).pipe(function() { if (self.can_be_discarded()) { - var keys = _.keys(self.fields_view.fields); - if (keys.length) { - self.dataset.default_get(keys).pipe(self.on_record_loaded).then(function() { - def.resolve(); - }); - } else { - self.on_record_loaded({}).then(function() { - def.resolve(); - }); - } + return self.load_defaults(); } }); - return def.promise(); }, on_button_edit: function() { - return this.set({mode: "edit"}); + return this.to_edit_mode(); }, on_button_create: function() { this.dataset.index = null; @@ -589,7 +765,7 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) { return self.on_created({ result : new_id }); }).then(function() { - return self.set({mode: "edit"}); + return self.to_edit_mode(); }).then(function() { def.resolve(); }); @@ -614,17 +790,24 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. return def.promise(); }, can_be_discarded: function() { - return !this.$element.is('.oe_form_dirty') || confirm(_t("Warning, the record has been modified, your changes will be discarded.")); + if (this.$el.is('.oe_form_dirty')) { + if (!confirm(_t("Warning, the record has been modified, your changes will be discarded.\n\nAre you sure you want to leave this page ?"))) { + return false; + } + this.$el.removeClass('oe_form_dirty'); + } + return true; }, /** * Triggers saving the form's record. Chooses between creating a new * record or saving an existing one depending on whether the record * already has an id property. * - * @param {Function} success callback on save success - * @param {Boolean} [prepend_on_create=false] if ``do_save`` creates a new record, should that record be inserted at the start of the dataset (by default, records are added at the end) + * @param {Boolean} [prepend_on_create=false] if ``do_save`` creates a new + * record, should that record be inserted at the start of the dataset (by + * default, records are added at the end) */ - do_save: function(success, prepend_on_create) { + do_save: function(prepend_on_create) { var self = this; return this.mutating_mutex.exec(function() { return self.is_initialized.pipe(function() { try { @@ -632,13 +815,14 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. values = {}, first_invalid_field = null; for (var f in self.fields) { + if (!self.fields.hasOwnProperty(f)) { continue; } f = self.fields[f]; if (!f.is_valid()) { form_invalid = true; if (!first_invalid_field) { first_invalid_field = f; } - } else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f._dirty_flag)) { + } else if (f.name !== 'id' && (!self.datarecord.id || (!f.get("readonly") && f._dirty_flag))) { // Special case 'id' field, do not save this field // on 'create' : save all non readonly fields // on 'edit' : save non readonly modified fields @@ -647,8 +831,9 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. } if (form_invalid) { self.set({'display_invalid_fields': true}); - for (var f in self.fields) { - self.fields[f]._check_css_flags(); + for (var g in self.fields) { + if (!self.fields.hasOwnProperty(g)) { continue; } + self.fields[g]._check_css_flags(); } first_invalid_field.focus(); self.on_invalid(); @@ -657,21 +842,21 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. self.set({'display_invalid_fields': false}); var save_deferral; if (!self.datarecord.id) { - //console.log("FormView(", self, ") : About to create", values); + // Creation save save_deferral = self.dataset.create(values).pipe(function(r) { - return self.on_created(r, undefined, prepend_on_create); + return self.on_created(r, prepend_on_create); }, null); } else if (_.isEmpty(values) && ! self.force_dirty) { - //console.log("FormView(", self, ") : Nothing to save"); + // Not dirty, noop save save_deferral = $.Deferred().resolve({}).promise(); } else { self.force_dirty = false; - //console.log("FormView(", self, ") : About to save", values); + // Write save save_deferral = self.dataset.write(self.datarecord.id, values, {}).pipe(function(r) { return self.on_saved(r); }, null); } - return save_deferral.then(success); + return save_deferral; } } catch (e) { console.error(e); @@ -680,22 +865,29 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. });}); }, on_invalid: function() { - var msg = "<ul>"; - _.each(this.fields, function(f) { - if (!f.is_valid()) { - msg += "<li>" + (f.node.attrs.string || f.field.string) + "</li>"; - } - }); - msg += "</ul>"; - this.do_warn("The following fields are invalid :", msg); + var warnings = _(this.fields).chain() + .filter(function (f) { return !f.is_valid(); }) + .map(function (f) { + return _.str.sprintf('<li>%s</li>', + _.escape(f.string)); + }).value(); + warnings.unshift('<ul>'); + warnings.push('</ul>'); + this.do_warn("The following fields are invalid :", warnings.join('')); }, - on_saved: function(r, success) { + /** + * Reload the form after saving + * + * @param {Object} r result of the write function. + */ + on_saved: function(r) { if (!r.result) { // should not happen in the server, but may happen for internal purpose return $.Deferred().reject(); } else { return $.when(this.reload()).pipe(function () { - return $.when(r).then(success); }, null); + return r; + }); } }, /** @@ -708,10 +900,10 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. * * Updates the pager and sidebar displays * * @param {Object} r - * @param {Function} success callback to execute after having updated the dataset - * @param {Boolean} [prepend_on_create=false] adds the newly created record at the beginning of the dataset instead of the end + * @param {Boolean} [prepend_on_create=false] adds the newly created record + * at the beginning of the dataset instead of the end */ - on_created: function(r, success, prepend_on_create) { + on_created: function(r, prepend_on_create) { if (!r.result) { // should not happen in the server, but may happen for internal purpose return $.Deferred().reject(); @@ -728,9 +920,10 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. if (this.sidebar) { this.sidebar.do_attachement_update(this.dataset, this.datarecord.id); } - //instance.log("The record has been created with id #" + this.datarecord.id); - this.reload(); - return $.when(_.extend(r, {created: true})).then(success); + //openerp.log("The record has been created with id #" + this.datarecord.id); + return $.when(this.reload()).pipe(function () { + return _.extend(r, {created: true}); + }); } }, on_action: function (action) { @@ -746,8 +939,10 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. if (self.dataset.index == null || self.dataset.index < 0) { return $.when(self.on_button_new()); } else { - return self.dataset.read_index(_.keys(self.fields_view.fields), { - context : { 'bin_size' : true } + var fields = _.keys(self.fields_view.fields); + fields.push('display_name'); + return self.dataset.read_index(fields, { + context : { 'bin_size' : true, 'future_display_name' : true } }).pipe(self.on_record_loaded); } }); @@ -758,15 +953,15 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. }); }, get_fields_values: function(blacklist) { - blacklist = blacklist || []; + blacklist = blacklist || []; var values = {}; var ids = this.get_selected_ids(); values["id"] = ids.length > 0 ? ids[0] : false; _.each(this.fields, function(value_, key) { - if (_.include(blacklist, key)) - return; - var val = value_.get_value(); - values[key] = val; + if (_.include(blacklist, key)) { + return; + } + values[key] = value_.get_value(); }); return values; }, @@ -781,6 +976,15 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. return self.dataset.parent_view.recursive_save(); }); }, + recursive_reload: function() { + var self = this; + var pre = $.when(); + if (self.dataset.parent_view) + pre = self.dataset.parent_view.recursive_reload(); + return pre.pipe(function() { + return self.reload(); + }); + }, is_dirty: function() { return _.any(this.fields, function (value_) { return value_._dirty_flag; @@ -811,21 +1015,22 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. || field.get('invisible') || field.get("readonly") || field.field.type === 'one2many' - || field.field.type === 'many2many') { + || field.field.type === 'many2many' + || field.field.type === 'binary') { return false; } var displayed = value; - switch (field.field.type) { - case 'selection': + if (field instanceof instance.web.form.FieldSelection) { displayed = _(field.values).find(function (option) { return option[0] === value; })[1]; - break; + } else if (field instanceof instance.web.form.FieldMany2One) { + displayed = field.get_displayed(); } return { name: name, - string: field.node.attrs.string || field.field.string, + string: field.string, value: value, displayed: displayed, // convert undefined to false @@ -848,14 +1053,14 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. buttons: [ {text: _t("Close"), click: function () { d.close(); }}, {text: _t("Save default"), click: function () { - var $defaults = d.$element.find('#formview_default_fields'); + var $defaults = d.$el.find('#formview_default_fields'); var field_to_set = $defaults.val(); if (!field_to_set) { $defaults.parent().addClass('oe_form_invalid'); return; } - var condition = d.$element.find('#formview_default_conditions').val(), - all_users = d.$element.find('#formview_default_all').is(':checked'); + var condition = d.$el.find('#formview_default_conditions').val(), + all_users = d.$el.find('#formview_default_all').is(':checked'); new instance.web.DataSet(self, 'ir.values').call( 'set_default', [ self.dataset.model, @@ -874,10 +1079,22 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. register_field: function(field, name) { this.fields[name] = field; this.fields_order.push(name); + if (JSON.parse(field.node.attrs.default_focus || "0")) { + this.default_focus_field = field; + } + + field.on('focused', null, this.proxy('widgetFocused')) + .on('blurred', null, this.proxy('widgetBlurred')); if (this.get_field(name).translate) { this.translatable_fields.push(field); } field.on('changed_value', this, function() { + if (field.is_syntax_valid()) { + this.trigger('field_changed:' + name); + } + if (field._inhibit_on_change_flag) { + return; + } field._dirty_flag = true; if (field.is_syntax_valid()) { this.do_onchange(field); @@ -889,10 +1106,10 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. get_field: function(field_name) { return this.fields_view.fields[field_name]; }, - is_create_mode: function() { - return !this.datarecord.id; + get_field_value: function(field_name) { + return this.fields[field_name].get_value(); }, -})); +}); /** * Interface to be implemented by rendering engines for the form view. @@ -900,12 +1117,12 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. instance.web.form.FormRenderingEngineInterface = instance.web.Class.extend({ set_fields_view: function(fields_view) {}, set_fields_registry: function(fields_registry) {}, - render_to: function($element) {}, + render_to: function($el) {}, }); /** * Default rendering engine for the form view. - * + * * It is necessary to set the view using set_view() before usage. */ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({ @@ -914,6 +1131,10 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt }, set_fields_view: function(fvg) { this.fvg = fvg; + this.version = parseFloat(this.fvg.arch.attrs.version); + if (isNaN(this.version)) { + this.version = 6.1; + } }, set_tags_registry: function(tags_registry) { this.tags_registry = tags_registry; @@ -921,6 +1142,20 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt set_fields_registry: function(fields_registry) { this.fields_registry = fields_registry; }, + set_widgets_registry: function(widgets_registry) { + this.widgets_registry = widgets_registry; + }, + // Backward compatibility tools, current default version: v6.1 + process_version: function() { + if (this.version < 7.0) { + this.$form.find('form:first').wrapInner('<group col="4"/>'); + this.$form.find('page').each(function() { + if (!$(this).parents('field').length) { + $(this).wrapInner('<group col="4"/>'); + } + }); + } + }, render_to: function($target) { var self = this; this.$target = $target; @@ -930,8 +1165,11 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt var xml = instance.web.json_node_to_xml(this.fvg.arch); this.$form = $('<div class="oe_form">' + xml + '</div>'); + this.process_version(); + this.fields_to_init = []; this.tags_to_init = []; + this.widgets_to_init = []; this.labels = {}; this.process(this.$form); @@ -961,6 +1199,12 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); w.replace($elem); }); + _.each(this.widgets_to_init, function($elem) { + var widget_type = $elem.attr("type"); + var obj = self.widgets_registry.get_object(widget_type); + var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); + w.replace($elem); + }); // TODO: return a deferred }, render_element: function(template /* dictionaries */) { @@ -1003,6 +1247,10 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt return $tag; } }, + process_widget: function($widget) { + this.widgets_to_init.push($widget); + return $widget; + }, process_sheet: function($sheet) { var $new_sheet = this.render_element('FormRenderingSheet', $sheet.getAttributes()); this.handle_common_properties($new_sheet, $sheet); @@ -1051,7 +1299,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt if (found) return; - $label = $('<label/>').attr({ + var $label = $('<label/>').attr({ 'for' : name, "modifiers": JSON.stringify({invisible: field_modifiers.invisible}), "string": $field.attr('string'), @@ -1116,6 +1364,9 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt if (!$tr || row_cols < colspan) { $tr = $('<tr/>').addClass('oe_form_group_row').appendTo($table); row_cols = cols; + } else if (tagName==='group') { + // When <group> <group/><group/> </group>, we need a spacing between the two groups + $td.addClass('oe_group_right') } row_cols -= colspan; @@ -1141,12 +1392,6 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt $child = $td.children(':first'); switch ($child[0].tagName.toLowerCase()) { case 'separator': - if ($child.attr('orientation') === 'vertical') { - $td.addClass('oe_vertical_separator').attr('width', '1'); - $td.empty(); - row_cols-= $td.attr('colspan') || 1; - total--; - } break; case 'label': if ($child.attr('for')) { @@ -1206,7 +1451,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt page_attrs.__page = $new_page; page_attrs.__ic = ic; pages.push(page_attrs); - + $new_page.children().each(function() { self.process($(this)); }); @@ -1221,6 +1466,10 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt if (! page.__ic) return; page.__ic.on("change:effective_invisible", null, function() { + if (!page.__ic.get('effective_invisible')) { + $new_notebook.tabs('select', i); + return; + } var current = $new_notebook.tabs("option", "selected"); if (! pages[current].__ic || ! pages[current].__ic.get("effective_invisible")) return; @@ -1232,7 +1481,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } }); }); - + this.handle_common_properties($new_notebook, $notebook); return $new_notebook; }, @@ -1268,7 +1517,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt return $new_label; }, handle_common_properties: function($new_element, $node) { - var str_modifiers = $node.attr("modifiers") || "{}" + var str_modifiers = $node.attr("modifiers") || "{}"; var modifiers = JSON.parse(str_modifiers); var ic = null; if (modifiers.invisible !== undefined) @@ -1279,12 +1528,6 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt }, }); -instance.web.form.FormRenderingEngineReadonly = instance.web.form.FormRenderingEngine.extend({ - alter_field: function(field) { - field.set({"force_readonly": true}); - }, -}); - instance.web.form.FormDialog = instance.web.Dialog.extend({ init: function(parent, options, view_id, dataset) { this._super(parent, options); @@ -1297,7 +1540,7 @@ instance.web.form.FormDialog = instance.web.Dialog.extend({ this.form = new instance.web.FormView(this, this.dataset, this.view_id, { pager: false }); - this.form.appendTo(this.$element); + this.form.appendTo(this.$el); this.form.on_created.add_last(this.on_form_dialog_saved); this.form.on_saved.add_last(this.on_form_dialog_saved); return this; @@ -1389,77 +1632,115 @@ instance.web.form.compute_domain = function(expr, fields) { /** * Must be applied over an class already possessing the PropertiesMixin. * - * Apply the result of the "invisible" domain to this.$element. + * Apply the result of the "invisible" domain to this.$el. */ instance.web.form.InvisibilityChangerMixin = { init: function(field_manager, invisible_domain) { - this._ic_field_manager = field_manager + var self = this; + this._ic_field_manager = field_manager; this._ic_invisible_modifier = invisible_domain; this._ic_field_manager.on("view_content_has_changed", this, function() { - var result = this._ic_invisible_modifier === undefined ? false : - instance.web.form.compute_domain(this._ic_invisible_modifier, this._ic_field_manager.fields); - this.set({"invisible": result}); + var result = self._ic_invisible_modifier === undefined ? false : + instance.web.form.compute_domain( + self._ic_invisible_modifier, + self._ic_field_manager.fields); + self.set({"invisible": result}); }); this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false}); var check = function() { - if (this.get("invisible") || this.get('force_invisible')) { - this.set({"effective_invisible": true}); + if (self.get("invisible") || self.get('force_invisible')) { + self.set({"effective_invisible": true}); } else { - this.set({"effective_invisible": false}); + self.set({"effective_invisible": false}); } }; this.on('change:invisible', this, check); this.on('change:force_invisible', this, check); - _.bind(check, this)(); + check.call(this); }, start: function() { this.on("change:effective_invisible", this, this._check_visibility); this._check_visibility(); }, _check_visibility: function() { - this.$element.toggleClass('oe_form_invisible', this.get("effective_invisible")); + this.$el.toggleClass('oe_form_invisible', this.get("effective_invisible")); }, }; -instance.web.form.InvisibilityChanger = instance.web.Class.extend(_.extend({}, instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, { - init: function(parent, field_manager, invisible_domain, $element) { +instance.web.form.InvisibilityChanger = instance.web.Class.extend(instance.web.PropertiesMixin, instance.web.form.InvisibilityChangerMixin, { + init: function(parent, field_manager, invisible_domain, $el) { this.setParent(parent); instance.web.PropertiesMixin.init.call(this); instance.web.form.InvisibilityChangerMixin.init.call(this, field_manager, invisible_domain); - this.$element = $element; + this.$el = $el; this.start(); }, -})); +}); + +/** + Base class for all fields, custom widgets and buttons to be displayed in the form view. -instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance.web.form.InvisibilityChangerMixin, { + Properties: + - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending + the values of the "readonly" property and the "mode" property on the field manager. +*/ +instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.InvisibilityChangerMixin, { /** * @constructs instance.web.form.FormWidget * @extends instance.web.Widget * - * @param view + * @param field_manager * @param node */ - init: function(view, node) { - this._super(view); - this.view = view; + init: function(field_manager, node) { + this._super(field_manager); + this.view = field_manager; + this.field_manager = field_manager; this.node = node; this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}'); - instance.web.form.InvisibilityChangerMixin.init.call(this, view, this.modifiers.invisible); + instance.web.form.InvisibilityChangerMixin.init.call(this, this.field_manager, this.modifiers.invisible); + + this.field_manager.on("view_content_has_changed", this, this.process_modifiers); - this.view.on("view_content_has_changed", this, this.process_modifiers); + this.set({required: this.modifiers['required'] === true}); + // some events to make the property "effective_readonly" sync automatically with "readonly" and + // "mode" on field_manager + var self = this; + this.set({"readonly": this.modifiers['readonly'] === true}); + var test_effective_readonly = function() { + self.set({"effective_readonly": self.get("readonly") || self.field_manager.get("actual_mode") === "view"}); + }; + this.on("change:readonly", this, test_effective_readonly); + this.field_manager.on("change:actual_mode", this, test_effective_readonly); + test_effective_readonly.call(this); }, renderElement: function() { this._super(); - this.$element.addClass(this.node.attrs["class"] || ""); + this.$el.addClass(this.node.attrs["class"] || ""); }, destroy: function() { $.fn.tipsy.clear(); this._super.apply(this, arguments); }, + /** + * Sets up blur/focus forwarding from DOM elements to a widget (`this`). + * + * This method is an utility method that is meant to be called by child classes. + * + * @param {jQuery} $e jQuery object of elements to bind focus/blur on + */ + setupFocus: function ($e) { + var self = this; + $e.on({ + focus: function () { self.trigger('focused'); }, + blur: function () { self.trigger('blurred'); } + }); + }, process_modifiers: function() { var compute_domain = instance.web.form.compute_domain; var to_set = {}; for (var a in this.modifiers) { + if (!this.modifiers.hasOwnProperty(a)) { continue; } if (!_.include(["invisible"], a)) { var val = compute_domain(this.modifiers[a], this.view.fields); to_set[a] = val; @@ -1469,7 +1750,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. }, do_attach_tooltip: function(widget, trigger, options) { widget = widget || this; - trigger = trigger || this.$element; + trigger = trigger || this.$el; options = _.extend({ delayIn: 500, delayOut: 0, @@ -1480,7 +1761,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. template = 'WidgetLabel.tooltip'; } return QWeb.render(template, { - debug: instance.connection.debug, + debug: instance.session.debug, widget: widget })}, gravity: $.fn.tipsy.autoBounds(50, 'nw'), @@ -1501,7 +1782,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. parent: {} }); if (a_dataset.parent_view) { - fields_values.parent = a_dataset.parent_view.get_fields_values([a_dataset.child_name]); + fields_values.parent = a_dataset.parent_view.get_fields_values([a_dataset.child_name]); } return fields_values; }, @@ -1519,7 +1800,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. if (! v_context) { v_context = (this.field || {}).context || {}; } - + if (v_context.__ref || true) { //TODO: remove true var fields_values = this._build_eval_context(blacklist); v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values); @@ -1537,26 +1818,30 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. } return final_domain; } -})); +}); instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ template: 'WidgetButton', - init: function(view, node) { - this._super(view, node); + init: function(field_manager, node) { + this._super(field_manager, node); this.force_disabled = false; this.string = (this.node.attrs.string || '').replace(/_/g, ''); - if (this.node.attrs.default_focus == '1') { + if (JSON.parse(this.node.attrs.default_focus || "0")) { // TODO fme: provide enter key binding to widgets this.view.default_focus_button = this; } + if (this.node.attrs.icon && (! /\//.test(this.node.attrs.icon))) { + this.node.attrs.icon = '/web/static/src/img/icons/' + this.node.attrs.icon + '.png'; + } this.view.on('view_content_has_changed', this, this.check_disable); }, start: function() { this._super.apply(this, arguments); - this.$element.click(this.on_click); - if (this.node.attrs.help || instance.connection.debug) { + this.$el.click(this.on_click); + if (this.node.attrs.help || instance.session.debug) { this.do_attach_tooltip(); } + this.setupFocus(this.$el); }, on_click: function() { var self = this; @@ -1605,37 +1890,30 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ on_confirmed: function() { var self = this; - var context = this.node.attrs.context; - if (context && context.__ref) { - context = new instance.web.CompoundContext(context); - context.set_eval_context(this._build_eval_context()); - } + var context = this.build_context(); return this.view.do_execute_action( _.extend({}, this.node.attrs, {context: context}), this.view.dataset, this.view.datarecord.id, function () { - self.view.reload(); + self.view.recursive_reload(); }); }, check_disable: function() { var disabled = (this.force_disabled || !this.view.is_interactible_record()); - this.$element.prop('disabled', disabled); - this.$element.css('color', disabled ? 'grey' : ''); + this.$el.prop('disabled', disabled); + this.$el.css('color', disabled ? 'grey' : ''); } }); /** * Interface to be implemented by fields. - * - * Properties: - * - readonly: boolean. If set to true the field should appear in readonly mode. - * - force_readonly: boolean, When it is true, the field should always appear - * in read only mode, no matter what the value of the "readonly" property can be. + * * Events: - * - changed_value: triggered to inform the view to check on_changes - * + * - changed_value: triggered when the value of the field has changed. This can be due + * to a user interaction or a call to set_value(). + * */ -instance.web.form.FieldMixin = { +instance.web.form.FieldInterface = { /** * Constructor takes 2 arguments: * - field_manager: Implements FieldManagerMixin @@ -1644,14 +1922,14 @@ instance.web.form.FieldMixin = { init: function(field_manager, node) {}, /** * Called by the form view to indicate the value of the field. - * + * * set_value() may return an object that can be passed to $.when() that represents the moment when * the field has finished all operations necessary before the user can effectively use the widget. - * + * * Multiple calls to set_value() can occur at any time and must be handled correctly by the implementation, * regardless of any asynchronous operation currently running and the status of any promise that a * previous call to set_value() could have returned. - * + * * set_value() must be able, at any moment, to handle the syntax returned by the "read" method of the * osv class in the OpenERP server as well as the syntax used by the set_value() (see below). It must * also be able to handle any other format commonly used in the _defaults key on the models in the addons @@ -1661,19 +1939,19 @@ instance.web.form.FieldMixin = { set_value: function(value_) {}, /** * Get the current value of the widget. - * - * Must always return a syntaxically correct value to be passed to the "write" method of the osv class in + * + * Must always return a syntactically correct value to be passed to the "write" method of the osv class in * the OpenERP server, although it is not assumed to respect the constraints applied to the field. - * For example if the field is marqued as "required", a call to get_value() can return false. - * + * For example if the field is marked as "required", a call to get_value() can return false. + * * get_value() can also be called *before* a call to set_value() and, in that case, is supposed to - * return a defaut value according to the type of field. - * + * return a default value according to the type of field. + * * This method is always assumed to perform synchronously, it can not return a promise. - * + * * If there was no user interaction to modify the value of the field, it is always assumed that * get_value() return the same semantic value than the one passed in the last call to set_value(), - * altough the syntax can be different. This can be the case for type of fields that have a different + * although the syntax can be different. This can be the case for type of fields that have a different * syntax for "read" and "write" (example: m2o: set_value([0, "Administrator"]), get_value() => 0). */ get_value: function() {}, @@ -1688,28 +1966,30 @@ instance.web.form.FieldMixin = { */ is_valid: function() {}, /** - * Returns true if the field holds a value which is syntaxically correct, ignoring + * Returns true if the field holds a value which is syntactically correct, ignoring * the potential semantic restrictions applied to the field. */ is_syntax_valid: function() {}, /** - * Must set the focus on the field. + * Must set the focus on the field. Return false if field is not focusable. */ focus: function() {}, + /** + * Called when the translate button is clicked. + */ + on_translate: function() {}, }; /** - * Abstract class for classes implementing FieldMixin. - * + * Abstract class for classes implementing FieldInterface. + * * Properties: - * - effective_readonly: when it is true, the widget is displayed as readonly. Vary depending - * the values of the "readonly" property and the "force_readonly" property on the field manager. * - value: useful property to hold the value of the field. By default, set_value() and get_value() * set and retrieve the value property. Changing the value property also triggers automatically * a 'changed_value' event that inform the view to trigger on_changes. - * + * */ -instance.web.form.AbstractField = instance.web.form.FormWidget.extend(_.extend({}, instance.web.form.FieldMixin, { +instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.web.form.FieldInterface, { /** * @constructs instance.web.form.AbstractField * @extends instance.web.form.FormWidget @@ -1718,26 +1998,17 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(_.extend({ * @param node */ init: function(field_manager, node) { + var self = this this._super(field_manager, node); - this.field_manager = field_manager; this.name = this.node.attrs.name; - this.set({'value': false}); this.field = this.field_manager.get_field(this.name); - this.set({required: this.modifiers['required'] === true}); - - // some events to make the property "effective_readonly" sync automatically with "readonly" and - // "force_readonly" - this.set({"readonly": this.modifiers['readonly'] === true}); - var test_effective_readonly = function() { - this.set({"effective_readonly": this.get("readonly") || !!this.get("force_readonly")}); - }; - this.on("change:readonly", this, test_effective_readonly); - this.on("change:force_readonly", this, test_effective_readonly); - _.bind(test_effective_readonly, this)(); - + this.widget = this.node.attrs.widget; + this.string = this.node.attrs.string || this.field.string || this.name; + this.options = JSON.parse(this.node.attrs.options || '{}'); + this.set({'value': false}); + this.on("change:value", this, function() { - if (! this._inhibit_on_change) - this.trigger('changed_value'); + this.trigger('changed_value'); this._check_css_flags(); }); }, @@ -1745,14 +2016,12 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(_.extend({ var self = this; this._super(); if (this.field.translate) { - this.$element.addClass('oe_form_field_translatable'); - this.$element.find('.oe_field_translate').click(_.bind(function() { - this.field_manager.open_translate_dialog(this); - }, this)); + this.$el.addClass('oe_form_field_translatable'); + this.$el.find('.oe_field_translate').click(this.on_translate); } - this.$label = this.view.$element.find('label[for=' + this.id_for_label + ']'); - if (instance.connection.debug) { - this.do_attach_tooltip(this, this.$label[0] || this.$element); + this.$label = this.view.$el.find('label[for=' + this.id_for_label + ']'); + if (instance.session.debug) { + this.do_attach_tooltip(this, this.$label[0] || this.$el); this.$label.off('dblclick').on('dblclick', function() { console.log("Field '%s' of type '%s' in View: %o", self.name, (self.node.attrs.widget || self.field.type), self.view); window.w = self; @@ -1771,18 +2040,16 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(_.extend({ * Private. Do not use. */ _set_required: function() { - this.$element.toggleClass('oe_form_required', this.get("required")); + this.$el.toggleClass('oe_form_required', this.get("required")); }, set_value: function(value_) { - this._inhibit_on_change = true; this.set({'value': value_}); - this._inhibit_on_change = false; }, get_value: function() { return this.get('value'); }, is_valid: function() { - return this.is_syntax_valid() && (! this.get('required') || ! this.is_false()); + return this.is_syntax_valid() && !(this.get('required') && this.is_false()); }, is_syntax_valid: function() { return true; @@ -1794,46 +2061,36 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(_.extend({ is_false: function() { return this.get('value') === false; }, - _check_css_flags: function(show_invalid) { + _check_css_flags: function() { if (this.field.translate) { - this.$element.find('.oe_field_translate').toggle(!this.field_manager.is_create_mode()); + this.$el.find('.oe_field_translate').toggle(this.field_manager.get('actual_mode') !== "create"); } if (!this.disable_utility_classes) { if (this.field_manager.get('display_invalid_fields')) { - this.$element.toggleClass('oe_form_invalid', !this.is_valid()); + this.$el.toggleClass('oe_form_invalid', !this.is_valid()); } } }, focus: function() { - }, - /** - * Utility method to focus an element, but only after a small amount of time. - */ - delay_focus: function($elem) { - setTimeout(function() { - $elem.focus(); - }, 50); - }, - /** - * Utility method to get the widget options defined in the field xml description. - */ - get_definition_options: function() { - if (!this.definition_options) { - var str = this.node.attrs.options || '{}'; - this.definition_options = JSON.parse(str); - } - return this.definition_options; + return false; }, set_input_id: function(id) { this.id_for_label = id; }, -})); + on_translate: function() { + var self = this; + var trans = new instance.web.DataSet(this, 'ir.translation'); + return trans.call_button('translate_fields', [this.view.dataset.model, this.view.datarecord.id, this.name, this.view.dataset.get_context()]).then(function(r) { + self.do_action(r.result); + }); + }, +}); /** - * A mixin to apply on any field that has to completely re-render when its readonly state + * A mixin to apply on any FormWidget that has to completely re-render when its readonly state * switch. */ -instance.web.form.ReinitializeFieldMixin = { +instance.web.form.ReinitializeWidgetMixin = { /** * Default implementation of start(), use it or call explicitly initialize_field(). */ @@ -1846,10 +2103,8 @@ instance.web.form.ReinitializeFieldMixin = { this.destroy_content(); this.renderElement(); this.initialize_content(); - this.render_value(); }); this.initialize_content(); - this.render_value(); }, /** * Called to destroy anything that could have been created previously, called before a @@ -1860,13 +2115,27 @@ instance.web.form.ReinitializeFieldMixin = { * Called to initialize the content. */ initialize_content: function() {}, +}; + +/** + * A mixin to apply on any field that has to completely re-render when its readonly state + * switch. + */ +instance.web.form.ReinitializeFieldMixin = _.extend({}, instance.web.form.ReinitializeWidgetMixin, { + initialize_field: function() { + instance.web.form.ReinitializeWidgetMixin.initialize_field.call(this); + this.on("change:effective_readonly", this, function() { + this.render_value(); + }); + this.render_value(); + }, /** * Called to render the value. Should also be explicitly called at the end of a set_value(). */ render_value: function() {}, -}; +}); -instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldChar', widget_class: 'oe_form_field_char', init: function (field_manager, node) { @@ -1875,9 +2144,11 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({} }, initialize_content: function() { var self = this; - this.$element.find('input').change(function() { - self.set({'value': instance.web.parse_value(self.$element.find('input').val(), self)}); + var $input = this.$el.find('input'); + $input.change(function() { + self.set({'value': instance.web.parse_value($input.val(), self)}); }); + this.setupFocus($input); }, set_value: function(value_) { this._super(value_); @@ -1886,18 +2157,18 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({} render_value: function() { var show_value = instance.web.format_value(this.get('value'), this, ''); if (!this.get("effective_readonly")) { - this.$element.find('input').val(show_value); + this.$el.find('input').val(show_value); } else { if (this.password) { show_value = new Array(show_value.length + 1).join('*'); } - this.$element.text(show_value); + this.$el.text(show_value); } }, is_syntax_valid: function() { if (!this.get("effective_readonly")) { try { - var value_ = instance.web.parse_value(this.$element.find('input').val(), this, ''); + var value_ = instance.web.parse_value(this.$el.find('input').val(), this, ''); return true; } catch(e) { return false; @@ -1909,27 +2180,29 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({} return this.get('value') === '' || this._super(); }, focus: function() { - this.delay_focus(this.$element.find('input:first')); + this.$('input:first').focus(); } -})); +}); instance.web.form.FieldID = instance.web.form.FieldChar.extend({ - + }); instance.web.form.FieldEmail = instance.web.form.FieldChar.extend({ template: 'FieldEmail', initialize_content: function() { this._super(); - this.$element.find('button').click(this.on_button_clicked); + var $button = this.$el.find('button'); + $button.click(this.on_button_clicked); + this.setupFocus($button); }, render_value: function() { if (!this.get("effective_readonly")) { this._super(); } else { - this.$element.find('a') + this.$el.find('a') .attr('href', 'mailto:' + this.get('value')) - .text(this.get('value')); + .text(this.get('value') || ''); } }, on_button_clicked: function() { @@ -1945,7 +2218,9 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({ template: 'FieldUrl', initialize_content: function() { this._super(); - this.$element.find('button').click(this.on_button_clicked); + var $button = this.$el.find('button'); + $button.click(this.on_button_clicked); + this.setupFocus($button); }, render_value: function() { if (!this.get("effective_readonly")) { @@ -1956,7 +2231,7 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({ if (!s) { tmp = "http://" + this.get('value'); } - this.$element.find('a').attr('href', tmp).text(tmp); + this.$el.find('a').attr('href', tmp).text(this.get('value') ? tmp : ''); } }, on_button_clicked: function() { @@ -1978,7 +2253,7 @@ instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({ this._super(field_manager, node); this.set({'value': 0}); if (this.node.attrs.digits) { - this.digits = py.eval(node.attrs.digits); + this.digits = this.node.attrs.digits; } else { this.digits = this.field.digits; } @@ -1989,10 +2264,13 @@ instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({ value_ = 0; } this._super.apply(this, [value_]); + }, + focus: function () { + this.$('input:first').select(); } }); -instance.web.DateTimeWidget = instance.web.OldWidget.extend({ +instance.web.DateTimeWidget = instance.web.Widget.extend({ template: "web.datepicker", jqueryui_object: 'datetimepicker', type_of_date: "datetime", @@ -2002,23 +2280,27 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ }, start: function() { var self = this; - this.$input = this.$element.find('input.oe_datepicker_master'); - this.$input_picker = this.$element.find('input.oe_datepicker_container'); + this.$input = this.$el.find('input.oe_datepicker_master'); + this.$input_picker = this.$el.find('input.oe_datepicker_container'); this.$input.change(this.on_change); this.picker({ + onClose: this.on_picker_select, onSelect: this.on_picker_select, changeMonth: true, changeYear: true, showWeek: true, - showButtonPanel: true + showButtonPanel: true, + firstDay: Date.CultureInfo.firstDayOfWeek }); - this.$element.find('img.oe_datepicker_trigger').click(function() { - if (!self.get("effective_readonly") && !self.picker('widget').is(':visible')) { - self.picker('setDate', self.get('value') ? instance.web.auto_str_to_date(self.get('value')) : new Date()); - self.$input_picker.show(); - self.picker('show'); - self.$input_picker.hide(); + this.$el.find('img.oe_datepicker_trigger').click(function() { + if (self.get("effective_readonly") || self.picker('widget').is(':visible')) { + self.$input.focus(); + return; } + self.picker('setDate', self.value ? instance.web.auto_str_to_date(self.value) : new Date()); + self.$input_picker.show(); + self.picker('show'); + self.$input_picker.hide(); }); this.set_readonly(false); this.set({'value': false}); @@ -2028,7 +2310,10 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ }, on_picker_select: function(text, instance_) { var date = this.picker('getDate'); - this.$input.val(date ? this.format_client(date) : '').change(); + this.$input + .val(date ? this.format_client(date) : '') + .change() + .focus(); }, set_value: function(value_) { this.set({'value': value_}); @@ -2044,7 +2329,7 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ set_readonly: function(readonly) { this.readonly = readonly; this.$input.prop('readonly', this.readonly); - this.$element.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly); + this.$el.find('img.oe_datepicker_trigger').toggleClass('oe_input_icon_disabled', readonly); }, is_valid_: function() { var value_ = this.$input.val(); @@ -2077,7 +2362,7 @@ instance.web.DateWidget = instance.web.DateTimeWidget.extend({ type_of_date: "date" }); -instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: "FieldDatetime", build_widget: function() { return new instance.web.DateTimeWidget(this); @@ -2094,7 +2379,8 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.exten this.datewidget.on_change.add_last(_.bind(function() { this.set({'value': this.datewidget.get_value()}); }, this)); - this.datewidget.appendTo(this.$element); + this.datewidget.appendTo(this.$el); + this.setupFocus(this.datewidget.$input); } }, set_value: function(value_) { @@ -2105,7 +2391,7 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.exten if (!this.get("effective_readonly")) { this.datewidget.set_value(this.get('value')); } else { - this.$element.text(instance.web.format_value(this.get('value'), this, '')); + this.$el.text(instance.web.format_value(this.get('value'), this, '')); } }, is_syntax_valid: function() { @@ -2118,10 +2404,11 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.exten return this.get('value') === '' || this._super(); }, focus: function() { - if (this.datewidget && this.datewidget.$input) - this.delay_focus(this.datewidget.$input); + if (this.datewidget && this.datewidget.$input) { + this.datewidget.$input.focus(); + } } -})); +}); instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({ template: "FieldDate", @@ -2130,28 +2417,41 @@ instance.web.form.FieldDate = instance.web.form.FieldDatetime.extend({ } }); -instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldText', + init: function (field_manager, node) { + this._super(field_manager, node); + }, initialize_content: function() { - this.$textarea = this.$element.find('textarea'); + var self = this; + this.$textarea = this.$el.find('textarea'); + this.default_height = this.$textarea.css('height'); if (!this.get("effective_readonly")) { this.$textarea.change(_.bind(function() { - this.set({'value': instance.web.parse_value(this.$textarea.val(), this)}); + self.set({'value': instance.web.parse_value(self.$textarea.val(), self)}); }, this)); } else { this.$textarea.attr('disabled', 'disabled'); } + this.$el.keyup(function (e) { + if (e.which === $.ui.keyCode.ENTER) { + e.stopPropagation(); + } + }); + this.setupFocus(this.$textarea); }, set_value: function(value_) { - this._super.apply(this, arguments); + this._super(value_); this.render_value(); + $(window).resize(); }, render_value: function() { var show_value = instance.web.format_value(this.get('value'), this, ''); - this.$textarea.val(show_value); - if (show_value && this.view.options.resize_textareas) { - this.do_resize(this.view.options.resize_textareas); + if (show_value === '') { + this.$textarea.css('height', parseInt(this.default_height)+"px"); } + this.$textarea.val(show_value); + this.$textarea.autosize(); }, is_syntax_valid: function() { if (!this.get("effective_readonly")) { @@ -2167,51 +2467,87 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({} is_false: function() { return this.get('value') === '' || this._super(); }, - focus: function($element) { - this.delay_focus(this.$textarea); - }, - do_resize: function(max_height) { - max_height = parseInt(max_height, 10); - var $input = this.$textarea, - $div = $('<div style="position: absolute; z-index: 1000; top: 0"/>').width($input.width()), - new_height; - $div.text($input.val()); - _.each('font-family,font-size,white-space'.split(','), function(style) { - $div.css(style, $input.css(style)); - }); - $div.appendTo($('body')); - new_height = $div.height(); - if (new_height < 90) { - new_height = 90; + focus: function($el) { + this.$textarea.focus(); + }, +}); + +/** + * FieldTextHtml Widget + * Intended for FieldText widgets meant to display HTML content. This + * widget will instantiate the CLEditor (see cleditor in static/src/lib) + * To find more information about CLEditor configutation: go to + * http://premiumsoftware.net/cleditor/docs/GettingStarted.html + */ +instance.web.form.FieldTextHtml = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { + template: 'FieldTextHtml', + init: function() { + this._super.apply(this, arguments); + }, + initialize_content: function() { + var self = this; + if (! this.get("effective_readonly")) { + self._updating_editor = false; + this.$textarea = this.$el.find('textarea'); + var width = ((this.node.attrs || {}).editor_width || 468); + var height = ((this.node.attrs || {}).editor_height || 250); + this.$textarea.cleditor({ + width: width, // width not including margins, borders or padding + height: height, // height not including margins, borders or padding + controls: // controls to add to the toolbar + "bold italic underline strikethrough " + + "| removeformat | bullets numbering | outdent " + + "indent | link unlink | source", + bodyStyle: // style to assign to document body contained within the editor + "margin:4px; font:12px monospace; cursor:text; color:#1F1F1F" + }); + this.$cleditor = this.$textarea.cleditor()[0]; + this.$cleditor.change(function() { + if (! self._updating_editor) { + self.$cleditor.updateTextArea(); + self.set({'value': self.$textarea.val()}); + } + }); } - if (!isNaN(max_height) && new_height > max_height) { - new_height = max_height; + }, + set_value: function(value_) { + this._super.apply(this, arguments); + this.render_value(); + }, + render_value: function() { + if (! this.get("effective_readonly")) { + this.$textarea.val(this.get('value') || ''); + this._updating_editor = true; + this.$cleditor.updateFrame(); + this._updating_editor = false; + } else { + this.$el.html(this.get('value')); } - $div.remove(); - $input.height(new_height); }, -})); +}); instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ template: 'FieldBoolean', start: function() { + var self = this; this._super.apply(this, arguments); - this.$checkbox = $("input", this.$element); - this.$element.click(_.bind(function() { + this.$checkbox = $("input", this.$el); + this.setupFocus(this.$checkbox); + this.$el.click(_.bind(function() { this.set({'value': this.$checkbox.is(':checked')}); }, this)); var check_readonly = function() { - this.$checkbox.prop('disabled', this.get("effective_readonly")); + self.$checkbox.prop('disabled', self.get("effective_readonly")); }; this.on("change:effective_readonly", this, check_readonly); - _.bind(check_readonly, this)(); + check_readonly.call(this); }, set_value: function(value_) { this._super.apply(this, arguments); this.$checkbox[0].checked = value_; }, focus: function() { - this.delay_focus(this.$checkbox); + this.$checkbox.focus(); } }); @@ -2219,7 +2555,7 @@ instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({ template: 'FieldProgressBar', start: function() { this._super.apply(this, arguments); - this.$element.progressbar({ + this.$el.progressbar({ value: this.get('value'), disabled: this.get("effective_readonly") }); @@ -2231,15 +2567,12 @@ instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({ show_value = 0; } var formatted_value = instance.web.format_value(show_value, { type : 'float' }, '0'); - this.$element.progressbar('option', 'value', show_value).find('span').html(formatted_value + '%'); + this.$el.progressbar('option', 'value', show_value).find('span').html(formatted_value + '%'); } }); -instance.web.form.FieldTextXml = instance.web.form.AbstractField.extend({ -// to replace view editor -}); -instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldSelection', init: function(field_manager, node) { var self = this; @@ -2265,9 +2598,9 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte // changing the selected value), takes the action as validating the // row var ischanging = false; - this.$element.find('select') + var $select = this.$el.find('select') .change(_.bind(function() { - this.set({'value': this.values[this.$element.find('select')[0].selectedIndex][0]}); + this.set({'value': this.values[this.$el.find('select')[0].selectedIndex][0]}); }, this)) .change(function () { ischanging = true; }) .click(function () { ischanging = false; }) @@ -2276,6 +2609,7 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte e.stopPropagation(); ischanging = false; }); + this.setupFocus($select); }, set_value: function(value_) { value_ = value_ === null ? false : value_; @@ -2289,27 +2623,27 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte for (var i = 0, ii = this.values.length; i < ii; i++) { if (this.values[i][0] === this.get('value')) index = i; } - this.$element.find('select')[0].selectedIndex = index; + this.$el.find('select')[0].selectedIndex = index; } else { var self = this; var option = _(this.values) - .detect(function (record) { return record[0] === self.get('value'); }); - this.$element.text(option ? option[1] : this.values[0][1]); + .detect(function (record) { return record[0] === self.get('value'); }); + this.$el.text(option ? option[1] : this.values[0][1]); } }, is_syntax_valid: function() { if (this.get("effective_readonly")) { return true; } - var value_ = this.values[this.$element.find('select')[0].selectedIndex]; + var value_ = this.values[this.$el.find('select')[0].selectedIndex]; return !! value_; }, focus: function() { - this.delay_focus(this.$element.find('select:first')); + this.$el.find('select:first').focus(); } -})); +}); -// jquery autocomplete tweak to allow html +// jquery autocomplete tweak to allow html and classnames (function() { var proto = $.ui.autocomplete.prototype, initSource = proto._initSource; @@ -2336,7 +2670,8 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(_.exte return $( "<li></li>" ) .data( "item.autocomplete", item ) .append( $( "<a></a>" )[ this.options.html ? "html" : "text" ]( item.label ) ) - .appendTo( ul ); + .appendTo( ul ) + .addClass(item.classname); } }); })(); @@ -2357,43 +2692,56 @@ instance.web.form.CompletionFieldMixin = { var dataset = new instance.web.DataSet(this, this.field.relation, self.build_context()); var blacklist = this.get_search_blacklist(); + this.last_query = search_val; return this.orderer.add(dataset.name_search( search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]), - 'ilike', this.limit + 1)).pipe(function(data) { + 'ilike', this.limit + 1, self.build_context())).pipe(function(data) { self.last_search = data; // possible selections for the m2o var values = _.map(data, function(x) { + x[1] = x[1].split("\n")[0]; return { label: _.str.escapeHTML(x[1]), - value:x[1], - name:x[1], - id:x[0] + value: x[1], + name: x[1], + id: x[0], }; }); // search more... if more results that max if (values.length > self.limit) { values = values.slice(0, self.limit); - values.push({label: _t("<em>Â Â Â Search More...</em>"), action: function() { - dataset.name_search(search_val, self.build_domain(), 'ilike' - , false, function(data) { - self._search_create_popup("search", data); - }); - }}); + values.push({ + label: _t("Search More..."), + action: function() { + dataset.name_search(search_val, self.build_domain(), 'ilike', false).then(function(data) { + self._search_create_popup("search", data); + }); + }, + classname: 'oe_m2o_dropdown_option' + }); } // quick create var raw_result = _(data.result).map(function(x) {return x[1];}); if (search_val.length > 0 && !_.include(raw_result, search_val)) { - values.push({label: _.str.sprintf(_t('<em>Â Â Â Create "<strong>%s</strong>"</em>'), - $('<span />').text(search_val).html()), action: function() { - self._quick_create(search_val); - }}); + values.push({ + label: _.str.sprintf(_t('Create "<strong>%s</strong>"'), + $('<span />').text(search_val).html()), + action: function() { + self._quick_create(search_val); + }, + classname: 'oe_m2o_dropdown_option' + }); } // create... - values.push({label: _t("<em>Â Â Â Create and Edit...</em>"), action: function() { - self._search_create_popup("form", undefined, {"default_name": search_val}); - }}); + values.push({ + label: _t("Create and Edit..."), + action: function() { + self._search_create_popup("form", undefined, self._create_context(search_val)); + }, + classname: 'oe_m2o_dropdown_option' + }); return values; }); @@ -2404,11 +2752,11 @@ instance.web.form.CompletionFieldMixin = { _quick_create: function(name) { var self = this; var slow_create = function () { - self._search_create_popup("form", undefined, {"default_name": name}); + self._search_create_popup("form", undefined, self._create_context(name)); }; - if (self.get_definition_options().quick_create === undefined || self.get_definition_options().quick_create) { + if (self.options.quick_create === undefined || self.options.quick_create) { new instance.web.DataSet(this, this.field.relation, self.build_context()) - .name_create(name, function(data) { + .name_create(name).then(function(data) { self.add_id(data[0]); }).fail(function(error, event) { event.preventDefault(); @@ -2424,7 +2772,7 @@ instance.web.form.CompletionFieldMixin = { pop.select_element( self.field.relation, { - title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + (this.string || this.name), + title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string, initial_ids: ids ? _.map(ids, function(x) {return x[0]}) : undefined, initial_view: view, disable_multiple_selection: true @@ -2434,16 +2782,51 @@ instance.web.form.CompletionFieldMixin = { ); pop.on_select_elements.add(function(element_ids) { self.add_id(element_ids[0]); + self.focus(); }); }, /** * To implement. */ add_id: function(id) {}, + _create_context: function(name) { + var tmp = {}; + var field = (this.options || {}).create_name_field; + if (field === undefined) + field = "name"; + if (field !== false && name && (this.options || {}).quick_create !== false) + tmp["default_" + field] = name; + return tmp; + }, }; -instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, - instance.web.form.CompletionFieldMixin, { +instance.web.form.M2ODialog = instance.web.Dialog.extend({ + template: "M2ODialog", + init: function(parent) { + this._super(parent, { + title: _.str.sprintf(_t("Add %s"), parent.string), + width: 312, + }); + }, + start: function() { + var self = this; + this.$buttons.html(QWeb.render("M2ODialog.buttons")); + this.$("input").val(this.getParent().last_query); + this.$buttons.find(".oe_form_m2o_qc_button").click(function(){ + self.getParent()._quick_create(self.$("input").val()); + self.destroy(); + }); + this.$buttons.find(".oe_form_m2o_sc_button").click(function(){ + self.getParent()._search_create_popup("form", undefined, self.getParent()._create_context(self.$("input").val())); + self.destroy(); + }); + this.$buttons.find(".oe_form_m2o_cancel_button").click(function(){ + self.destroy(); + }); + }, +}); + +instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, { template: "FieldMany2One", init: function(field_manager, node) { this._super(field_manager, node); @@ -2453,6 +2836,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten this.last_search = []; this.floating = false; this.inhibit_on_change = false; + this.current_display = null; }, start: function() { this._super(); @@ -2461,29 +2845,43 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten this.floating = false; this.render_value(); }); + instance.web.bus.on('click', this, function() { + if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) { + this.$input.autocomplete("close"); + } + }); }, initialize_content: function() { if (!this.get("effective_readonly")) this.render_editable(); this.render_value(); }, + init_error_displayer: function() { + // nothing + }, + hide_error_displayer: function() { + // doesn't work + }, + show_error_displayer: function() { + new instance.web.form.M2ODialog(this).open(); + }, render_editable: function() { var self = this; - this.$input = this.$element.find("input"); - - self.$input.tipsy({ - title: function() { - return "No element was selected, you should create or select one from the dropdown list."; - }, - trigger:'manual', - fade: true, + this.$input = this.$el.find("input"); + + this.init_error_displayer(); + + self.$input.on('focus', function() { + self.hide_error_displayer(); }); - - this.$drop_down = this.$element.find(".oe-m2o-drop-down-button"); - this.$follow_button = $(".oe-m2o-cm-button", this.$element); - - this.$follow_button.click(function() { + + this.$drop_down = this.$el.find(".oe_m2o_drop_down_button"); + this.$follow_button = $(".oe_m2o_cm_button", this.$el); + + this.$follow_button.click(function(ev) { + ev.preventDefault(); if (!self.get('value')) { + self.focus(); return; } var pop = new instance.web.form.FormOpenPopup(self.view); @@ -2492,40 +2890,48 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten self.get("value"), self.build_context(), { - title: _t("Open: ") + (self.string || self.name) + title: _t("Open: ") + self.string } ); pop.on_write_completed.add_last(function() { self.display_value = {}; self.render_value(); + self.focus(); }); }); // some behavior for input - this.$input.keyup(function() { - if (self.$input.val() === "") { - self.set({value: false}); - } else { - self.floating = true; + var input_changed = function() { + if (self.current_display !== self.$input.val()) { + self.current_display = self.$input.val(); + if (self.$input.val() === "") { + self.set({value: false}); + self.floating = false; + } else { + self.floating = true; + } } - }); + }; + this.$input.keydown(input_changed); + this.$input.change(input_changed); this.$drop_down.click(function() { if (self.$input.autocomplete("widget").is(":visible")) { self.$input.autocomplete("close"); + self.$input.focus(); } else { if (self.get("value") && ! self.floating) { self.$input.autocomplete("search", ""); } else { self.$input.autocomplete("search"); } - self.$input.focus(); } }); - var tip_def = $.Deferred(); - var untip_def = $.Deferred(); - var tip_delay = 200; - var tip_duration = 3000; + self.ed_def = $.Deferred(); + self.uned_def = $.Deferred(); + var ed_delay = 200; + var ed_duration = 15000; var anyoneLoosesFocus = function() { + var used = false; if (self.floating) { if (self.last_search.length > 0) { if (self.last_search[0][0] != self.get("value")) { @@ -2533,33 +2939,52 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten self.display_value["" + self.last_search[0][0]] = self.last_search[0][1]; self.set({value: self.last_search[0][0]}); } else { + used = true; self.render_value(); } } else { + used = true; self.set({value: false}); + self.render_value(); } + self.floating = false; } - if (! self.get("value")) { - tip_def.reject(); - untip_def.reject(); - tip_def = $.Deferred(); - tip_def.then(function() { - self.$input.tipsy("show"); + if (used && self.get("value") === false && ! self.no_ed) { + self.ed_def.reject(); + self.uned_def.reject(); + self.ed_def = $.Deferred(); + self.ed_def.then(function() { + self.show_error_displayer(); }); setTimeout(function() { - tip_def.resolve(); - untip_def.reject(); - untip_def = $.Deferred(); - untip_def.then(function() { - self.$input.tipsy("hide"); + self.ed_def.resolve(); + self.uned_def.reject(); + self.uned_def = $.Deferred(); + self.uned_def.then(function() { + self.hide_error_displayer(); }); - setTimeout(function() {untip_def.resolve();}, tip_duration); - }, tip_delay); + setTimeout(function() {self.uned_def.resolve();}, ed_duration); + }, ed_delay); } else { - tip_def.reject(); + self.no_ed = false; + self.ed_def.reject(); } }; - this.$input.focusout(anyoneLoosesFocus); + var ignore_blur = false; + this.$input.on({ + focusout: anyoneLoosesFocus, + focus: function () { self.trigger('focused'); }, + autocompleteopen: function () { ignore_blur = true; }, + autocompleteclose: function () { ignore_blur = false; }, + blur: function () { + // autocomplete open + if (ignore_blur) { return; } + if (_(self.getChildren()).any(function (child) { + return child instanceof instance.web.form.AbstractFormPopup; + })) { return; } + self.trigger('blurred'); + } + }); var isSelecting = false; // autocomplete @@ -2577,8 +3002,9 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten self.display_value["" + item.id] = item.name; self.set({value: item.id}); } else if (item.action) { - self.floating = true; item.action(); + // Cancel widget blurring, to avoid form blur event + self.trigger('focused'); return false; } }, @@ -2586,21 +3012,22 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten e.preventDefault(); }, html: true, - close: anyoneLoosesFocus, + // disabled to solve a bug, but may cause others + //close: anyoneLoosesFocus, minLength: 0, delay: 0 }); this.$input.autocomplete("widget").addClass("openerp"); // used to correct a bug when selecting an element by pushing 'enter' in an editable list this.$input.keyup(function(e) { - if (e.which === 13) { + if (e.which === 13) { // ENTER if (isSelecting) e.stopPropagation(); } isSelecting = false; }); + this.setupFocus(this.$follow_button); }, - render_value: function(no_recurse) { var self = this; if (! this.get("value")) { @@ -2613,8 +3040,8 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten return; } if (! no_recurse) { - var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context()); - dataset.name_get([self.get("value")], function(data) { + var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context()); + dataset.name_get([self.get("value")]).then(function(data) { self.display_value["" + self.get("value")] = data[0][1]; self.render_value(true); }); @@ -2623,12 +3050,24 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten display_string: function(str) { var self = this; if (!this.get("effective_readonly")) { - this.$input.val(str); + this.$input.val(str.split("\n")[0]); + this.current_display = this.$input.val(); + if(this.is_false()){ + this.$('.oe_m2o_cm_button').css({'display':'none'}); + } } else { - this.$element.find('a') + var lines = _.escape(str).split("\n"); + var link = ""; + var follow = ""; + link = lines[0]; + follow = _.rest(lines).join("<br />"); + if (follow) + link += "<br />"; + var $link = this.$el.find('.oe_form_uri') .unbind('click') - .text(str) - .click(function () { + .html(link); + if (! this.options.no_open) + $link.click(function () { self.do_action({ type: 'ir.actions.act_window', res_model: self.field.relation, @@ -2639,13 +3078,16 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten }); return false; }); + $(".oe_form_m2o_follow", this.$el).html(follow); } }, set_value: function(value_) { var self = this; if (value_ instanceof Array) { this.display_value = {}; - this.display_value["" + value_[0]] = value_[1]; + if (! this.options.always_reload) { + this.display_value["" + value_[0]] = value_[1]; + } value_ = value_[0]; } value_ = value_ || false; @@ -2653,6 +3095,9 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten this._super(value_); this.inhibit_on_change = false; }, + get_displayed: function() { + return this.display_value["" + this.get("value")]; + }, add_id: function(id) { this.display_value = {}; this.set({value: id}); @@ -2661,9 +3106,19 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten return ! this.get("value"); }, focus: function () { - this.delay_focus(this.$input); - } -})); + this.$input.focus(); + }, + _quick_create: function() { + this.no_ed = true; + this.ed_def.reject(); + return instance.web.form.CompletionFieldMixin._quick_create.apply(this, arguments); + }, + _search_create_popup: function() { + this.no_ed = true; + this.ed_def.reject(); + return instance.web.form.CompletionFieldMixin._search_create_popup.apply(this, arguments); + }, +}); /* # Values: (0, 0, { fields }) create @@ -2724,6 +3179,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ }, start: function() { this._super.apply(this, arguments); + this.$el.addClass('oe_form_field oe_form_field_one2many'); var self = this; @@ -2731,7 +3187,6 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ this.dataset.o2m = this; this.dataset.parent_view = this.view; this.dataset.child_name = this.name; - //this.dataset.child_name = this.dataset.on_change.add_last(function() { self.trigger_on_change(); }); @@ -2758,11 +3213,18 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ }, load_views: function() { var self = this; - + var modes = this.node.attrs.mode; modes = !!modes ? modes.split(",") : ["tree"]; var views = []; _.each(modes, function(mode) { + if (! _.include(["list", "tree", "graph", "kanban"], mode)) { + try { + throw new Error(_.str.sprintf("View type '%s' is not supported in One2Many.", mode)); + } catch(e) { + instance.webclient.crashmanager.on_javascript_exception(e) + } + } var view = { view_id: false, view_type: mode == "tree" ? "list" : mode, @@ -2772,23 +3234,37 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ view.embedded_view = self.field.views[mode]; } if(view.view_type === "list") { - view.options.selectable = self.multi_selection; + _.extend(view.options, { + addable: null, + selectable: self.multi_selection, + sortable: false, + import_enabled: false, + deletable: true + }); if (self.get("effective_readonly")) { - view.options.addable = null; - view.options.deletable = null; + _.extend(view.options, { + deletable: null, + reorderable: false, + }); } } else if (view.view_type === "form") { if (self.get("effective_readonly")) { view.view_type = 'form'; } - view.options.not_interactible_on_create = true; + _.extend(view.options, { + not_interactible_on_create: true, + }); } else if (view.view_type === "kanban") { - view.options.confirm_on_delete = false; + _.extend(view.options, { + confirm_on_delete: false, + }); if (self.get("effective_readonly")) { - view.options.action_buttons = false; - view.options.quick_creatable = false; - view.options.creatable = false; - view.options.read_only_mode = true; + _.extend(view.options, { + action_buttons: false, + quick_creatable: false, + creatable: false, + read_only_mode: true, + }); } } views.push(view); @@ -2806,11 +3282,14 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ this.viewmanager.on_controller_inited.add_last(function(view_type, controller) { controller.o2m = self; if (view_type == "list") { - if (self.get("effective_readonly")) - controller.set_editable(false); + if (self.get("effective_readonly")) { + controller.on('edit:before', self, function (e) { + e.cancel = true; + }); + } } else if (view_type === "form") { if (self.get("effective_readonly")) { - $(".oe_form_buttons", controller.$element).children().remove(); + $(".oe_form_buttons", controller.$el).children().remove(); } controller.on_record_loaded.add_last(function() { once.resolve(); @@ -2831,7 +3310,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ }); this.is_setted.then(function() { $.async_when().then(function () { - self.viewmanager.appendTo(self.$element); + self.viewmanager.appendTo(self.$el); }); }); return def; @@ -2913,6 +3392,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ this.dataset.index = 0; } self.is_setted.resolve(); + this.trigger_on_change(); return self.reload_current_view(); }, get_value: function() { @@ -2939,42 +3419,44 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ save_any_view: function() { if (this.doing_on_change) return false; - return this.session.synchronized_mode(_.bind(function() { - if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view && - this.viewmanager.views[this.viewmanager.active_view] && - this.viewmanager.views[this.viewmanager.active_view].controller) { - var view = this.viewmanager.views[this.viewmanager.active_view].controller; - if (this.viewmanager.active_view === "form") { - if (!view.is_initialized.isResolved()) { - return false; - } - var res = $.when(view.do_save()); - if (!res.isResolved() && !res.isRejected()) { - console.warn("Asynchronous get_value() is not supported in form view."); - } - return res; - } else if (this.viewmanager.active_view === "list") { - var res = $.when(view.ensure_saved()); - if (!res.isResolved() && !res.isRejected()) { - console.warn("Asynchronous get_value() is not supported in list view."); - } - return res; - } - } - return false; - }, this)); + return this.session.synchronized_mode(_.bind(function() { + if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view && + this.viewmanager.views[this.viewmanager.active_view] && + this.viewmanager.views[this.viewmanager.active_view].controller) { + var view = this.viewmanager.views[this.viewmanager.active_view].controller; + if (this.viewmanager.active_view === "form") { + if (!view.is_initialized.isResolved()) { + return false; + } + var res = $.when(view.do_save()); + if (!res.isResolved() && !res.isRejected()) { + console.warn("Asynchronous get_value() is not supported in form view."); + } + return res; + } else if (this.viewmanager.active_view === "list") { + var res = $.when(view.ensure_saved()); + if (!res.isResolved() && !res.isRejected()) { + console.warn("Asynchronous get_value() is not supported in list view."); + } + return res; + } + } + return false; + }, this)); }, is_syntax_valid: function() { - if (!this.viewmanager.views[this.viewmanager.active_view]) + if (! this.viewmanager || ! this.viewmanager.views[this.viewmanager.active_view]) return true; var view = this.viewmanager.views[this.viewmanager.active_view].controller; - if (this.viewmanager.active_view === "form") { - for (var f in view.fields) { - f = view.fields[f]; - if (!f.is_valid()) { - return false; - } - } + switch (this.viewmanager.active_view) { + case 'form': + return _(view.fields).chain() + .invoke('is_valid') + .all(_.identity) + .value(); + break; + case 'list': + return view.is_valid(); } return true; }, @@ -2989,6 +3471,7 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({ form: 'instance.web.form.One2ManyFormView', kanban: 'instance.web.form.One2ManyKanbanView', }); + this.__ignore_blur = false; }, switch_view: function(mode, unused) { if (mode !== 'form') { @@ -2998,7 +3481,7 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({ var id = self.o2m.dataset.index !== null ? self.o2m.dataset.ids[self.o2m.dataset.index] : null; var pop = new instance.web.form.FormOpenPopup(self.o2m.view); pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), { - title: _t("Open: ") + self.name, + title: _t("Open: ") + self.o2m.string, create_function: function(data) { return self.o2m.dataset.create(data).then(function(r) { self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result])); @@ -3034,8 +3517,51 @@ instance.web.form.One2ManyDataSet = instance.web.BufferedDataSet.extend({ instance.web.form.One2ManyListView = instance.web.ListView.extend({ _template: 'One2Many.listview', + init: function (parent, dataset, view_id, options) { + this._super(parent, dataset, view_id, _.extend(options || {}, { + ListType: instance.web.form.One2ManyList + })); + this.on('edit:before', this, this.proxy('_before_edit')); + this.on('edit:after', this, this.proxy('_after_edit')); + this.on('save:before cancel:before', this, this.proxy('_before_unedit')); + + this.records + .bind('add', this.proxy("changed_records")) + .bind('edit', this.proxy("changed_records")) + .bind('remove', this.proxy("changed_records")); + }, + start: function () { + var ret = this._super(); + this.$el + .off('mousedown.handleButtons') + .on('mousedown.handleButtons', 'table button', this.proxy('_button_down')); + return ret; + }, + changed_records: function () { + this.o2m.trigger_on_change(); + }, + is_valid: function () { + var form = this.editor.form; + + // If the form has not been modified, the view can only be valid + // NB: is_dirty will also be set on defaults/onchanges/whatever? + // oe_form_dirty seems to only be set on actual user actions + if (!form.$el.is('.oe_form_dirty')) { + return true; + } + this.o2m._dirty_flag = true; + + // Otherwise validate internal form + return _(form.fields).chain() + .invoke(function () { + this._check_css_flags(); + return this.is_valid(); + }) + .all(_.identity) + .value(); + }, do_add_record: function () { - if (this.options.editable) { + if (this.editable()) { this._super.apply(this, arguments); } else { var self = this; @@ -3043,7 +3569,7 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ pop.select_element( self.o2m.field.relation, { - title: _t("Create: ") + self.name, + title: _t("Create: ") + self.o2m.string, initial_view: "form", alternative_form_view: self.o2m.field.views ? self.o2m.field.views["form"] : undefined, create_function: function(data, callback, error_callback) { @@ -3071,7 +3597,7 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ var self = this; var pop = new instance.web.form.FormOpenPopup(self.o2m.view); pop.show_element(self.o2m.field.relation, id, self.o2m.build_context(), { - title: _t("Open: ") + self.name, + title: _t("Open: ") + self.o2m.string, write_function: function(id, data) { return self.o2m.dataset.write(id, data, {}, function(r) { self.o2m.reload_current_view(); @@ -3084,13 +3610,147 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ return self.o2m.dataset.read_ids.apply(self.o2m.dataset, arguments); }, form_view_options: {'not_interactible_on_create':true}, - readonly: self.o2m.get("effective_readonly") + readonly: !this.is_action_enabled('edit') || self.o2m.get("effective_readonly") }); }, do_button_action: function (name, id, callback) { + if (!_.isNumber(id)) { + instance.webclient.notification.warn( + _t("Action Button"), + _t("The o2m record must be saved before an action can be used")); + return; + } + var parent_form = this.o2m.view; + var self = this; + this.ensure_saved().pipe(function () { + return parent_form.do_save(); + }).then(function () { + self.handle_button(name, id, callback); + }); + }, + + _before_edit: function () { + this.__ignore_blur = false; + this.editor.form.on('blurred', this, this._on_form_blur); + }, + _after_edit: function () { + // The form's blur thing may be jiggered during the edition setup, + // potentially leading to the o2m instasaving the row. Cancel any + // blurring triggered the edition startup here + this.editor.form.widgetFocused(); + }, + _before_unedit: function () { + this.editor.form.off('blurred', this, this._on_form_blur); + }, + _button_down: function () { + // If a button is clicked (usually some sort of action button), it's + // the button's responsibility to ensure the editable list is in the + // correct state -> ignore form blurring + this.__ignore_blur = true; + }, + /** + * Handles blurring of the nested form (saves the currently edited row), + * unless the flag to ignore the event is set to ``true`` + * + * Makes the internal form go away + */ + _on_form_blur: function () { + if (this.__ignore_blur) { + this.__ignore_blur = false; + return; + } + // FIXME: why isn't there an API for this? + if (this.editor.form.$el.hasClass('oe_form_dirty')) { + this.save_edition(); + return; + } + this.cancel_edition(); + }, + keyup_ENTER: function () { + // blurring caused by hitting the [Return] key, should skip the + // autosave-on-blur and let the handler for [Return] do its thing (save + // the current row *anyway*, then create a new one/edit the next one) + this.__ignore_blur = true; + this._super.apply(this, arguments); + }, + do_delete: function (ids) { + var self = this; + var next = $.when(); + var _super = this._super; + // handle deletion of an item which does not exist + // TODO: better handle that in the editable list? + var false_id_index = _(ids).indexOf(false); + if (false_id_index !== -1) { + ids.splice(false_id_index, 1); + next = this.cancel_edition(true); + } + return next.pipe(function () { + // wheeee + var confirm = window.confirm; + window.confirm = function () { return true; }; + try { + return _super.call(self, ids); + } finally { + window.confirm = confirm; + } + }); + } +}); +instance.web.form.One2ManyList = instance.web.ListView.List.extend({ + pad_table_to: function (count) { + if (!this.view.is_action_enabled('create')) { + this._super(count); + } else { + this._super(count > 0 ? count - 1 : 0); + } + + // magical invocation of wtf does that do + if (this.view.o2m.get('effective_readonly')) { + return; + } + var self = this; - var def = $.Deferred().then(callback).then(function() {self.o2m.view.reload();}); - return this._super(name, id, _.bind(def.resolve, def)); + var columns = _(this.columns).filter(function (column) { + return column.invisible !== '1'; + }).length; + if (this.options.selectable) { columns++; } + if (this.options.deletable) { columns++; } + + if (!this.view.is_action_enabled('create')) { + return; + } + + var $cell = $('<td>', { + colspan: columns, + 'class': 'oe_form_field_one2many_list_row_add' + }).append( + $('<a>', {href: '#'}).text(_t("Add a row")) + .mousedown(function () { + // FIXME: needs to be an official API somehow + if (self.view.editor.is_editing()) { + self.view.__ignore_blur = true; + } + }) + .click(function (e) { + e.preventDefault(); + e.stopPropagation(); + // FIXME: there should also be an API for that one + if (self.view.editor.form.__blur_timeout) { + clearTimeout(self.view.editor.form.__blur_timeout); + self.view.editor.form.__blur_timeout = false; + } + self.view.ensure_saved().then(function () { + self.view.do_add_record(); + }); + })); + + var $padding = this.$current.find('tr:not([data-id]):first'); + var $newrow = $('<tr>').append($cell); + if ($padding.length) { + $padding.before($newrow); + } else { + this.$current.append($newrow) + } } }); @@ -3119,8 +3779,7 @@ instance.web.form.One2ManyKanbanView = instance.web_kanban.KanbanView.extend({ }); } -instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.CompletionFieldMixin, - instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, instance.web.form.ReinitializeFieldMixin, { template: "FieldMany2ManyTags", init: function() { this._super.apply(this, arguments); @@ -3138,7 +3797,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. if (this.get("effective_readonly")) return; var self = this; - self.$text = $("textarea", this.$element); + self.$text = $("textarea", this.$el); self.$text.textext({ plugins : 'tags arrow autocomplete', autocomplete: { @@ -3196,10 +3855,10 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. self._drop_shown = true; }); self.tags = self.$text.textext()[0].tags(); - $("textarea", this.$element).focusout(function() { + $("textarea", this.$el).focusout(function() { self.$text.trigger("setInputData", ""); }).keydown(function(e) { - if (event.keyCode === 9 && self._drop_shown) { + if (e.which === $.ui.keyCode.TAB && self._drop_shown) { self.$text.textext()[0].autocomplete().selectFromDropdown(); } }); @@ -3221,22 +3880,23 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. render_value: function() { var self = this; var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context()); + var values = self.get("value") var handle_names = function(data) { var indexed = {}; _.each(data, function(el) { indexed[el[0]] = el; }); - data = _.map(self.get("value"), function(el) { return indexed[el]; }); + data = _.map(values, function(el) { return indexed[el]; }); if (! self.get("effective_readonly")) { self.tags.containerElement().children().remove(); - $("textarea", self.$element).css("padding-left", "3px"); + $("textarea", self.$el).css("padding-left", "3px"); self.tags.addTags(_.map(data, function(el) {return {name: el[1], id:el[0]};})); } else { - self.$element.html(QWeb.render("FieldMany2ManyTags.box", {elements: data})); + self.$el.html(QWeb.render("FieldMany2ManyTag", {elements: data})); } }; - if (! self.get('values') || self.get('values').length > 0) { - this._display_orderer.add(dataset.name_get(self.get("value"))).then(handle_names); + if (! values || values.length > 0) { + this._display_orderer.add(dataset.name_get(values)).then(handle_names); } else { handle_names([]); } @@ -3244,7 +3904,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. add_id: function(id) { this.set({'value': _.uniq(this.get('value').concat([id]))}); }, -})); +}); /* * TODO niv: clean those deferred stuff, it could be better @@ -3260,6 +3920,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ }, start: function() { this._super.apply(this, arguments); + this.$el.addClass('oe_form_field oe_form_field_many2many'); var self = this; @@ -3268,7 +3929,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ this.dataset.on_unlink.add_last(function(ids) { self.dataset_changed(); }); - + this.is_setted.then(function() { self.load_view(); }); @@ -3281,7 +3942,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ }); }); }); - }) + }); }, set_value: function(value_) { value_ = value_ || []; @@ -3297,12 +3958,19 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ get_value: function() { return [commands.replace_with(this.get('value'))]; }, + + is_false: function () { + return _(this.dataset.ids).isEmpty(); + }, load_view: function() { var self = this; this.list_view = new instance.web.form.Many2ManyListView(this, this.dataset, false, { 'addable': self.get("effective_readonly") ? null : _t("Add"), 'deletable': self.get("effective_readonly") ? false : true, 'selectable': self.multi_selection, + 'sortable': false, + 'reorderable': false, + 'import_enabled': false, }); var embedded = (this.field.views || {}).tree; if (embedded) { @@ -3315,7 +3983,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ loaded.resolve(); }); $.async_when().then(function () { - self.list_view.appendTo(self.$element); + self.list_view.appendTo(self.$el); }); return loaded; }, @@ -3347,7 +4015,7 @@ instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends in pop.select_element( this.model, { - title: _t("Add: ") + this.name + title: _t("Add: ") + this.m2m_field.string }, new instance.web.CompoundDomain(this.m2m_field.build_domain(), ["!", ["id", "in", this.m2m_field.dataset.ids]]), this.m2m_field.build_context() @@ -3367,7 +4035,7 @@ instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends in var self = this; var pop = new instance.web.form.FormOpenPopup(this); pop.show_element(this.dataset.model, id, this.m2m_field.build_context(), { - title: _t("Open: ") + this.name, + title: _t("Open: ") + this.m2m_field.string, readonly: this.getParent().get("effective_readonly") }); pop.on_write_completed.add_last(function() { @@ -3376,7 +4044,7 @@ instance.web.form.Many2ManyListView = instance.web.ListView.extend(/** @lends in } }); -instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.CompletionFieldMixin, { +instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend(instance.web.form.CompletionFieldMixin, { disable_utility_classes: true, init: function(field_manager, node) { this._super(field_manager, node); @@ -3396,7 +4064,7 @@ instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend( this.dataset.on_unlink.add_last(function(ids) { self.dataset_changed(); }); - + this.is_setted.then(function() { self.load_view(); }); @@ -3443,7 +4111,7 @@ instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend( }); this.kanban_view.do_switch_view.add_last(_.bind(this.open_popup, this)); $.async_when().then(function () { - self.kanban_view.appendTo(self.$element); + self.kanban_view.appendTo(self.$el); }); return loaded; }, @@ -3465,7 +4133,7 @@ instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend( pop.select_element( this.field.relation, { - title: _t("Add: ") + this.name + title: _t("Add: ") + this.string }, new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]), this.build_context() @@ -3483,7 +4151,7 @@ instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend( var id = self.dataset.ids[self.dataset.index]; var pop = new instance.web.form.FormOpenPopup(self.view); pop.show_element(self.field.relation, id, self.build_context(), { - title: _t("Open: ") + self.name, + title: _t("Open: ") + self.string, write_function: function(id, data, options) { return self.dataset.write(id, data, {}).then(function() { self.reload_content(); @@ -3499,7 +4167,7 @@ instance.web.form.FieldMany2ManyKanban = instance.web.form.AbstractField.extend( add_id: function(id) { this.quick_create.add_id(id); }, -})); +}); function m2m_kanban_lazy_init() { if (instance.web.form.Many2ManyKanbanView) @@ -3512,7 +4180,7 @@ instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({ }); instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({ template: 'Many2ManyKanban.quick_create', - + /** * close_btn: If true, the widget will display a "Close" button able to trigger * a "close" event. @@ -3527,7 +4195,7 @@ instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({ }, start: function () { var self = this; - self.$text = this.$element.find('input').css("width", "200px"); + self.$text = this.$el.find('input').css("width", "200px"); self.$text.textext({ plugins : 'arrow autocomplete', autocomplete: { @@ -3584,7 +4252,7 @@ instance.web.form.Many2ManyQuickCreate = instance.web.Widget.extend({ /** * Class with everything which is common between FormOpenPopup and SelectCreatePopup. */ -instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ +instance.web.form.AbstractFormPopup = instance.web.Widget.extend({ template: "AbstractFormPopup.render", /** * options: @@ -3627,14 +4295,16 @@ instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ display_popup: function() { var self = this; this.renderElement(); - new instance.web.Dialog(this, { - width: '90%', + var dialog = new instance.web.Dialog(this, { min_width: '800px', + dialogClass: 'oe_act_window', close: function() { self.check_exit(true); }, title: this.options.title || "", - }, this.$element).open(); + buttons: [{text:"tmp"}], + }, this.$el).open(); + this.$buttonpane = dialog.$el.dialog("widget").find(".ui-dialog-buttonpane").html(""); this.start(); }, on_write_completed: function() {}, @@ -3650,16 +4320,21 @@ instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ if (this.row_id !== null) { options.initial_mode = this.options.readonly ? "view" : "edit"; } + _.extend(options, { + $buttons: this.$buttonpane, + }); this.view_form = new instance.web.FormView(this, this.dataset, false, options); if (this.options.alternative_form_view) { this.view_form.set_embedded_view(this.options.alternative_form_view); } - this.view_form.appendTo(this.$element.find(".oe-form-view-popup-form-placeholder")); + this.view_form.appendTo(this.$el.find(".oe_popup_form")); this.view_form.on_loaded.add_last(function() { - var $buttons = self.view_form.$element.find(".oe_form_buttons"); var multi_select = self.row_id === null && ! self.options.disable_multiple_selection; - $buttons.html(QWeb.render("AbstractFormPopup.buttons", {multi_select: multi_select})); - var $snbutton = $buttons.find(".oe_abstractformpopup-form-save-new"); + self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", { + multi_select: multi_select, + readonly: self.row_id !== null && self.options.readonly, + })); + var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new"); $snbutton.click(function() { $.when(self.view_form.do_save()).then(function() { self.view_form.reload_mutex.exec(function() { @@ -3667,7 +4342,7 @@ instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ }); }); }); - var $sbutton = $buttons.find(".oe_abstractformpopup-form-save"); + var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save"); $sbutton.click(function() { $.when(self.view_form.do_save()).then(function() { self.view_form.reload_mutex.exec(function() { @@ -3675,15 +4350,10 @@ instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ }); }); }); - var $cbutton = $buttons.find(".oe_abstractformpopup-form-close"); + var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close"); $cbutton.click(function() { self.check_exit(); }); - if (self.row_id !== null && self.options.readonly) { - $snbutton.hide(); - $sbutton.hide(); - $cbutton.text(_t("Close")); - } self.view_form.do_show(); }); }, @@ -3697,7 +4367,7 @@ instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ this.destroy(); }, destroy: function () { - this.$element.dialog('close'); + this.$el.dialog('close'); this._super(); }, }); @@ -3782,32 +4452,32 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend self.dataset, false, _.extend({'deletable': false, 'selectable': !self.options.disable_multiple_selection, - 'read_only': true, + 'import_enabled': false, + '$buttons': self.$buttonpane, }, self.options.list_view_options || {})); + self.view_list.on('edit:before', self, function (e) { + e.cancel = true; + }); self.view_list.popup = self; - self.view_list.appendTo($(".oe-select-create-popup-view-list", self.$element)).pipe(function() { + self.view_list.appendTo($(".oe_popup_list", self.$el)).pipe(function() { self.view_list.do_show(); }).pipe(function() { self.searchview.do_search(); }); self.view_list.on_loaded.add_last(function() { - var $buttons = self.view_list.$element.find(".oe-actions"); - $buttons.prepend(QWeb.render("SelectCreatePopup.search.buttons")); - var $cbutton = $buttons.find(".oe_selectcreatepopup-search-close"); + self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self})); + var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close"); $cbutton.click(function() { self.destroy(); }); - var $sbutton = $buttons.find(".oe_selectcreatepopup-search-select"); - if(self.options.disable_multiple_selection) { - $sbutton.hide(); - } + var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select"); $sbutton.click(function() { self.on_select_elements(self.selected_ids); self.destroy(); }); }); }); - this.searchview.appendTo($(".oe-select-create-popup-view-list", self.$element)); + this.searchview.appendTo($(".oe_popup_list", self.$el)); }, do_search: function(domains, contexts, groupbys) { var self = this; @@ -3820,11 +4490,12 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend }); }, on_click_element: function(ids) { + var self = this; this.selected_ids = ids || []; if(this.selected_ids.length > 0) { - this.$element.find(".oe_selectcreatepopup-search-select").removeAttr('disabled'); + self.$buttonpane.find(".oe_selectcreatepopup-search-select").removeAttr('disabled'); } else { - this.$element.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled"); + self.$buttonpane.find(".oe_selectcreatepopup-search-select").attr('disabled', "disabled"); } }, new_object: function() { @@ -3832,7 +4503,7 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend this.searchview.hide(); } if (this.view_list) { - this.view_list.$element.hide(); + this.view_list.$el.hide(); } this.setup_form_view(); }, @@ -3852,7 +4523,7 @@ instance.web.form.SelectCreateListView = instance.web.ListView.extend({ } }); -instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldReference', init: function(field_manager, node) { this._super(field_manager, node); @@ -3865,7 +4536,7 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.exte var sel = this.selection.get_value(); this.m2o.field.relation = sel; this.m2o.set_value(false); - this.m2o.$element.toggle(sel !== false); + this.m2o.$el.toggle(sel !== false); } }, destroy_content: function() { @@ -3879,25 +4550,32 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.exte } }, initialize_content: function() { + var self = this; this.selection = new instance.web.form.FieldSelection(this, { attrs: { - name: 'selection' + name: 'selection', + modifiers: JSON.stringify({readonly: this.get('effective_readonly')}), }}); this.selection.view = this.view; - this.selection.set({force_readonly: this.get('effective_readonly')}); this.selection.on("change:value", this, this.on_selection_changed); - this.selection.$element = $(".oe_form_view_reference_selection", this.$element); + this.selection.setElement(this.$(".oe_form_view_reference_selection")); this.selection.renderElement(); this.selection.start(); + this.selection + .on('focused', null, function () {self.trigger('focused')}) + .on('blurred', null, function () {self.trigger('blurred')}); this.m2o = new instance.web.form.FieldMany2One(this, { attrs: { - name: 'm2o' + name: 'm2o', + modifiers: JSON.stringify({readonly: this.get('effective_readonly')}), }}); this.m2o.view = this.view; - this.m2o.set({force_readonly: this.get("effective_readonly")}); this.m2o.on("change:value", this, this.data_changed); - this.m2o.$element = $(".oe_form_view_reference_m2o", this.$element); + this.m2o.setElement(this.$(".oe_form_view_reference_m2o")); this.m2o.renderElement(); this.m2o.start(); + this.m2o + .on('focused', null, function () {self.trigger('focused')}) + .on('blurred', null, function () {self.trigger('blurred')}); }, is_false: function() { return typeof(this.get_value()) !== 'string'; @@ -3944,18 +4622,27 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(_.exte } throw Exception("Should not happen"); }, -})); +}); -instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(_.extend({}, instance.web.form.ReinitializeFieldMixin, { +instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { init: function(field_manager, node) { + var self = this; this._super(field_manager, node); - this.iframe = this.element_id + '_iframe'; this.binary_value = false; + this.fileupload_id = _.uniqueId('oe_fileupload'); + $(window).on(this.fileupload_id, function() { + var args = [].slice.call(arguments).slice(1); + self.on_file_uploaded.apply(self, args); + }); + }, + stop: function() { + $(window).off(this.fileupload_id); + this._super.apply(this, arguments); }, initialize_content: function() { - this.$element.find('input.oe-binary-file').change(this.on_file_change); - this.$element.find('button.oe-binary-file-save').click(this.on_save_as); - this.$element.find('.oe-binary-file-clear').click(this.on_clear); + this.$el.find('input.oe_form_binary_file').change(this.on_file_change); + this.$el.find('button.oe_form_binary_file_save').click(this.on_save_as); + this.$el.find('.oe_form_binary_file_clear').click(this.on_clear); }, human_filesize : function(size) { var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; @@ -3970,42 +4657,62 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(_.extend( // TODO: on modern browsers, we could directly read the file locally on client ready to be used on image cropper // http://www.html5rocks.com/tutorials/file/dndfiles/ // http://deepliquid.com/projects/Jcrop/demos.php?demo=handler - window[this.iframe] = this.on_file_uploaded; - if ($(e.target).val() != '') { - this.$element.find('form.oe-binary-form input[name=session_id]').val(this.session.session_id); - this.$element.find('form.oe-binary-form').submit(); - this.$element.find('.oe-binary-progress').show(); - this.$element.find('.oe-binary').hide(); + + if ($(e.target).val() !== '') { + this.$el.find('form.oe_form_binary_form input[name=session_id]').val(this.session.session_id); + this.$el.find('form.oe_form_binary_form').submit(); + this.$el.find('.oe_form_binary_progress').show(); + this.$el.find('.oe_form_binary').hide(); } }, on_file_uploaded: function(size, name, content_type, file_base64) { - delete(window[this.iframe]); if (size === false) { this.do_warn("File Upload", "There was a problem while uploading your file"); // TODO: use openerp web crashmanager console.warn("Error while uploading file : ", name); } else { + this.filename = name; this.on_file_uploaded_and_valid.apply(this, arguments); } - this.$element.find('.oe-binary-progress').hide(); - this.$element.find('.oe-binary').show(); + this.$el.find('.oe_form_binary_progress').hide(); + this.$el.find('.oe_form_binary').show(); }, on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { }, - on_save_as: function() { - $.blockUI(); - this.session.get_file({ - url: '/web/binary/saveas_ajax', - data: {data: JSON.stringify({ - model: this.view.dataset.model, - id: (this.view.datarecord.id || ''), - field: this.name, - filename_field: (this.node.attrs.filename || ''), - context: this.view.dataset.get_context() - })}, - complete: $.unblockUI, - error: instance.webclient.crashmanager.on_rpc_error - }); + on_save_as: function(ev) { + var value = this.get('value'); + if (!value) { + this.do_warn(_t("Save As..."), _t("The field is empty, there's nothing to save !")); + ev.stopPropagation(); + } else if (this._dirty_flag) { + var link = this.$('.oe_form_binary_file_save_data')[0]; + link.download = this.filename || "download.bin"; // Works on only on Google Chrome + //link.target = '_blank'; + link.href = "data:application/octet-stream;base64," + value; + } else { + instance.web.blockUI(); + this.session.get_file({ + url: '/web/binary/saveas_ajax', + data: {data: JSON.stringify({ + model: this.view.dataset.model, + id: (this.view.datarecord.id || ''), + field: this.name, + filename_field: (this.node.attrs.filename || ''), + context: this.view.dataset.get_context() + })}, + complete: instance.web.unblockUI, + error: instance.webclient.crashmanager.on_rpc_error + }); + ev.stopPropagation(); + return false; + } + }, + set_filename: function(value) { + var filename = this.node.attrs.filename; + if (this.view.fields[filename]) { + this.view.fields[filename].set_value(value); + this.view.fields[filename].on_ui_change(); + } }, on_clear: function() { if (this.get('value') !== false) { @@ -4014,7 +4721,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(_.extend( } return false; } -})); +}); instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ template: 'FieldBinaryFile', @@ -4022,7 +4729,7 @@ instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ this._super(); if (this.get("effective_readonly")) { var self = this; - this.$element.find('a').click(function() { + this.$el.find('a').click(function() { if (self.get('value')) { self.on_save_as(); } @@ -4042,12 +4749,12 @@ instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ } else { show_value = (this.get('value') != null && this.get('value') !== false) ? this.get('value') : ''; } - this.$element.find('input').eq(0).val(show_value); + this.$el.find('input').eq(0).val(show_value); } else { - this.$element.find('a').show(!!this.get('value')); + this.$el.find('a').show(!!this.get('value')); if (this.get('value')) { var show_value = _t("Download") + " " + (this.view.datarecord[this.node.attrs.filename] || ''); - this.$element.find('a').text(show_value); + this.$el.find('a').text(show_value); } } }, @@ -4055,7 +4762,7 @@ instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ this.binary_value = true; this.set({'value': file_base64}); var show_value = name + " (" + this.human_filesize(size) + ")"; - this.$element.find('input').eq(0).val(show_value); + this.$el.find('input').eq(0).val(show_value); this.set_filename(name); }, set_filename: function(value_) { @@ -4066,38 +4773,43 @@ instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ }, on_clear: function() { this._super.apply(this, arguments); - this.$element.find('input').eq(0).val(''); + this.$el.find('input').eq(0).val(''); this.set_filename(''); } }); instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ template: 'FieldBinaryImage', - initialize_content: function() { - this._super(); - if (!this.get("effective_readonly")) { - this.$element.find('.oe_form_field_image_controls').show(); - } else { - this.$element.find('.oe_form_field_image_controls').hide(); - } - }, set_value: function(value_) { this._super.apply(this, arguments); this.render_value(); }, render_value: function() { + var self = this; var url; - if (this.get('value') && this.get('value').substr(0, 10).indexOf(' ') == -1) { + if (this.get('value') && ! /^\d+(\.\d*)? \w+$/.test(this.get('value'))) { url = 'data:image/png;base64,' + this.get('value'); } else if (this.get('value')) { + var id = escape(JSON.stringify(this.view.datarecord.id || null)); + var field = this.name; + if (this.options.preview_image) + field = this.options.preview_image; url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' + - this.view.dataset.model +'&id=' + (this.view.datarecord.id || '') + '&field=' + this.name + '&t=' + (new Date().getTime()); + this.view.dataset.model +'&id=' + id + '&field=' + field + '&t=' + (new Date().getTime()); } else { url = "/web/static/src/img/placeholder.png"; } - var img = QWeb.render("FieldBinaryImage-img", { widget: this, url: url }); - this.$element.find('> img').remove(); - this.$element.prepend(img); + var $img = $(QWeb.render("FieldBinaryImage-img", { widget: this, url: url })); + this.$el.find('> img').remove(); + this.$el.prepend($img); + $img.load(function() { + if (! self.options.size) + return; + $img.css("max-width", "" + self.options.size[0] + "px"); + $img.css("max-height", "" + self.options.size[1] + "px"); + $img.css("margin-left", "" + (self.options.size[0] - $img.width()) / 2 + "px"); + $img.css("margin-top", "" + (self.options.size[1] - $img.height()) / 2 + "px"); + }); }, on_file_change: function() { this.render_value(); @@ -4115,13 +4827,23 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ }); instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ - tagName: "span", + template: "FieldStatus", + init: function(field_manager, node) { + this._super(field_manager, node); + this.options.clickable = this.options.clickable || (this.node.attrs || {}).clickable || false; + this.options.visible = this.options.visible || (this.node.attrs || {}).statusbar_visible || false; + this.selected_value = null; + }, start: function() { this._super(); - this.selected_value = null; - // preview in start only for selection fields, because of the dynamic behavior of many2one fields. - if (this.field.type in ['selection']) { - this.render_list(); + // backward compatibility + this.loaded = new $.Deferred(); + if (this.options.clickable) { + this.$el.on('click','li',this.on_click_stage); + } + // TODO move the following into css :after + if (this.$el.parent().is('header')) { + this.$el.after('<div class="oe_clear"/>'); } }, set_value: function(value_) { @@ -4137,97 +4859,73 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ } // trick to be sure all values are loaded in the form, therefore // enabling the evaluation of dynamic domains + self.selection = []; $.async_when().then(function() { - return self.render_list(); + self.get_selection(); }); + return this.loaded; }, - - /** Get the status list and render them - * to_show: [[identifier, value_to_display]] where - * - identifier = key for a selection, id for a many2one - * - display_val = label that will be displayed - * - ex: [[0, "New"]] (many2one) or [["new", "In Progress"]] (selection) - */ - render_list: function() { - var self = this; - // get selection values, filter them and render them - var selection_done = this.get_selection().pipe(self.proxy('filter_selection')).pipe(self.proxy('render_elements')); - }, - - /** Get the selection list to be displayed in the statusbar widget. + /** Get the selection and render it + * selection: [[identifier, value_to_display], ...] * For selection fields: this is directly given by this.field.selection - * For many2one fields : - * - perform a search on the relation of the many2one field (given by - * field.relation ) - * - get the field domain for the search - * - self.build_domain() gives the domain given by the view or by - * the field - * - if the optional statusbar_fold attribute is set to true, make - * an AND with build_domain to hide all 'fold=true' columns - * - make an OR with current value, to be sure it is displayed, - * with the correct order, even if it is folded + * For many2one fields: perform a search on the relation of the many2one field */ get_selection: function() { var self = this; if (this.field.type == "many2one") { - this.selection = []; - // get fold information from widget - var fold = ((this.node.attrs || {}).statusbar_fold || true); - // build final domain: if fold option required, add the - if (fold == true) { - var domain = new instance.web.CompoundDomain(['|'], ['&'], self.build_domain(), [['fold', '=', false]], [['id', '=', self.selected_value]]); - } else { - var domain = new instance.web.CompoundDomain(['|'], self.build_domain(), [['id', '=', self.selected_value]]); + var domain = []; + if(!_.isEmpty(this.field.domain) || !_.isEmpty(this.node.attrs.domain)) { + domain = new instance.web.CompoundDomain(['|'], self.build_domain(), [['id', '=', self.selected_value]]); } - // get a DataSetSearch on the current field relation (ex: crm.lead.stage_id -> crm.case.stage) - var model_ext = new instance.web.DataSetSearch(this, this.field.relation, self.build_context(), domain); - // fetch selection - var read_defer = model_ext.read_slice(['name'], {}).pipe( function (records) { - _(records).each(function (record) { - self.selection.push([record.id, record.name]); - }); + var ds = new instance.web.DataSetSearch(this, this.field.relation, self.build_context(), domain); + ds.read_slice(['name'], {}).done( function (records) { + for(var i = 0; i < records.length; i++) { + self.selection.push([records[i].id, records[i].name]); + } + self.render_elements(); + self.loaded.resolve(); }); } else { - this.selection = this.field.selection; - var read_defer = new $.Deferred().resolve(); - } - return read_defer; - }, - - /** Filters this.selection, according to values coming from the statusbar_visible - * attribute of the field. For example: statusbar_visible="draft,open" - * Currently, the key of (key, label) pairs has to be used in the - * selection of visible items. This feature is not meant to be used - * with many2one fields. - */ - filter_selection: function() { - var self = this; - var shown = _.map(((this.node.attrs || {}).statusbar_visible || "").split(","), - function(x) { return _.str.trim(x); }); - shown = _.select(shown, function(x) { return x.length > 0; }); - - if (shown.length == 0) { - this.to_show = this.selection; - } else { - this.to_show = _.select(this.selection, function(x) { - return _.indexOf(shown, x[0]) !== -1 || x[0] === self.selected_value; - }); + this.loaded.resolve(); + // For field type selection filter values according to + // statusbar_visible attribute of the field. For example: + // statusbar_visible="draft,open". + var selection = this.field.selection; + for(var i=0; i< selection.length; i++) { + var key = selection[i][0]; + if(key == this.selected_value || !this.options.visible || this.options.visible.indexOf(key) != -1) { + this.selection.push(selection[i]); + } + } + this.render_elements(); } }, - /** Renders the widget. This function also checks for statusbar_colors='{"pending": "blue"}' * attribute in the widget. This allows to set a given color to a given * state (given by the key of (key, label)). */ render_elements: function () { - var content = instance.web.qweb.render("FieldStatus.content", {widget: this, _:_}); - this.$element.html(content); - + var self = this; + var content = instance.web.qweb.render("FieldStatus.content", {widget: this}); + this.$el.html(content); var colors = JSON.parse((this.node.attrs || {}).statusbar_colors || "{}"); var color = colors[this.selected_value]; if (color) { - var elem = this.$element.find("li.oe_form_steps_active span"); - elem.css("color", color); + this.$("oe_active").css("color", color); + } + }, + on_click_stage: function (ev) { + var self = this; + var $li = $(ev.currentTarget); + var val = parseInt($li.data("id")); + if (val != self.selected_value) { + this.view.recursive_save().then(function() { + var change = {}; + change[self.name] = val; + self.view.dataset.write(self.view.datarecord.id, change).then(function() { + self.view.reload(); + }); + }); } }, }); @@ -4235,7 +4933,7 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ /** * Registry of form fields, called by :js:`instance.web.FormView`. * - * All referenced classes must implement FieldMixin. Those represent the classes whose instances + * All referenced classes must implement FieldInterface. Those represent the classes whose instances * will substitute to the <field> tags as defined in OpenERP's views. */ instance.web.form.widgets = new instance.web.Registry({ @@ -4244,6 +4942,7 @@ instance.web.form.widgets = new instance.web.Registry({ 'email' : 'instance.web.form.FieldEmail', 'url' : 'instance.web.form.FieldUrl', 'text' : 'instance.web.form.FieldText', + 'html' : 'instance.web.form.FieldTextHtml', 'date' : 'instance.web.form.FieldDate', 'datetime' : 'instance.web.form.FieldDatetime', 'selection' : 'instance.web.form.FieldSelection', @@ -4274,6 +4973,9 @@ instance.web.form.tags = new instance.web.Registry({ 'button' : 'instance.web.form.WidgetButton', }); +instance.web.form.custom_widgets = new instance.web.Registry({ +}); + }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index de73ec21884..fd34ffddb7b 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -21,9 +21,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi // whether the view rows can be reordered (via vertical drag & drop) 'reorderable': true, 'action_buttons': true, - // if true, the view can't be editable, ignoring the view's and the context's - // instructions - 'read_only': false, }, /** * Core class for list-type displays. @@ -64,7 +61,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi this.records = new Collection(); - this.set_groups(new instance.web.ListView.Groups(this)); + this.set_groups(new (this.options.GroupsType)(this)); if (this.dataset instanceof instance.web.DataSetStatic) { this.groups.datagroup = new instance.web.StaticDataGroup(this.dataset); @@ -87,6 +84,14 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi this.no_leaf = false; }, + set_default_options: function (options) { + this._super(options); + _.defaults(this.options, { + GroupsType: instance.web.ListView.Groups, + ListType: instance.web.ListView.List + }); + }, + /** * Retrieves the view's number of records per page (|| section) * @@ -132,13 +137,13 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }); }, /** - * View startup method, the default behavior is to set the ``oe-listview`` + * View startup method, the default behavior is to set the ``oe_list`` * class on its root element and to perform an RPC load call. * * @returns {$.Deferred} loading promise */ start: function() { - this.$element.addClass('oe-listview'); + this.$el.addClass('oe_list'); return this.reload_view(null, null, true); }, /** @@ -241,19 +246,26 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi this.setup_columns(this.fields_view.fields, grouped); - this.$element.html(QWeb.render(this._template, this)); + this.$el.html(QWeb.render(this._template, this)); + this.$el.addClass(this.fields_view.arch.attrs['class']); + + // add css classes that reflect the (absence of) access rights + this.$el.toggleClass('oe_list_cannot_create', !this.is_action_enabled('create')) + .toggleClass('oe_list_cannot_edit', !this.is_action_enabled('edit')) + .toggleClass('oe_list_cannot_delete', !this.is_action_enabled('delete')); + // Head hook // Selecting records - this.$element.find('.all-record-selector').click(function(){ - self.$element.find('.oe-record-selector input').prop('checked', - self.$element.find('.all-record-selector').prop('checked') || false); + this.$el.find('.oe_list_record_selector').click(function(){ + self.$el.find('.oe_list_record_selector input').prop('checked', + self.$el.find('.oe_list_record_selector').prop('checked') || false); var selection = self.groups.get_selection(); $(self.groups).trigger( 'selected', [selection.ids, selection.records]); }); // Sorting columns - this.$element.find('thead').delegate('th.oe-sortable[data-id]', 'click', function (e) { + this.$el.find('thead').delegate('th.oe_sortable[data-id]', 'click', function (e) { e.stopPropagation(); var $this = $(this); self.dataset.sort($this.data('id')); @@ -262,26 +274,22 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi } else { $this.toggleClass("sortdown"); } - $this.siblings('.oe-sortable').removeClass("sortup sortdown"); + $this.siblings('.oe_sortable').removeClass("sortup sortdown"); self.reload_content(); }); - // Add button and Import link + // Add button if (!this.$buttons) { this.$buttons = $(QWeb.render("ListView.buttons", {'widget':self})); if (this.options.$buttons) { this.$buttons.appendTo(this.options.$buttons); } else { - this.$element.find('.oe_list_buttons').replaceWith(this.$buttons); + this.$el.find('.oe_list_buttons').replaceWith(this.$buttons); } this.$buttons.find('.oe_list_add') .click(this.proxy('do_add_record')) - .prop('disabled', grouped && this.options.editable); - this.$buttons.on('click', '.oe_list_button_import', function() { - self.on_sidebar_import(); - return false; - }); + .prop('disabled', grouped); } // Pager @@ -290,7 +298,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi if (this.options.$buttons) { this.$pager.appendTo(this.options.$pager); } else { - this.$element.find('.oe_list_pager').replaceWith(this.$pager); + this.$el.find('.oe_list_pager').replaceWith(this.$pager); } this.$pager @@ -314,7 +322,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi self.page = 0; } self.reload_content(); - }).find('.oe-pager-state') + }).find('.oe_list_pager_state') .click(function (e) { e.stopPropagation(); var $this = $(this); @@ -332,6 +340,8 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi self._limit = (isNaN(val) ? null : val); self.page = 0; self.reload_content(); + }).blur(function() { + $(this).trigger('change'); }) .val(self._limit || 'NaN'); }); @@ -341,12 +351,12 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi if (!this.sidebar && this.options.$sidebar) { this.sidebar = new instance.web.Sidebar(this); this.sidebar.appendTo(this.options.$sidebar); - this.sidebar.add_items('other', [ - { label: _t("Import"), callback: this.on_sidebar_import }, + this.sidebar.add_items('other', _.compact([ { label: _t("Export"), callback: this.on_sidebar_export }, - { label: _t('Delete'), callback: this.do_delete_selected }, - ]); + self.is_action_enabled('delete') && { label: _t('Delete'), callback: this.do_delete_selected } + ])); this.sidebar.add_toolbar(this.fields_view.toolbar); + this.sidebar.$el.hide(); } }, /** @@ -364,17 +374,20 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi } var total = dataset.size(); + var limit = this.limit() || total; + this.$pager.toggle(total !== 0); + this.$pager.toggleClass('oe_list_pager_single_page', (total <= limit)); var spager = '-'; if (total) { - var range_start = this.page * this.limit() + 1; - var range_stop = range_start - 1 + this.limit(); + var range_start = this.page * limit + 1; + var range_stop = range_start - 1 + limit; if (range_stop > total) { range_stop = total; } spager = _.str.sprintf('%d-%d of %d', range_start, range_stop, total); } - this.$pager.find('.oe-pager-state').text(spager); + this.$pager.find('.oe_list_pager_state').text(spager); }, /** * Sets up the listview's columns: merges view and fields data, move @@ -385,68 +398,26 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi * @param {Boolean} [grouped] Should the grouping columns (group and count) be displayed */ setup_columns: function (fields, grouped) { - var domain_computer = instance.web.form.compute_domain; - - var noop = function () { return {}; }; - var field_to_column = function (field) { - var name = field.attrs.name; - var column = _.extend({id: name, tag: field.tag}, - fields[name], field.attrs); - // modifiers computer - if (column.modifiers) { - var modifiers = JSON.parse(column.modifiers); - column.modifiers_for = function (fields) { - if (!modifiers.invisible) { - return {}; - } - return { - 'invisible': domain_computer(modifiers.invisible, fields) - }; - }; - if (modifiers['tree_invisible']) { - column.invisible = '1'; - } else { - delete column.invisible; - } - } else { - column.modifiers_for = noop; - } - return column; - }; - + var registry = instance.web.list.columns; + var reorder = this.options.reorderable; this.columns.splice(0, this.columns.length); - this.columns.push.apply( - this.columns, - _(this.fields_view.arch.children).map(field_to_column)); + this.columns.push.apply(this.columns, + _(this.fields_view.arch.children).map(function (field) { + var id = field.attrs.name; + if(field.attrs.widget == 'handle' && !reorder){ + field.attrs.reorderable = reorder || true; + } + return registry.for_(id, fields[id], field); + })); if (grouped) { - this.columns.unshift({ - id: '_group', tag: '', string: _t("Group"), meta: true, - modifiers_for: function () { return {}; } - }, { - id: '_count', tag: '', string: '#', meta: true, - modifiers_for: function () { return {}; } - }); + this.columns.unshift( + new instance.web.list.MetaColumn('_group', _t("Group"))); } this.visible_columns = _.filter(this.columns, function (column) { - return column.invisible !== '1'; + return column.invisible !== '1' && !column.reorderable; }); - - this.aggregate_columns = _(this.visible_columns) - .map(function (column) { - if (column.type !== 'integer' && column.type !== 'float') { - return {}; - } - var aggregation_func = column['group_operator'] || 'sum'; - if (!(aggregation_func in column)) { - return {}; - } - - return _.extend({}, column, { - 'function': aggregation_func, - label: column[aggregation_func] - }); - }); + this.aggregate_columns = _(this.visible_columns).invoke('to_aggregate'); }, /** * Used to handle a click on a table row, if no other handler caught the @@ -472,9 +443,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }, do_show: function () { this._super(); - if (this.sidebar) { - this.sidebar.$element.show(); - } if (this.$buttons) { this.$buttons.show(); } @@ -484,7 +452,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }, do_hide: function () { if (this.sidebar) { - this.sidebar.$element.hide(); + this.sidebar.$el.hide(); } if (this.$buttons) { this.$buttons.hide(); @@ -508,7 +476,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi if (this.embedded_view) { return $.Deferred().then(callback).resolve(this.embedded_view); } else { - return this.rpc('/web/listview/load', { + return this.rpc('/web/view/load', { model: this.model, view_id: this.view_id, view_type: "tree", @@ -524,10 +492,10 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi */ reload_content: function () { var self = this; - self.$element.find('.all-record-selector').prop('checked', false); + self.$el.find('.oe_list_record_selector').prop('checked', false); this.records.reset(); var reloaded = $.Deferred(); - this.$element.find('.oe-listview-content').append( + this.$el.find('.oe_list_content').append( this.groups.render(function () { if (self.dataset.index == null) { var has_one = false; @@ -548,6 +516,20 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi reload: function () { return this.reload_content(); }, + reload_record: function (record) { + return this.dataset.read_ids( + [record.get('id')], + _.pluck(_(this.columns).filter(function (r) { + return r.tag === 'field'; + }), 'name') + ).then(function (records) { + _(records[0]).each(function (value, key) { + record.set(key, value, {silent: true}); + }); + record.trigger('change', record); + }); + }, + do_load_state: function(state, warm) { var reload = false; if (state.page && this.page !== state.page) { @@ -615,7 +597,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi if (!ids.length) { this.dataset.index = 0; if (this.sidebar) { - this.sidebar.$element.hide(); + this.sidebar.$el.hide(); } this.compute_aggregates(); return; @@ -623,7 +605,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi this.dataset.index = _(this.dataset.ids).indexOf(ids[0]); if (this.sidebar) { - this.sidebar.$element.show(); + this.sidebar.$el.show(); } this.compute_aggregates(_(records).map(function (record) { @@ -638,6 +620,23 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi * @param {Function} callback should be called after the action is executed, if non-null */ do_button_action: function (name, id, callback) { + this.handle_button(name, id, callback); + }, + /** + * Base handling of buttons, can be called when overriding do_button_action + * in order to bypass parent overrides. + * + * The callback will be provided with the ``id`` as its parameter, in case + * handle_button's caller had to alter the ``id`` (or even create one) + * while not being ``callback``'s creator. + * + * This method should not be overridden. + * + * @param {String} name action name + * @param {Object} id id of the record the action should be called on + * @param {Function} callback should be called after the action is executed, if non-null + */ + handle_button: function (name, id, callback) { var action = _.detect(this.columns, function (field) { return field.name === name; }); @@ -656,7 +655,8 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi c.add(action.context); } action.context = c; - this.do_execute_action(action, this.dataset, id, callback); + this.do_execute_action( + action, this.dataset, id, _.bind(callback, null, id)); }, /** * Handles the activation of a record (clicking on it) @@ -765,16 +765,14 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }, display_aggregates: function (aggregation) { var self = this; - var $footer_cells = this.$element.find('.oe-list-footer'); + var $footer_cells = this.$el.find('.oe_list_footer'); _(this.aggregate_columns).each(function (column) { if (!column['function']) { return; } $footer_cells.filter(_.str.sprintf('[data-field=%s]', column.id)) - .html(instance.web.format_cell(aggregation, column, { - process_modifiers: false - })); + .html(column.format(aggregation, { process_modifiers: false })); }); }, get_selected_ids: function() { @@ -793,7 +791,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi pad_columns: function (count, options) { options = options || {}; // padding for action/pager header - var $first_header = this.$element.find('thead tr:first th'); + var $first_header = this.$el.find('thead tr:first th'); var colspan = $first_header.attr('colspan'); if (colspan) { if (!this.previous_colspan) { @@ -802,10 +800,10 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi $first_header.attr('colspan', parseInt(colspan, 10) + count); } // Padding for column titles, footer and data rows - var $rows = this.$element - .find('.oe-listview-header-columns, tr:not(thead tr)') + var $rows = this.$el + .find('.oe_list_header_columns, tr:not(thead tr)') .not(options['except']); - var newcols = new Array(count+1).join('<td class="oe-listview-padding"></td>'); + var newcols = new Array(count+1).join('<td class="oe_list_padding"></td>'); if (options.position === 'before') { $rows.prepend(newcols); } else { @@ -816,27 +814,29 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi * Removes all padding columns of the table */ unpad_columns: function () { - this.$element.find('.oe-listview-padding').remove(); + this.$el.find('.oe_list_padding').remove(); if (this.previous_colspan) { - this.$element + this.$el .find('thead tr:first th') .attr('colspan', this.previous_colspan); this.previous_colspan = null; } }, no_result: function () { - this.$element.find('.oe_view_nocontent').remove(); + this.$el.find('.oe_view_nocontent').remove(); if (this.groups.group_by || !this.options.action || !this.options.action.help) { return; } - this.$element.find('table:first').hide(); - this.$element.prepend( - $('<div class="oe_view_nocontent">') - .append($('<img>', { src: '/web/static/src/img/view_empty_arrow.png' })) - .append($('<div>').html(this.options.action.help)) + this.$el.find('table:first').hide(); + this.$el.prepend( + $('<div class="oe_view_nocontent">').html(this.options.action.help) ); + var create_nocontent = this.$buttons; + this.$el.find('.oe_view_nocontent').click(function() { + create_nocontent.effect('bounce', {distance: 18, times: 5}, 150); + }); } }); instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web.ListView.List# */{ @@ -882,52 +882,63 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. this.record_callbacks = { 'remove': function (event, record) { - var $row = self.$current.find( - '[data-id=' + record.get('id') + ']'); + var $row = self.$current.children( + '[data-id=' + record.get('id') + ']'); var index = $row.data('index'); $row.remove(); - self.refresh_zebra(index); }, 'reset': function () { return self.on_records_reset(); }, - 'change': function (event, record) { - var $row = self.$current.find('[data-id=' + record.get('id') + ']'); + 'change': function (event, record, attribute, value, old_value) { + var $row; + if (attribute === 'id') { + if (old_value) { + throw new Error("Setting 'id' attribute on existing record " + + JSON.stringify(record.attributes)); + } + if (!_.contains(self.dataset.ids, value)) { + // add record to dataset if not already in (added by + // the form view?) + self.dataset.ids.splice( + self.records.indexOf(record), 0, value); + } + // Set id on new record + $row = self.$current.children('[data-id=false]'); + } else { + $row = self.$current.children( + '[data-id=' + record.get('id') + ']'); + } $row.replaceWith(self.render_record(record)); }, 'add': function (ev, records, record, index) { - var $new_row = $('<tr>').attr({ - 'data-id': record.get('id') - }); + var $new_row = $(self.render_record(record)); if (index === 0) { $new_row.prependTo(self.$current); } else { var previous_record = records.at(index-1), - $previous_sibling = self.$current.find( + $previous_sibling = self.$current.children( '[data-id=' + previous_record.get('id') + ']'); $new_row.insertAfter($previous_sibling); } - - self.refresh_zebra(index, 1); } }; _(this.record_callbacks).each(function (callback, event) { this.records.bind(event, callback); }, this); - this.$_element = $('<tbody>') - .appendTo(document.body) - .delegate('th.oe-record-selector', 'click', function (e) { + this.$current = $('<tbody>') + .delegate('th.oe_list_record_selector', 'click', function (e) { e.stopPropagation(); var selection = self.get_selection(); $(self).trigger( 'selected', [selection.ids, selection.records]); }) - .delegate('td.oe-record-delete button', 'click', function (e) { + .delegate('td.oe_list_record_delete button', 'click', function (e) { e.stopPropagation(); var $row = $(e.target).closest('tr'); $(self).trigger('deleted', [[self.row_id($row)]]); }) - .delegate('td.oe-field-cell button', 'click', function (e) { + .delegate('td.oe_list_field_cell button', 'click', function (e) { e.stopPropagation(); var $target = $(e.currentTarget), field = $target.closest('td').data('field'), @@ -938,19 +949,19 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. // of digits, nice when storing actual numbers, not nice when // storing strings composed only of digits. Force the action // name to be a string - $(self).trigger('action', [field.toString(), record_id, function () { - return self.reload_record(self.records.get(record_id)); + $(self).trigger('action', [field.toString(), record_id, function (id) { + return self.reload_record(self.records.get(id)); }]); }) .delegate('a', 'click', function (e) { e.stopPropagation(); }) .delegate('tr', 'click', function (e) { - e.stopPropagation(); var row_id = self.row_id(e.currentTarget); - if (row_id !== undefined) { + if (row_id) { + e.stopPropagation(); if (!self.dataset.select_id(row_id)) { - throw "Could not find id in dataset" + throw new Error("Could not find id in dataset"); } self.row_clicked(e); } @@ -978,7 +989,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. // to get a correctly displayable value in the field var model = ref_match[1], id = parseInt(ref_match[2], 10); - new instance.web.DataSet(this.view, model).name_get([id], function(names) { + new instance.web.DataSet(this.view, model).name_get([id]).then(function(names) { if (!names.length) { return; } record.set(column.id, names[0][1]); }); @@ -994,29 +1005,46 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. // and let the various registered events handle refreshing the // row new instance.web.DataSet(this.view, column.relation) - .name_get([value], function (names) { + .name_get([value]).then(function (names) { if (!names.length) { return; } record.set(column.id, names[0]); }); } + } else if (column.type === 'many2many') { + value = record.get(column.id); + // non-resolved (string) m2m values are arrays + if (value instanceof Array && !_.isEmpty(value)) { + var ids; + // they come in two shapes: + if (value[0] instanceof Array) { + var command = value[0]; + // 1. an array of m2m commands (usually (6, false, ids)) + if (command[0] !== 6) { + throw new Error(_t("Unknown m2m command ") + command[0]); + } + ids = command[2]; + } else { + // 2. an array of ids + ids = value; + } + new instance.web.Model(column.relation) + .call('name_get', [ids]).then(function (names) { + record.set(column.id, _(names).pluck(1).join(', ')); + }) + } } - return instance.web.format_cell(record.toForm().data, column, { + return column.format(record.toForm().data, { model: this.dataset.model, id: record.get('id') }); }, render: function () { - var self = this; - if (this.$current) { - this.$current.remove(); - } - this.$current = this.$_element.clone(true); this.$current.empty().append( QWeb.render('ListView.rows', _.extend({ render_cell: function () { return self.render_cell.apply(self, arguments); } }, this))); - this.pad_table_to(5); + this.pad_table_to(4); }, pad_table_to: function (count) { if (this.records.length >= count || @@ -1025,20 +1053,20 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. } var cells = []; if (this.options.selectable) { - cells.push('<th class="oe-record-selector"></td>'); + cells.push('<th class="oe_list_record_selector"></td>'); } _(this.columns).each(function(column) { if (column.invisible === '1') { return; } if (column.tag === 'button') { - cells.push('<td class="oe-button" title="' + column.string + '"> </td>'); + cells.push('<td class="oe_button" title="' + column.string + '"> </td>'); } else { cells.push('<td title="' + column.string + '"> </td>'); } }); if (this.options.deletable) { - cells.push('<td class="oe-record-delete"><button type="button" style="visibility: hidden"> </button></td>'); + cells.push('<td class="oe_list_record_delete"><button type="button" style="visibility: hidden"> </button></td>'); } cells.unshift('<tr>'); cells.push('</tr>'); @@ -1047,19 +1075,18 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. this.$current .children('tr:not([data-id])').remove().end() .append(new Array(count - this.records.length + 1).join(row)); - this.refresh_zebra(this.records.length); }, /** * Gets the ids of all currently selected records, if any * @returns {Object} object with the keys ``ids`` and ``records``, holding respectively the ids of all selected records and the records themselves. */ get_selection: function () { + var result = {ids: [], records: []}; if (!this.options.selectable) { - return []; + return result; } var records = this.records; - var result = {ids: [], records: []}; - this.$current.find('th.oe-record-selector input:checked') + this.$current.find('th.oe_list_record_selector input:checked') .closest('tr').each(function () { var record = records.get($(this).data('id')); result.ids.push(record.get('id')); @@ -1087,7 +1114,6 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. if (!this.$current) { return; } this.$current.remove(); this.$current = null; - this.$_element.remove(); }, get_records: function () { return this.records.map(function (record) { @@ -1101,17 +1127,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. * @returns {$.Deferred} promise to the finalization of the reloading */ reload_record: function (record) { - return this.dataset.read_ids( - [record.get('id')], - _.pluck(_(this.columns).filter(function (r) { - return r.tag === 'field'; - }), 'name') - ).then(function (records) { - _(records[0]).each(function (value, key) { - record.set(key, value, {silent: true}); - }); - record.trigger('change', record); - }); + return this.view.reload_record(record); }, /** * Renders a list record to HTML @@ -1120,6 +1136,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. * @returns {String} QWeb rendering of the selected record */ render_record: function (record) { + var self = this; var index = this.records.indexOf(record); return QWeb.render('ListView.row', { columns: this.columns, @@ -1128,26 +1145,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. row_parity: (index % 2 === 0) ? 'even' : 'odd', view: this.view, render_cell: function () { - return this.render_cell.apply(this, arguments); } - }); - }, - /** - * Fixes fixes the even/odd classes - * - * @param {Number} [from_index] index from which to resequence - * @param {Number} [offset = 0] selection offset for DOM, in case there are rows to ignore in the table - */ - refresh_zebra: function (from_index, offset) { - offset = offset || 0; - from_index = from_index || 0; - var dom_offset = offset + from_index; - var sel = dom_offset ? ':gt(' + (dom_offset - 1) + ')' : null; - this.$current.children(sel).each(function (i, e) { - var index = from_index + i; - // reset record-index accelerators on rows and even/odd - var even = index%2 === 0; - $(e).toggleClass('even', even) - .toggleClass('odd', !even); + return self.render_cell.apply(self, arguments); } }); } }); @@ -1236,7 +1234,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we }); this.$row.children().last() .append($prev) - .append('<span class="oe-pager-state"></span>') + .append('<span class="oe_list_pager_state"></span>') .append($next); }, open: function (point_insertion) { @@ -1274,7 +1272,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we self.records.proxy(group.value).reset(); delete self.children[group.value]; } - var child = self.children[group.value] = new instance.web.ListView.Groups(self.view, { + var child = self.children[group.value] = new (self.view.options.GroupsType)(self.view, { records: self.records.proxy(group.value), options: self.options, columns: self.columns @@ -1282,7 +1280,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we self.bind_child_events(child); child.datagroup = group; - var $row = child.$row = $('<tr>'); + var $row = child.$row = $('<tr class="oe_group_header">'); if (group.openable && group.length) { $row.click(function (e) { if (!$row.data('open')) { @@ -1302,22 +1300,30 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we } placeholder.appendChild($row[0]); - var $group_column = $('<th class="oe-group-name">').appendTo($row); + var $group_column = $('<th class="oe_list_group_name">').appendTo($row); // Don't fill this if group_by_no_leaf but no group_by if (group.grouped_on) { var row_data = {}; row_data[group.grouped_on] = group; var group_column = _(self.columns).detect(function (column) { return column.id === group.grouped_on; }); + if (! group_column) { + throw new Error(_.str.sprintf( + _t("Grouping on field '%s' is not possible because that field does not appear in the list view."), + group.grouped_on)); + } + var group_label; try { - $group_column.html(instance.web.format_cell( - row_data, group_column, { - value_if_empty: _t("Undefined"), - process_modifiers: false - })); + group_label = group_column.format(row_data, { + value_if_empty: _t("Undefined"), + process_modifiers: false + }); } catch (e) { - $group_column.html(row_data[group_column.id].value); + group_label = row_data[group_column.id].value; } + $group_column.text(_.str.sprintf("%s (%d)", + group_label, group.length)); + if (group.length && group.openable) { // Make openable if not terminal group & group_by_no_leaf $group_column.prepend('<span class="ui-icon ui-icon-triangle-1-e" style="float: left;">'); @@ -1329,14 +1335,12 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we } } self.indent($group_column, group.level); - // count column - $('<td>').text(group.length).appendTo($row); if (self.options.selectable) { $row.append('<td>'); } _(self.columns).chain() - .filter(function (column) {return !column.invisible;}) + .filter(function (column) { return column.invisible !== '1'; }) .each(function (column) { if (column.meta) { // do not do anything @@ -1344,15 +1348,14 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we var r = {}; r[column.id] = {value: group.aggregates[column.id]}; $('<td class="oe_number">') - .html(instance.web.format_cell( - r, column, {process_modifiers: false})) + .html(column.format(r, {process_modifiers: false})) .appendTo($row); } else { $row.append('<td>'); } }); if (self.options.deletable) { - $row.append('<td class="oe-group-pagination">'); + $row.append('<td class="oe_list_group_pagination">'); } }); return placeholder; @@ -1377,7 +1380,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we }, render_dataset: function (dataset) { var self = this, - list = new instance.web.ListView.List(this, { + list = new (this.view.options.ListType)(this, { options: this.options, columns: this.columns, dataset: dataset, @@ -1403,11 +1406,11 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we } else { if (dataset.size() == records.length) { // only one page - self.$row.find('td.oe-group-pagination').empty(); + self.$row.find('td.oe_list_group_pagination').empty(); } else { var pages = Math.ceil(dataset.size() / limit); self.$row - .find('.oe-pager-state') + .find('.oe_list_pager_state') .text(_.str.sprintf(_t("%(page)d/%(page_count)d"), { page: page + 1, page_count: pages @@ -1432,18 +1435,31 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we }, setup_resequence_rows: function (list, dataset) { // drag and drop enabled if list is not sorted and there is a - // "sequence" column in the view. + // visible column with @widget=handle or "sequence" column in the view. if ((dataset.sort && dataset.sort()) || !_(this.columns).any(function (column) { - return column.name === 'sequence'; })) { + return column.widget === 'handle' + || column.name === 'sequence'; })) { return; } + var sequence_field = _(this.columns).find(function (c) { + return c.widget === 'handle'; + }); + var seqname = sequence_field ? sequence_field.name : 'sequence'; + // ondrop, move relevant record & fix sequences list.$current.sortable({ axis: 'y', items: '> tr[data-id]', - containment: 'parent', - helper: 'clone', + helper: 'clone' + }); + if (sequence_field) { + list.$current.sortable('option', 'handle', '.oe_list_field_handle'); + } + list.$current.sortable('option', { + start: function (e, ui) { + ui.placeholder.height(ui.item.height()); + }, stop: function (event, ui) { var to_move = list.records.get(ui.item.data('id')), target_id = ui.item.prev().data('id'), @@ -1461,7 +1477,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we var record, index = to, // if drag to 1st row (to = 0), start sequencing from 0 // (exclusive lower bound) - seq = to ? list.records.at(to - 1).get('sequence') : 0; + seq = to ? list.records.at(to - 1).get(seqname) : 0; while (++seq, record = list.records.at(index++)) { // write are independent from one another, so we can just // launch them all at the same time and we don't really @@ -1471,39 +1487,39 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we // when synchronous (without setTimeout) (function (dataset, id, seq) { $.async_when().then(function () { - dataset.write(id, {sequence: seq}); + var attrs = {}; + attrs[seqname] = seq; + dataset.write(id, attrs); }); }(dataset, record.get('id'), seq)); - record.set('sequence', seq); + record.set(seqname, seq); } - - list.refresh_zebra(); } }); }, render: function (post_render) { var self = this; - var $element = $('<tbody>'); - this.elements = [$element[0]]; + var $el = $('<tbody>'); + this.elements = [$el[0]]; this.datagroup.list( _(this.view.visible_columns).chain() .filter(function (column) { return column.tag === 'field' }) .pluck('name').value(), function (groups) { - $element[0].appendChild( + $el[0].appendChild( self.render_groups(groups)); if (post_render) { post_render(); } }, function (dataset) { self.render_dataset(dataset).then(function (list) { self.children[null] = list; self.elements = - [list.$current.replaceAll($element)[0]]; + [list.$current.replaceAll($el)[0]]; self.setup_resequence_rows(list, dataset); if (post_render) { post_render(); } }); }); - return $element; + return $el; }, /** * Returns the ids of all selected records for this group, and the records @@ -1807,7 +1823,7 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{ * @returns this */ remove: function (record) { - var index = _(this.records).indexOf(record); + var index = this.indexOf(record); if (index === -1) { _(this._proxies).each(function (proxy) { proxy.remove(record); @@ -1823,13 +1839,42 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{ return this; }, - _onRecordEvent: function (event, record, options) { + _onRecordEvent: function (event) { + switch(event) { // don't propagate reset events - if (event === 'reset') { return; } + case 'reset': return; + case 'change:id': + var record = arguments[1]; + var new_value = arguments[2]; + var old_value = arguments[3]; + // [change:id, record, new_value, old_value] + if (this._byId[old_value] === record) { + delete this._byId[old_value]; + this._byId[new_value] = record; + } + break; + } this.trigger.apply(this, arguments); }, // underscore-type methods + find: function (callback) { + var record; + for(var section in this._proxies) { + if (!this._proxies.hasOwnProperty(section)) { + continue + } + if ((record = this._proxies[section].find(callback))) { + return record; + } + } + for(var i=0; i<this.length; ++i) { + record = this.records[i]; + if (callback(record)) { + return record; + } + } + }, each: function (callback) { for(var section in this._proxies) { if (this._proxies.hasOwnProperty(section)) { @@ -1854,6 +1899,46 @@ var Collection = instance.web.Class.extend(/** @lends Collection# */{ }, indexOf: function (record) { return _(this.records).indexOf(record); + }, + succ: function (record, options) { + options = options || {wraparound: false}; + var result; + for(var section in this._proxies) { + if (!this._proxies.hasOwnProperty(section)) { + continue; + } + if ((result = this._proxies[section].succ(record, options))) { + return result; + } + } + var index = this.indexOf(record); + if (index === -1) { return null; } + var next_index = index + 1; + if (options.wraparound && (next_index === this.length)) { + return this.at(0); + } + return this.at(next_index); + }, + pred: function (record, options) { + options = options || {wraparound: false}; + + var result; + for (var section in this._proxies) { + if (!this._proxies.hasOwnProperty(section)) { + continue; + } + if ((result = this._proxies[section].pred(record, options))) { + return result; + } + } + + var index = this.indexOf(record); + if (index === -1) { return null; } + var next_index = index - 1; + if (options.wraparound && (next_index === -1)) { + return this.at(this.length - 1); + } + return this.at(next_index); } }); Collection.include(Events); @@ -1861,6 +1946,213 @@ instance.web.list = { Events: Events, Record: Record, Collection: Collection -} +}; +/** + * Registry for column objects used to format table cells (and some other tasks + * e.g. aggregation computations). + * + * Maps a field or button to a Column type via its ``$tag.$widget``, + * ``$tag.$type`` or its ``$tag`` (alone). + * + * This specific registry has a dedicated utility method ``for_`` taking a + * field (from fields_get/fields_view_get.field) and a node (from a view) and + * returning the right object *already instantiated from the data provided*. + * + * @type {instance.web.Registry} + */ +instance.web.list.columns = new instance.web.Registry({ + 'field': 'instance.web.list.Column', + 'field.boolean': 'instance.web.list.Boolean', + 'field.binary': 'instance.web.list.Binary', + 'field.progressbar': 'instance.web.list.ProgressBar', + 'field.handle': 'instance.web.list.Handle', + 'button': 'instance.web.list.Button', +}); +instance.web.list.columns.for_ = function (id, field, node) { + var description = _.extend({tag: node.tag}, field, node.attrs); + var tag = description.tag; + var Type = this.get_any([ + tag + '.' + description.widget, + tag + '.'+ description.type, + tag + ]); + return new Type(id, node.tag, description) +}; + +instance.web.list.Column = instance.web.Class.extend({ + init: function (id, tag, attrs) { + _.extend(attrs, { + id: id, + tag: tag + }); + + this.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {}; + delete attrs.modifiers; + _.extend(this, attrs); + + if (this.modifiers['tree_invisible']) { + this.invisible = '1'; + } else { delete this.invisible; } + }, + modifiers_for: function (fields) { + var out = {}; + var domain_computer = instance.web.form.compute_domain; + + for (var attr in this.modifiers) { + if (!this.modifiers.hasOwnProperty(attr)) { continue; } + var modifier = this.modifiers[attr]; + out[attr] = _.isBoolean(modifier) + ? modifier + : domain_computer(modifier, fields); + } + + return out; + }, + to_aggregate: function () { + if (this.type !== 'integer' && this.type !== 'float') { + return {}; + } + var aggregation_func = this['group_operator'] || 'sum'; + if (!(aggregation_func in this)) { + return {}; + } + var C = function (fn, label) { + this['function'] = fn; + this.label = label; + }; + C.prototype = this; + return new C(aggregation_func, this[aggregation_func]); + }, + /** + * + * @param row_data record whose values should be displayed in the cell + * @param {Object} [options] + * @param {String} [options.value_if_empty=''] what to display if the field's value is ``false`` + * @param {Boolean} [options.process_modifiers=true] should the modifiers be computed ? + * @param {String} [options.model] current record's model + * @param {Number} [options.id] current record's id + * @return {String} + */ + format: function (row_data, options) { + options = options || {}; + var attrs = {}; + if (options.process_modifiers !== false) { + attrs = this.modifiers_for(row_data); + } + if (attrs.invisible) { return ''; } + + if (!row_data[this.id]) { + return options.value_if_empty === undefined + ? '' + : options.value_if_empty; + } + return this._format(row_data, options); + }, + /** + * Method to override in order to provide alternative HTML content for the + * cell. Column._format will simply call ``instance.web.format_value`` and + * escape the output. + * + * The output of ``_format`` will *not* be escaped by ``format``, any + * escaping *must be done* by ``format``. + * + * @private + */ + _format: function (row_data, options) { + return _.escape(instance.web.format_value( + row_data[this.id].value, this, options.value_if_empty)); + } +}); +instance.web.list.MetaColumn = instance.web.list.Column.extend({ + meta: true, + init: function (id, string) { + this._super(id, '', {string: string}); + } +}); +instance.web.list.Button = instance.web.list.Column.extend({ + /** + * Return an actual ``<button>`` tag + */ + format: function (row_data, options) { + options = options || {}; + var attrs = {}; + if (options.process_modifiers !== false) { + attrs = this.modifiers_for(row_data); + } + if (attrs.invisible) { return ''; } + + return QWeb.render('ListView.row.button', { + widget: this, + prefix: instance.session.prefix, + disabled: attrs.readonly + || isNaN(row_data.id.value) + || instance.web.BufferedDataSet.virtual_id_regex.test(row_data.id.value) + }); + } +}); +instance.web.list.Boolean = instance.web.list.Column.extend({ + /** + * Return a potentially disabled checkbox input + * + * @private + */ + _format: function (row_data, options) { + return _.str.sprintf('<input type="checkbox" %s disabled="disabled"/>', + row_data[this.id].value ? 'checked="checked"' : ''); + } +}); +instance.web.list.Binary = instance.web.list.Column.extend({ + /** + * Return a link to the binary data as a file + * + * @private + */ + _format: function (row_data, options) { + var text = _t("Download"); + var download_url = _.str.sprintf( + '/web/binary/saveas?session_id=%s&model=%s&field=%s&id=%d', + instance.session.session_id, options.model, this.id, options.id); + if (this.filename) { + download_url += '&filename_field=' + this.filename; + if (row_data[this.filename]) { + text = _.str.sprintf(_t("Download \"%s\""), instance.web.format_value( + row_data[this.filename].value, {type: 'char'})); + } + } + return _.template('<a href="<%-href%>"><%-text%></a> (%<-size%>)', { + text: text, + href: download_url, + size: row_data[this.id].value + }); + } +}); +instance.web.list.ProgressBar = instance.web.list.Column.extend({ + /** + * Return a formatted progress bar display + * + * @private + */ + _format: function (row_data, options) { + return _.template( + '<progress value="<%-value%>" max="100"><%-value%>%</progress>', { + value: _.str.sprintf("%.0f", row_data[this.id].value || 0) + }); + } +}); +instance.web.list.Handle = instance.web.list.Column.extend({ + init: function () { + this._super.apply(this, arguments); + // Handle overrides the field to not be form-editable. + this.modifiers.readonly = true; + }, + /** + * Return styling hooks for a drag handle + * + * @private + */ + _format: function (row_data, options) { + return '<div class="oe_list_handle">'; + } +}); }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/view_list_editable.js b/addons/web/static/src/js/view_list_editable.js index c80509a7b2b..892221a347b 100644 --- a/addons/web/static/src/js/view_list_editable.js +++ b/addons/web/static/src/js/view_list_editable.js @@ -3,10 +3,6 @@ * @namespace */ openerp.web.list_editable = function (instance) { - var KEY_RETURN = 13, - KEY_ESCAPE = 27; - var QWeb = instance.web.qweb; - // editability status of list rows instance.web.ListView.prototype.defaults.editable = null; @@ -15,6 +11,15 @@ openerp.web.list_editable = function (instance) { init: function () { var self = this; this._super.apply(this, arguments); + + this._force_editability = null; + this._context_editable = false; + this.editor = this.make_editor(); + // Stores records of {field, cell}, allows for re-rendering fields + // depending on cell state during and after resize events + this.fields_for_resize = []; + instance.web.bus.on('resize', this, this.resize_fields); + $(this.groups).bind({ 'edit': function (e, id, dataset) { self.do_edit(dataset.index, id, dataset); @@ -26,7 +31,35 @@ openerp.web.list_editable = function (instance) { self.configure_pager(self.dataset); self.compute_aggregates(); } - }) + }); + + this.records.bind('remove', function () { + if (self.editor.is_editing()) { + self.cancel_edition(); + } + }); + + this.on('edit:before', this, function (event) { + if (!self.editable() || self.editor.is_editing()) { + event.cancel = true; + } + }); + this.on('edit:after', this, function () { + self.$el.add(self.$buttons).addClass('oe_editing'); + }); + this.on('save:after cancel:after', this, function () { + self.$el.add(self.$buttons).removeClass('oe_editing'); + }); + }, + destroy: function () { + instance.web.bus.off('resize', this, this.resize_fields); + this._super(); + }, + do_hide: function () { + if (this.editor.is_editing()) { + this.cancel_edition(true); + } + this._super(); }, /** * Handles the activation of a record in editable mode (making a record @@ -42,25 +75,16 @@ openerp.web.list_editable = function (instance) { do_edit: function (index, id, dataset) { _.extend(this.dataset, dataset); }, - /** - * Sets editability status for the list, based on defaults, view - * architecture and the provided flag, if any. - * - * @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom". - */ - set_editable: function (force) { - // If ``force``, set editability to bottom - // otherwise rely on view default - // view' @editable is handled separately as we have not yet - // fetched and processed the view at this point. - this.options.editable = ( - ! this.options.read_only && ((force && "bottom") || this.defaults.editable)); + editable: function () { + return this.fields_view.arch.attrs.editable + || this._context_editable + || this.options.editable; }, /** * Replace do_search to handle editability process */ do_search: function(domain, context, group_by) { - this.set_editable(context['set_editable']); + this._context_editable = !!context.set_editable; this._super.apply(this, arguments); }, /** @@ -68,18 +92,66 @@ openerp.web.list_editable = function (instance) { * as an editable row at the top or bottom of the list) */ do_add_record: function () { - if (this.options.editable) { - this.$element.find('table:first').show(); - this.$element.find('.oe_view_nocontent').remove(); - this.groups.new_record(); + if (this.editable()) { + this.$el.find('table:first').show(); + this.$el.find('.oe_view_nocontent').remove(); + this.start_edition(); } else { this._super(); } }, on_loaded: function (data, grouped) { + var self = this; // tree/@editable takes priority on everything else if present. - this.options.editable = ! this.options.read_only && (data.arch.attrs.editable || this.options.editable); - return this._super(data, grouped); + var result = this._super(data, grouped); + if (this.editable()) { + this.$el.addClass('oe_list_editable'); + // FIXME: any hook available to ensure this is only done once? + this.$buttons + .off('click', '.oe_list_save') + .on('click', '.oe_list_save', this.proxy('save_edition')) + .off('click', '.oe_list_discard') + .on('click', '.oe_list_discard', function (e) { + e.preventDefault(); + self.cancel_edition(); + }); + this.$el + .off('click', 'tbody td:not(.oe_list_field_cell)') + .on('click', 'tbody td:not(.oe_list_field_cell)', function () { + if (!self.editor.is_editing()) { + self.start_edition(); + } + }); + this.editor.destroy(); + // Editor is not restartable due to formview not being + // restartable + this.editor = this.make_editor(); + var editor_ready = this.editor.prependTo(this.$el) + .then(this.proxy('setup_events')); + + return $.when(result, editor_ready); + } else { + this.$el.removeClass('oe_list_editable'); + } + + return result; + }, + /** + * Builds a new editor object + * + * @return {instance.web.list.Editor} + */ + make_editor: function () { + return new instance.web.list.Editor(this); + }, + do_button_action: function (name, id, callback) { + var self = this, args = arguments; + this.ensure_saved().then(function (done) { + if (!id && done.created) { + id = done.record.get('id'); + } + self.handle_button.call(self, name, id, callback); + }); }, /** * Ensures the editable list is saved (saves any pending edition if @@ -90,338 +162,627 @@ openerp.web.list_editable = function (instance) { * @returns {$.Deferred} */ ensure_saved: function () { - return this.groups.ensure_saved(); - } - }); - - instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{ - passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved", - new_record: function () { - // TODO: handle multiple children - this.children[null].new_record(); + if (!this.editor.is_editing()) { + return $.when(); + } + return this.save_edition(); }, /** - * Ensures descendant editable List instances are all saved if they have - * pending editions. + * Set up the edition of a record of the list view "inline" * - * @returns {$.Deferred} + * @param {instance.web.list.Record} [record] record to edit, leave empty to create a new record + * @param {Object} [options] + * @param {String} [options.focus_field] field to focus at start of edition + * @return {jQuery.Deferred} */ - ensure_saved: function () { - return $.when.apply(null, - _.invoke( - _.values(this.children), - 'ensure_saved')); - } - }); - - instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{ - row_clicked: function (event) { - if (!this.options.editable) { - return this._super.apply(this, arguments); + start_edition: function (record, options) { + var self = this; + var item = false; + if (record) { + item = record.attributes; + } else { + var attrs = {id: false}; + _(this.columns).chain() + .filter(function (x) { return x.tag === 'field'}) + .pluck('name') + .each(function (field) { attrs[field] = false; }); + record = new instance.web.list.Record(attrs); + this.records.add(record, { + at: this.prepends_on_create() ? 0 : null}); } - this.edit_record($(event.currentTarget).data('id')); + var $recordRow = this.groups.get_row_for(record); + var cells = this.get_cells_for($recordRow); + + return this.ensure_saved().pipe(function () { + self.fields_for_resize.splice(0, self.fields_for_resize.length); + return self.with_event('edit', { + record: record.attributes, + cancel: false + }, function () { + return self.editor.edit(item, function (field_name, field) { + var cell = cells[field_name]; + if (!cell || field.get('effective_readonly')) { + // Readonly fields can just remain the list's, + // form's usually don't have backgrounds &al + field.set({invisible: true}); + return; + } + + // FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow + field.$el.attr('data-fieldname', field_name); + self.fields_for_resize.push({field: field, cell: cell}); + }, options).pipe(function () { + $recordRow.addClass('oe_edition'); + self.resize_fields(); + return record.attributes; + }); + }).fail(function () { + // if the start_edition event is cancelled and it was a + // creation, remove the newly-created empty record + if (!record.get('id')) { + self.records.remove(record); + } + }); + }); + }, + get_cells_for: function ($row) { + var cells = {}; + $row.children('td').each(function (index, el) { + cells[el.getAttribute('data-field')] = el + }); + return cells; }, /** - * Checks if a record is being edited, and if so cancels it + * If currently editing a row, resizes all registered form fields based + * on the corresponding row cell */ - cancel_pending_edition: function () { - var self = this, cancelled = $.Deferred(); - if (!this.edition) { - cancelled.resolve(); - return cancelled.promise(); - } - - if (this.edition_id != null) { - this.reload_record(self.records.get(this.edition_id)).then(function () { - cancelled.resolve(); - }); - } else { - cancelled.resolve(); + resize_fields: function () { + if (!this.editor.is_editing()) { return; } + for(var i=0, len=this.fields_for_resize.length; i<len; ++i) { + var item = this.fields_for_resize[i]; + this.resize_field(item.field, item.cell); } - cancelled.then(function () { - self.view.unpad_columns(); - self.edition_form.destroy(); - self.edition_form.$element.remove(); - delete self.edition_form; - delete self.edition_id; - delete self.edition; - }); - this.pad_table_to(5); - return cancelled.promise(); }, /** - * Adapts this list's view description to be suitable to the inner form - * view of a row being edited. + * Resizes a field's root element based on the corresponding cell of + * a listview row * - * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows. + * @param {instance.web.form.AbstractField} field + * @param {jQuery} cell */ - get_form_fields_view: function () { - // deep copy of view - var view = $.extend(true, {}, this.group.view.fields_view); - _(view.arch.children).each(function (widget) { - widget.attrs.nolabel = true; - if (widget.tag === 'button') { - delete widget.attrs.string; - } + resize_field: function (field, cell) { + var $cell = $(cell); + var position = $cell.position(); + + // jquery does not understand !important + field.$el.attr('style', 'width: '+$cell.outerWidth()+'px !important'); + field.$el.css({ + top: position.top, + left: position.left, + minHeight: $cell.outerHeight() }); - view.arch.attrs.col = 2 * view.arch.children.length; - return view; }, - on_row_keyup: function (e) { + /** + * @return {jQuery.Deferred} + */ + save_edition: function () { var self = this; - switch (e.which) { - case KEY_RETURN: - this.save_row().then(function (result) { - if (result.created) { - self.new_record(); - return; - } - - var next_record_id, - next_record = self.records.at( - self.records.indexOf(result.edited_record) + 1); - if (next_record) { - next_record_id = next_record.get('id'); - self.dataset.index = _(self.dataset.ids) - .indexOf(next_record_id); - } else { - self.dataset.index = 0; - next_record_id = self.records.at(0).get('id'); + return this.with_event('save', { + editor: this.editor, + form: this.editor.form, + cancel: false + }, function () { + return this.editor.save().pipe(function (attrs) { + var created = false; + var record = self.records.get(attrs.id); + if (!record) { + // new record + created = true; + record = self.records.find(function (r) { + return !r.get('id'); + }).set('id', attrs.id); } - self.edit_record(next_record_id); + // onwrite callback could be altering & reloading the + // record which has *just* been saved, so first perform all + // onwrites then do a final reload of the record + return self.handle_onwrite(record) + .pipe(function () { + return self.reload_record(record); }) + .pipe(function () { + return { created: created, record: record }; }); }); - break; - case KEY_ESCAPE: - this.cancel_edition(); - break; - } + }); }, - render_row_as_form: function (row) { + /** + * @param {Boolean} [force=false] discards the data even if the form has been edited + * @return {jQuery.Deferred} + */ + cancel_edition: function (force) { var self = this; - this.cancel_pending_edition().then(function () { - var record_id = $(row).data('id'); - var $new_row = $('<tr>', { - id: _.uniqueId('oe-editable-row-'), - 'data-id': record_id, - 'class': (row ? $(row).attr('class') : '') + ' oe_form', - click: function (e) {e.stopPropagation();} - }) - .delegate('button.oe-edit-row-save', 'click', function () { - self.save_row(); - }) - .delegate('button', 'keyup', function (e) { - e.stopImmediatePropagation(); - }) - .keyup(function () { - return self.on_row_keyup.apply(self, arguments); }) - .keydown(function (e) { e.stopPropagation(); }); - if (row) { - $new_row.replaceAll(row); - } else if (self.options.editable) { - var $last_child = self.$current.children('tr:last'); - if (self.records.length) { - if (self.options.editable === 'top') { - $new_row.insertBefore( - self.$current.children('[data-id]:first')); - } else { - $new_row.insertAfter( - self.$current.children('[data-id]:last')); + return this.with_event('cancel', { + editor: this.editor, + form: this.editor.form, + cancel: false + }, function () { + return this.editor.cancel(force).pipe(function (attrs) { + if (attrs.id) { + var record = self.records.get(attrs.id); + if (!record) { + // Record removed by third party during edition + return } - } else { - $new_row.prependTo(self.$current); - } - if ($last_child.is(':not([data-id])')) { - $last_child.remove(); + return self.reload_record(record); } - } - self.edition = true; - self.edition_id = record_id; - $new_row.addClass("oe_form_container"); - self.edition_form = new instance.web.ListEditableFormView(self.view, self.dataset, false); - self.edition_form.$element = $new_row; - self.edition_form.editable_list = self; - // HO HO - // empty - $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () { - // put in $.when just in case FormView.on_loaded becomes asynchronous - $new_row.find('> td') - .end() - .find('td:last').removeClass('oe-field-cell').end(); - // pad in case of groupby - _(self.columns).each(function (column) { - if (column.meta) { - $new_row.prepend('<td>'); - } + var to_delete = self.records.find(function (r) { + return !r.get('id'); }); - // Add column for the save, if - // there is none in the list - if (!self.options.deletable) { - self.view.pad_columns( - 1, {except: $new_row}); + if (to_delete) { + self.records.remove(to_delete); } - - self.edition_form.do_show(); }); }); }, - handle_onwrite: function (source_record_id) { + /** + * Executes an action on the view's editor bracketed by a cancellable + * event of the name provided. + * + * The event name provided will be post-fixed with ``:before`` and + * ``:after``, the ``event`` parameter will be passed alongside the + * ``:before`` variant and if the parameter's ``cancel`` key is set to + * ``true`` the action *will not be called* and the method will return + * a rejection + * + * @param {String} event_name name of the event + * @param {Object} event event object, provided to ``:before`` sub-event + * @param {Function} action callable, called with the view's editor as its context + * @param {Array} [args] supplementary arguments provided to the action + * @param {Array} [trigger_params] supplementary arguments provided to the ``:after`` sub-event, before anything fetched by the ``action`` function + * @return {jQuery.Deferred} + */ + with_event: function (event_name, event, action) { var self = this; - var on_write_callback = self.view.fields_view.arch.attrs.on_write; - if (!on_write_callback) { return; } - this.dataset.call(on_write_callback, [source_record_id], function (ids) { - _(ids).each(function (id) { - var record = self.records.get(id); - if (!record) { - // insert after the source record - var index = self.records.indexOf( - self.records.get(source_record_id)) + 1; - record = new instance.web.list.Record({id: id}); - self.records.add(record, {at: index}); - self.dataset.ids.splice(index, 0, id); + event = event || {}; + this.trigger(event_name + ':before', event); + if (event.cancel) { + return $.Deferred().reject({ + message: _.str.sprintf("Event %s:before cancelled", + event_name)}); + } + return $.when(action.call(this)).then(function () { + self.trigger.apply(self, [event_name + ':after'] + .concat(_.toArray(arguments))); + }); + }, + edition_view: function (editor) { + var view = $.extend(true, {}, this.fields_view); + view.arch.tag = 'form'; + _.extend(view.arch.attrs, { + 'class': 'oe_form_container', + version: '7.0' + }); + _(view.arch.children).chain() + .zip(this.columns) + .each(function (ar) { + var widget = ar[0], column = ar[1]; + var modifiers = _.extend({}, column.modifiers); + widget.attrs.nolabel = true; + if (modifiers['tree_invisible'] || widget.tag === 'button') { + modifiers.invisible = true; } - self.reload_record(record); + widget.attrs.modifiers = JSON.stringify(modifiers); + }); + return view; + }, + handle_onwrite: function (source_record) { + var self = this; + var on_write_callback = self.fields_view.arch.attrs.on_write; + if (!on_write_callback) { return $.when(); } + return this.dataset.call(on_write_callback, [source_record.get('id')]) + .pipe(function (ids) { + return $.when.apply( + null, _(ids).map( + _.bind(self.handle_onwrite_record, self, source_record))); }); + }, + handle_onwrite_record: function (source_record, id) { + var record = this.records.get(id); + if (!record) { + // insert after the source record + var index = this.records.indexOf(source_record) + 1; + record = new instance.web.list.Record({id: id}); + this.records.add(record, {at: index}); + this.dataset.ids.splice(index, 0, id); + } + return this.reload_record(record); + }, + prepends_on_create: function () { + return this.editable() === 'top'; + }, + setup_events: function () { + var self = this; + this.editor.$el.on('keyup keydown', function (e) { + if (!self.editor.is_editing()) { return; } + var key = _($.ui.keyCode).chain() + .map(function (v, k) { return {name: k, code: v}; }) + .find(function (o) { return o.code === e.which; }) + .value(); + if (!key) { return; } + var method = e.type + '_' + key.name; + if (!(method in self)) { return; } + self[method](e); }); }, /** - * Saves the current row, and returns a Deferred resolving to an object - * with the following properties: + * Saves the current record, and goes to the next one (creation or + * edition) * - * ``created`` - * Boolean flag indicating whether the record saved was being created - * (``true`` or edited (``false``) - * ``edited_record`` - * The result of saving the record (either the newly created record, - * or the post-edition record), after insertion in the Collection if - * needs be. - * - * @returns {$.Deferred<{created: Boolean, edited_record: Record}>} + * @private + * @param {String} [next_record='succ'] method to call on the records collection to get the next record to edit + * @param {Object} [options] + * @param {String} [options.focus_field] + * @return {*} */ - save_row: function () { - //noinspection JSPotentiallyInvalidConstructorUsage - var self = this, done = $.Deferred(); - return this.edition_form - .do_save(null, this.options.editable === 'top') - .pipe(function (result) { - if (result.created && !self.edition_id) { - self.records.add({id: result.result}, - {at: self.options.editable === 'top' ? 0 : null}); - self.edition_id = result.result; - } - var edited_record = self.records.get(self.edition_id); - - return $.when( - self.handle_onwrite(self.edition_id), - self.cancel_pending_edition().then(function () { - $(self).trigger('saved', [self.dataset]); - })).pipe(function () { - return { - created: result.created || false, - edited_record: edited_record - }; - }, null); - }, null); + _next: function (next_record, options) { + next_record = next_record || 'succ'; + var self = this; + return this.save_edition().pipe(function (saveInfo) { + if (saveInfo.created) { + return self.start_edition(); + } + var record = self.records[next_record]( + saveInfo.record, {wraparound: true}); + return self.start_edition(record, options); + }); + }, + keyup_ENTER: function () { + return this._next(); + }, + keyup_ESCAPE: function () { + return this.cancel_edition(); }, /** - * If the current list is being edited, ensures it's saved + * Gets the selection range (start, end) for the provided element, + * returns ``null`` if it can't get a range. + * + * @private */ - ensure_saved: function () { - if (this.edition) { - return this.save_row(); + _text_selection_range: function (el) { + var selectionStart; + try { + selectionStart = el.selectionStart; + } catch (e) { + // radio or checkbox throw on selectionStart access + return null; + } + if (selectionStart !== undefined) { + return { + start: selectionStart, + end: el.selectionEnd + }; + } else if (document.body.createTextRange) { + throw new Error("Implement text range handling for MSIE"); + var sel = document.body.createTextRange(); + if (sel.parentElement() === el) { + + } } - //noinspection JSPotentiallyInvalidConstructorUsage - return $.Deferred().resolve().promise(); + // Element without selection ranges (select, div/@contenteditable) + return null; + }, + _text_cursor: function (el) { + var selection = this._text_selection_range(el); + if (!selection) { + return null; + } + if (selection.start !== selection.end) { + return {position: null, collapsed: false}; + } + return {position: selection.start, collapsed: true}; + }, + /** + * Checks if the cursor is at the start of the provided el + * + * @param {HTMLInputElement | HTMLTextAreaElement} + * @returns {Boolean} + * @private + */ + _at_start: function (cursor, el) { + return cursor.collapsed && (cursor.position === 0); }, /** - * Cancels the edition of the row for the current dataset index + * Checks if the cursor is at the end of the provided el + * + * @param {HTMLInputElement | HTMLTextAreaElement} + * @returns {Boolean} + * @private */ - cancel_edition: function () { - this.cancel_pending_edition(); + _at_end: function (cursor, el) { + return cursor.collapsed && (cursor.position === el.value.length); }, /** - * Edits record currently selected via dataset + * @param DOMEvent event + * @param {String} record_direction direction to move into to get the next record (pred | succ) + * @param {Function} is_valid_move whether the edition should be moved to the next record + * @private */ - edit_record: function (record_id) { - this.render_row_as_form( - this.$current.find('[data-id=' + record_id + ']')); - $(this).trigger( - 'edit', - [record_id, this.dataset]); - }, - new_record: function () { - this.dataset.index = null; - this.render_row_as_form(); - }, - render_record: function (record) { - var index = this.records.indexOf(record), - self = this; - // FIXME: context dict should probably be extracted cleanly - return QWeb.render('ListView.row', { - columns: this.columns, - options: this.options, - record: record, - row_parity: (index % 2 === 0) ? 'even' : 'odd', - view: this.view, - render_cell: function () { - return self.render_cell.apply(self, arguments); }, - edited: !!this.edition_form + _key_move_record: function (event, record_direction, is_valid_move) { + if (!this.editor.is_editing('edit')) { return $.when(); } + var cursor = this._text_cursor(event.target); + // if text-based input (has a cursor) + // and selecting (not collapsed) or not at a field boundary + // don't move to the next record + if (cursor && !is_valid_move(event.target, cursor)) { return $.when(); } + + event.preventDefault(); + var source_field = $(event.target).closest('[data-fieldname]') + .attr('data-fieldname'); + return this._next(record_direction, {focus_field: source_field}); + + }, + keydown_UP: function (e) { + var self = this; + return this._key_move_record(e, 'pred', function (el, cursor) { + return self._at_start(cursor, el); }); + }, + keydown_DOWN: function (e) { + var self = this; + return this._key_move_record(e, 'succ', function (el, cursor) { + return self._at_end(cursor, el); + }); + }, + + keydown_LEFT: function (e) { + // If the cursor is at the beginning of the field + var source_field = $(e.target).closest('[data-fieldname]') + .attr('data-fieldname'); + var cursor = this._text_cursor(e.target); + if (cursor && !this._at_start(cursor, e.target)) { return $.when(); } + + var fields_order = this.editor.form.fields_order; + var field_index = _(fields_order).indexOf(source_field); + + // Look for the closest visible form field to the left + var fields = this.editor.form.fields; + var field; + do { + if (--field_index < 0) { return $.when(); } + + field = fields[fields_order[field_index]]; + } while (!field.$el.is(':visible')); + + // and focus it + field.focus(); + return $.when(); + }, + keydown_RIGHT: function (e) { + // same as above, but with cursor at the end of the field and + // looking for new fields at the right + var source_field = $(e.target).closest('[data-fieldname]') + .attr('data-fieldname'); + var cursor = this._text_cursor(e.target); + if (cursor && !this._at_end(cursor, e.target)) { return $.when(); } + + var fields_order = this.editor.form.fields_order; + var field_index = _(fields_order).indexOf(source_field); + + var fields = this.editor.form.fields; + var field; + do { + if (++field_index >= fields_order.length) { return $.when(); } + + field = fields[fields_order[field_index]]; + } while (!field.$el.is(':visible')); + + field.focus(); + return $.when(); + }, + keydown_TAB: function (e) { + var form = this.editor.form; + var last_field = _(form.fields_order).chain() + .map(function (name) { return form.fields[name]; }) + .filter(function (field) { return field.$el.is(':visible'); }) + .last() + .value(); + // tabbed from last field in form + if (last_field && last_field.$el.has(e.target).length) { + e.preventDefault(); + return this._next(); + } + return $.when(); } }); - - instance.web.ListEditableFormView = instance.web.FormView.extend({ - init: function() { - this._super.apply(this, arguments); - this.rendering_engine = new instance.web.ListEditableRenderingEngine(this); - this.options.initial_mode = "edit"; + + instance.web.list.Editor = instance.web.Widget.extend({ + /** + * @constructs instance.web.list.Editor + * @extends instance.web.Widget + * + * Adapter between listview and formview for editable-listview purposes + * + * @param {instance.web.Widget} parent + * @param {Object} options + * @param {instance.web.FormView} [options.formView=instance.web.FormView] + * @param {Object} [options.delegate] + */ + init: function (parent, options) { + this._super(parent); + this.options = options || {}; + _.defaults(this.options, { + formView: instance.web.FormView, + delegate: this.getParent() + }); + this.delegate = this.options.delegate; + + this.record = null; + + this.form = new (this.options.formView)( + this, this.delegate.dataset, false, { + initial_mode: 'edit', + disable_autofocus: true, + $buttons: $(), + $pager: $() + }); }, - renderElement: function() {} - }); - - instance.web.ListEditableRenderingEngine = instance.web.form.FormRenderingEngineInterface.extend({ - init: function(view) { - this.view = view; + start: function () { + var self = this; + var _super = this._super(); + this.form.embedded_view = this._validate_view( + this.delegate.edition_view(this)); + var form_ready = this.form.appendTo(this.$el).then( + self.form.proxy('do_hide')); + return $.when(_super, form_ready); }, - set_fields_view: function(fields_view) { - this.fvg = fields_view; + _validate_view: function (edition_view) { + if (!edition_view) { + throw new Error("editor delegate's #edition_view must return " + + "a view descriptor"); + } + var arch = edition_view.arch; + if (!(arch && arch.children instanceof Array)) { + throw new Error("Editor delegate's #edition_view must have a" + + " non-empty arch") + } + if (!(arch.tag === "form")) { + throw new Error("Editor delegate's #edition_view must have a" + + " 'form' root node"); + } + if (!(arch.attrs && arch.attrs.version === "7.0")) { + throw new Error("Editor delegate's #edition_view must be a" + + " version 7 view"); + } + if (!/\boe_form_container\b/.test(arch.attrs['class'])) { + throw new Error("Editor delegate's #edition_view must have the" + + " class 'oe_form_container' on its root" + + " element"); + } + + return edition_view; }, - set_tags_registry: function(tags_registry) { - this.tags_registry = tags_registry; + + /** + * + * @param {String} [state] either ``new`` or ``edit`` + * @return {Boolean} + */ + is_editing: function (state) { + if (!this.record) { + return false; + } + switch(state) { + case null: case undefined: + return true; + case 'new': return !this.record.id; + case 'edit': return !!this.record.id; + } + throw new Error("is_editing's state filter must be either `new` or" + + " `edit` if provided"); }, - set_fields_registry: function(fields_registry) { - this.fields_registry = fields_registry; + _focus_setup: function (focus_field) { + var form = this.form; + + var field; + // If a field to focus was specified + if (focus_field + // Is actually in the form + && (field = form.fields[focus_field]) + // And is visible + && field.$el.is(':visible')) { + // focus it + field.focus(); + return; + } + + _(form.fields_order).detect(function (name) { + // look for first visible field in fields_order, focus it + var field = form.fields[name]; + if (!field.$el.is(':visible')) { + return false; + } + // Stop as soon as a field got focused + return field.focus() !== false; + }); }, - render_to: function($element) { + edit: function (record, configureField, options) { + // TODO: specify sequence of edit calls var self = this; - - var xml = instance.web.json_node_to_xml(this.fvg.arch); - var $xml = $(xml); - - if (this.view.editable_list.options.selectable) - $("<td>").appendTo($element); - - $xml.children().each(function(i, el) { - var modifiers = JSON.parse($(el).attr("modifiers") || "{}"); - var $td = $("<td>"); - if (modifiers.tree_invisible === true) - $td.hide(); - var tag_name = el.tagName.toLowerCase(); - var w; - if (tag_name === "field") { - var name = $(el).attr("name"); - var key = $(el).attr('widget') || self.fvg.fields[name].type; - var obj = self.view.fields_registry.get_object(key); - w = new (obj)(self.view, instance.web.xml_to_json(el)); - self.view.register_field(w, $(el).attr("name")); - } else { - var obj = self.tags_registry.get_object(tag_name); - w = new (obj)(self.view, instance.web.xml_to_json(el)); - } - w.appendTo($td); - $td.appendTo($element); + var form = self.form; + var loaded = record + ? form.on_record_loaded(_.extend({}, record)) + : form.load_defaults(); + + return loaded.pipe(function () { + return form.do_show({reload: false}); + }).pipe(function () { + self.record = form.datarecord; + _(form.fields).each(function (field, name) { + configureField(name, field); + }); + self._focus_setup(options && options.focus_field); + return form; }); - save = QWeb.render('ListView.row.save'); - $(save).appendTo($element); }, + save: function () { + var self = this; + return this.form + .do_save(this.delegate.prepends_on_create()) + .pipe(function (result) { + var created = result.created && !self.record.id; + if (created) { + self.record.id = result.result; + } + return self.cancel(); + }); + }, + cancel: function (force) { + if (!(force || this.form.can_be_discarded())) { + return $.Deferred().reject({ + message: "The form's data can not be discarded"}).promise(); + } + var record = this.record; + this.record = null; + this.form.do_hide(); + return $.when(record); + } + }); + + instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{ + passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved", + get_row_for: function (record) { + return _(this.children).chain() + .invoke('get_row_for', record) + .compact() + .first() + .value(); + } + }); + + instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{ + row_clicked: function (event) { + if (!this.view.editable() || ! this.view.is_action_enabled('edit')) { + return this._super.apply(this, arguments); + } + var record_id = $(event.currentTarget).data('id'); + this.view.start_edition( + record_id ? this.records.get(record_id) : null, { + focus_field: $(event.target).data('field') + }); + }, + /** + * If a row mapping to the record (@data-id matching the record's id or + * no @data-id if the record has no id), returns it. Otherwise returns + * ``null``. + * + * @param {Record} record the record to get a row for + * @return {jQuery|null} + */ + get_row_for: function (record) { + var id; + var $row = this.$current.children('[data-id=' + record.get('id') + ']'); + if ($row.length) { + return $row; + } + return null; + } }); }; diff --git a/addons/web/static/src/js/view_tree.js b/addons/web/static/src/js/view_tree.js index 9e938fe293a..e0493a446e2 100644 --- a/addons/web/static/src/js/view_tree.js +++ b/addons/web/static/src/js/view_tree.js @@ -63,6 +63,12 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie } return fields; }, + store_record:function(records){ + var self = this; + _(records).each(function (record) { + self.records[record.id] = record; + }); + }, on_loaded: function (fields_view) { var self = this; var has_toolbar = !!fields_view.arch.attrs.toolbar; @@ -78,25 +84,28 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie }); this.fields = fields_view.fields; this.hook_row_click(); - this.$element.html(QWeb.render('TreeView', { + this.$el.html(QWeb.render('TreeView', { 'title': this.fields_view.arch.attrs.string, 'fields_view': this.fields_view.arch.children, 'fields': this.fields, 'toolbar': has_toolbar })); + this.$el.addClass(this.fields_view.arch.attrs['class']); this.dataset.read_slice(this.fields_list()).then(function(records) { + self.store_record(records); if (!has_toolbar) { // WARNING: will do a second read on the same ids, but only on // first load so not very important - self.getdata(null, _(records).pluck('id')); + self.render_data({'null':records}) + self.getdata(_.pluck(records,"id")); return; } - var $select = self.$element.find('select') + var $select = self.$el.find('select') .change(function () { var $option = $(this).find(':selected'); - self.getdata($option.val(), $option.data('children')); + self.getdata($option.val()); }); _(records).each(function (record) { self.records[record.id] = record; @@ -111,7 +120,12 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie $select.change(); } }); - + this.$el.find("#tree_view_expand").click(function(){ + self.expand_all(); + }); + this.$el.find("#tree_view_collapse").click(function(){ + self.collpase_all(); + }); // TODO store open nodes in url ?... this.do_push_state({}); @@ -127,6 +141,22 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie return [color, py.parse(py.tokenize(expr)), expr]; }).value(); }, + expand_all: function(){ + var self = this; + var tr = this.$el.find(".oe-treeview-table tbody tr[id^='treerow_']"); + _.each(tr,function(rec){ + self.showcontent($(rec).attr('data-id'),true); + }); + }, + collpase_all: function(){ + var self = this; + var root_tr = this.$el.find(".oe-treeview-table tbody tr[data-level='"+1+"']"); + _.each(root_tr,function(rec){ + if($(rec).hasClass('oe_open')){ + self.showcontent($(rec).attr('data-id'),false); + } + }); + }, /** * Returns the color for the provided record in the current view (from the * ``@colors`` attribute) @@ -157,65 +187,61 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie */ hook_row_click: function () { var self = this; - this.$element.delegate('.treeview-td span, .treeview-tr span', 'click', function (e) { + this.$el.delegate('.treeview-td span, .treeview-tr span', 'click', function (e) { e.stopImmediatePropagation(); self.activate($(this).closest('tr').data('id')); }); - this.$element.delegate('.treeview-tr', 'click', function () { - var is_loaded = 0, - $this = $(this), + this.$el.delegate('.treeview-tr', 'click', function () { + var $this = $(this), record_id = $this.data('id'), - record = self.records[record_id], - children_ids = record[self.children_field]; - - _(children_ids).each(function(childid) { - if (self.$element.find('#treerow_' + childid).length) { - if (self.$element.find('#treerow_' + childid).is(':hidden')) { - is_loaded = -1; - } else { - is_loaded++; - } - } - }); - if (is_loaded === 0) { - if (!$this.parent().hasClass('oe-open')) { - self.getdata(record_id, children_ids); - } - } else { - self.showcontent(record_id, is_loaded < 0); - } + bool = $this.parent().hasClass('oe_open'); + self.showcontent(record_id, !bool); }); }, // get child data of selected value - getdata: function (id, children_ids) { + getdata: function (id) { var self = this; - - self.dataset.read_ids(children_ids, this.fields_list()).then(function(records) { - _(records).each(function (record) { - self.records[record.id] = record; - }); - - var $curr_node = self.$element.find('#treerow_' + id); + var parent_child ={}; + id = _.isArray(id)?id:parseInt(id); + var ir_model_data = new instance.web.Model(this.model,self.dataset.get_context() || {},[['id','child_of',id]]).query(); + ir_model_data._execute().then(function(records){ + self.store_record(records); + _.each(records,function(rec){ + if(rec[self.children_field].length === 0)return; + parent_child[rec.id] = []; + _.each(rec[self.children_field],function(key){ + parent_child[rec.id].push(self.records[key]); + }); + }) + self.render_data(parent_child); + }); + }, + render_data: function(groupby){ + var self = this; + _.each(_.keys(groupby),function(key){ + var $curr_node = self.$el.find('#treerow_' + key); + var record = groupby[key]; var children_rows = QWeb.render('TreeView.rows', { - 'records': records, + 'records': record, 'children_field': self.children_field, 'fields_view': self.fields_view.arch.children, 'fields': self.fields, - 'level': $curr_node.data('level') || 0, + 'level': ($curr_node.data('level') || 0) + 1, 'render': instance.web.format_value, 'color_for': self.color_for }); - if ($curr_node.length) { - $curr_node.addClass('oe-open'); + $curr_node.addClass('oe_open'); $curr_node.after(children_rows); } else { - self.$element.find('tbody').html(children_rows); + self.$el.find('tbody').html(children_rows); } }); + self.collpase_all(); }, + // Get details in listview activate: function(id) { var self = this; @@ -246,25 +272,17 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie // show & hide the contents showcontent: function (record_id, show) { - this.$element.find('#treerow_' + record_id) - .toggleClass('oe-open', show); + this.$el.find('#treerow_' + record_id) + .toggleClass('oe_open', show); _(this.records[record_id][this.children_field]).each(function (child_id) { - var $child_row = this.$element.find('#treerow_' + child_id); - if ($child_row.hasClass('oe-open')) { + var $child_row = this.$el.find('#treerow_' + child_id); + if ($child_row.hasClass('oe_open')) { this.showcontent(child_id, false); } $child_row.toggle(show); }, this); }, - do_show: function () { - this.$element.show(); - }, - - do_hide: function () { - this.$element.hide(); - this.hidden = true; - } }); }; diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index 753e14fa251..9e7db823bc0 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -10,51 +10,189 @@ instance.web.ActionManager = instance.web.Widget.extend({ init: function(parent) { this._super(parent); this.inner_action = null; - this.inner_viewmanager = null; + this.inner_widget = null; this.dialog = null; - this.dialog_viewmanager = null; - this.client_widget = null; + this.dialog_widget = null; + this.breadcrumbs = []; + this.on('history_back', this, function() { + return this.history_back(); + }); + }, + start: function() { + this._super.apply(this, arguments); + this.$el.on('click', 'a.oe_breadcrumb_item', this.on_breadcrumb_clicked); }, dialog_stop: function () { if (this.dialog) { - this.dialog_viewmanager.destroy(); - this.dialog_viewmanager = null; + this.dialog_widget.destroy(); + this.dialog_widget = null; this.dialog.destroy(); this.dialog = null; } }, - content_stop: function () { - if (this.inner_viewmanager) { - this.inner_viewmanager.destroy(); - this.inner_viewmanager = null; + /** + * Add a new item to the breadcrumb + * + * If the title of an item is an array, the multiple title mode is in use. + * (eg: a widget with multiple views might need to display a title for each view) + * In multiple title mode, the show() callback can check the index it receives + * in order to detect which of its titles has been clicked on by the user. + * + * @param {Object} item breadcrumb item + * @param {Object} item.widget widget containing the view(s) to be added to the breadcrumb added + * @param {Function} [item.show] triggered whenever the widget should be shown back + * @param {Function} [item.hide] triggered whenever the widget should be shown hidden + * @param {Function} [item.destroy] triggered whenever the widget should be destroyed + * @param {String|Array} [item.title] title(s) of the view(s) to be displayed in the breadcrumb + * @param {Function} [item.get_title] should return the title(s) of the view(s) to be displayed in the breadcrumb + */ + push_breadcrumb: function(item) { + var last = this.breadcrumbs.slice(-1)[0]; + if (last) { + last.hide(); + } + var item = _.extend({ + show: function(index) { + this.widget.$el.show(); + }, + hide: function() { + this.widget.$el.hide(); + }, + destroy: function() { + this.widget.destroy(); + }, + get_title: function() { + return this.title || this.widget.get('title'); + } + }, item); + item.id = _.uniqueId('breadcrumb_'); + this.breadcrumbs.push(item); + }, + history_back: function() { + var last = this.breadcrumbs.slice(-1)[0]; + if (!last) { + return false; + } + var title = last.get_title(); + if (_.isArray(title) && title.length > 1) { + return this.select_breadcrumb(this.breadcrumbs.length - 1, title.length - 2); + } else if (this.breadcrumbs.length === 1) { + // Only one single titled item in breadcrumb, most of the time you want to trigger back to home + return false; + } else { + var prev = this.breadcrumbs[this.breadcrumbs.length - 2]; + title = prev.get_title(); + return this.select_breadcrumb(this.breadcrumbs.length - 2, _.isArray(title) ? title.length - 1 : undefined); + } + }, + on_breadcrumb_clicked: function(ev) { + var $e = $(ev.target); + var id = $e.data('id'); + var index; + for (var i = this.breadcrumbs.length - 1; i >= 0; i--) { + if (this.breadcrumbs[i].id == id) { + index = i; + break; + } + } + var subindex = $e.parent().find('a.oe_breadcrumb_item[data-id=' + $e.data('id') + ']').index($e); + this.select_breadcrumb(index, subindex); + }, + select_breadcrumb: function(index, subindex) { + for (var i = this.breadcrumbs.length - 1; i >= 0; i--) { + if (i > index) { + if (this.remove_breadcrumb(i) === false) { + return false; + } + } + } + var item = this.breadcrumbs[index]; + item.show(subindex); + this.inner_widget = item.widget; + return true; + }, + clear_breadcrumbs: function() { + for (var i = this.breadcrumbs.length - 1; i >= 0; i--) { + if (this.remove_breadcrumb(0) === false) { + break; + } + } + }, + remove_breadcrumb: function(index) { + var item = this.breadcrumbs.splice(index, 1)[0]; + if (item) { + var dups = _.filter(this.breadcrumbs, function(it) { + return item.widget === it.widget; + }); + if (!dups.length) { + if (this.getParent().has_uncommitted_changes()) { + this.inner_widget = item.widget; + this.breadcrumbs.splice(index, 0, item); + return false; + } else { + item.destroy(); + } + } } - if (this.client_widget) { - this.client_widget.destroy(); - this.client_widget = null; + var last_widget = this.breadcrumbs.slice(-1)[0]; + this.inner_widget = last_widget && last_widget.widget; + }, + get_title: function() { + var titles = []; + for (var i = 0; i < this.breadcrumbs.length; i += 1) { + var item = this.breadcrumbs[i]; + var tit = item.get_title(); + if (!_.isArray(tit)) { + tit = [tit]; + } + for (var j = 0; j < tit.length; j += 1) { + var label = _.escape(tit[j]); + if (i === this.breadcrumbs.length - 1 && j === tit.length - 1) { + titles.push(_.str.sprintf('<span class="oe_breadcrumb_item">%s</span>', label)); + } else { + titles.push(_.str.sprintf('<a href="#" class="oe_breadcrumb_item" data-id="%s">%s</a>', item.id, label)); + } + } } + return titles.join(' <span class="oe_fade">/</span> '); }, do_push_state: function(state) { + state = state || {}; if (this.getParent() && this.getParent().do_push_state) { if (this.inner_action) { - state['model'] = this.inner_action.res_model; + state['title'] = this.inner_action.name; + if(this.inner_action.type == 'ir.actions.act_window') { + state['model'] = this.inner_action.res_model; + } if (this.inner_action.id) { - state['action_id'] = this.inner_action.id; + state['action'] = this.inner_action.id; + } else if (this.inner_action.type == 'ir.actions.client') { + state['action'] = this.inner_action.tag; + //state = _.extend(this.inner_action.params || {}, state); } } - this.getParent().do_push_state(state); + if(!this.dialog) { + this.getParent().do_push_state(state); + } } }, do_load_state: function(state, warm) { var self = this, action_loaded; - if (state.action_id) { - var run_action = (!this.inner_viewmanager) || this.inner_viewmanager.action.id !== state.action_id; - if (run_action) { + if (state.action) { + if (_.isString(state.action) && instance.web.client_actions.contains(state.action)) { + var action_client = {type: "ir.actions.client", tag: state.action, params: state}; this.null_action(); - action_loaded = this.do_action(state.action_id); - instance.webclient.menu.has_been_loaded.then(function() { - instance.webclient.menu.open_action(state.action_id); - }); + action_loaded = this.do_action(action_client); + } else { + var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.action.id !== state.action; + if (run_action) { + this.null_action(); + action_loaded = this.do_action(state.action); + instance.webclient.menu.has_been_loaded.then(function() { + instance.webclient.menu.open_action(state.action); + }); + } } } else if (state.model && state.id) { // TODO handle context & domain ? @@ -68,38 +206,37 @@ instance.web.ActionManager = instance.web.Widget.extend({ action_loaded = this.do_action(action); } else if (state.sa) { // load session action - var self = this; this.null_action(); action_loaded = this.rpc('/web/session/get_session_action', {key: state.sa}).pipe(function(action) { if (action) { return self.do_action(action); } }); - } else if (state.client_action) { - this.null_action(); - this.ir_actions_client(state.client_action); } $.when(action_loaded || null).then(function() { - if (self.inner_viewmanager) { - self.inner_viewmanager.do_load_state(state, warm); + if (self.inner_widget && self.inner_widget.do_load_state) { + self.inner_widget.do_load_state(state, warm); } }); }, - do_action: function(action, on_close) { - if (_.isNumber(action)) { + do_action: function(action, on_close, clear_breadcrumbs) { + if (_.isString(action) && instance.web.client_actions.contains(action)) { + var action_client = { type: "ir.actions.client", tag: action }; + return this.do_action(action_client, on_close, clear_breadcrumbs); + } else if (_.isNumber(action) || _.isString(action)) { var self = this; - return self.rpc("/web/action/load", { action_id: action }, function(result) { - self.do_action(result.result, on_close); + return self.rpc("/web/action/load", { action_id: action }).pipe(function(result) { + return self.do_action(result.result, on_close, clear_breadcrumbs); }); } if (!action.type) { console.error("No type for action", action); - return; + return $.Deferred().reject(); } var type = action.type.replace(/\./g,'_'); var popup = action.target === 'new'; - var inline = action.target === 'inline'; + var inline = action.target === 'inline' || action.target === 'inlineview'; action.flags = _.extend({ views_switcher : !popup && !inline, search_view : !popup && !inline, @@ -110,47 +247,79 @@ instance.web.ActionManager = instance.web.Widget.extend({ }, action.flags || {}); if (!(type in this)) { console.error("Action manager can't handle action of type " + action.type, action); - return; + return $.Deferred().reject(); } - return this[type](action, on_close); + return this[type](action, on_close, clear_breadcrumbs); }, null_action: function() { this.dialog_stop(); - this.content_stop(); - }, - ir_actions_act_window: function (action, on_close) { - var self = this; - if (_(['base.module.upgrade', 'base.setup.installer']) - .contains(action.res_model)) { - var old_close = on_close; - on_close = function () { - instance.webclient.do_reload().then(old_close); + this.clear_breadcrumbs(); + }, + ir_actions_common: function(action, on_close, clear_breadcrumbs) { + var self = this, klass, widget, post_process; + if (this.inner_widget && (action.type === 'ir.actions.client' || action.target !== 'new')) { + if (this.getParent().has_uncommitted_changes()) { + return $.Deferred().reject(); + } else if (clear_breadcrumbs) { + this.clear_breadcrumbs(); + } + } + if (action.type === 'ir.actions.client') { + var ClientWidget = instance.web.client_actions.get_object(action.tag); + widget = new ClientWidget(this, action.params); + klass = 'oe_act_client'; + post_process = function() { + self.push_breadcrumb({ + widget: widget, + title: action.name + }); + if (action.tag !== 'reload') { + self.do_push_state({}); + } }; + } else { + widget = new instance.web.ViewManagerAction(this, action); + klass = 'oe_act_window'; + post_process = widget.proxy('add_breadcrumb'); } if (action.target === 'new') { - if (this.dialog == null) { - this.dialog = new instance.web.Dialog(this, { width: '80%' }); + if (this.dialog === null) { + // These buttons will be overwrited by <footer> if any + this.dialog = new instance.web.Dialog(this, { + buttons: { "Close": function() { $(this).dialog("close"); }}, + dialogClass: klass + }); if(on_close) this.dialog.on_close.add(on_close); } else { - this.dialog_viewmanager.destroy(); + this.dialog_widget.destroy(); } this.dialog.dialog_title = action.name; - this.dialog_viewmanager = new instance.web.ViewManagerAction(this, action); - this.dialog_viewmanager.appendTo(this.dialog.$element); + this.dialog_widget = widget; + this.dialog_widget.appendTo(this.dialog.$el); this.dialog.open(); } else { this.dialog_stop(); - this.content_stop(); + this.inner_action = action; + this.inner_widget = widget; + post_process(); + this.inner_widget.appendTo(this.$el); + } + }, + ir_actions_act_window: function (action, on_close, clear_breadcrumbs) { + var self = this; + if (action.target !== 'new') { if(action.menu_id) { + this.dialog_stop(); return this.getParent().do_action(action, function () { instance.webclient.menu.open_menu(action.menu_id); - }); + }, clear_breadcrumbs); } - this.inner_action = action; - this.inner_viewmanager = new instance.web.ViewManagerAction(this, action); - this.inner_viewmanager.appendTo(this.$element); } + return this.ir_actions_common(action, on_close, clear_breadcrumbs); + }, + ir_actions_client: function (action, on_close, clear_breadcrumbs) { + return this.ir_actions_common(action, on_close, clear_breadcrumbs); }, ir_actions_act_window_close: function (action, on_closed) { if (!this.dialog && on_closed) { @@ -158,24 +327,18 @@ instance.web.ActionManager = instance.web.Widget.extend({ } this.dialog_stop(); }, - ir_actions_server: function (action, on_closed) { + ir_actions_server: function (action, on_closed, clear_breadcrumbs) { var self = this; this.rpc('/web/action/run', { action_id: action.id, context: action.context || {} }).then(function (action) { - self.do_action(action, on_closed) + self.do_action(action, on_closed, clear_breadcrumbs) }); }, - ir_actions_client: function (action) { - this.content_stop(); - this.dialog_stop(); - var ClientWidget = instance.web.client_actions.get_object(action.tag); - (this.client_widget = new ClientWidget(this, action.params)).appendTo(this.$element); - }, ir_actions_report_xml: function(action, on_closed) { var self = this; - $.blockUI(); + instance.web.blockUI(); self.rpc("/web/session/eval_domain_and_context", { contexts: [action.context], domains: [] @@ -185,7 +348,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ self.session.get_file({ url: '/web/report', data: {action: JSON.stringify(action)}, - complete: $.unblockUI, + complete: instance.web.unblockUI, success: function(){ if (!self.dialog && on_closed) { on_closed(); @@ -199,9 +362,6 @@ instance.web.ActionManager = instance.web.Widget.extend({ ir_actions_act_url: function (action) { window.open(action.url, action.target === 'self' ? '_self' : '_blank'); }, - ir_ui_menu: function (action) { - this.getParent().do_action(action); - } }); instance.web.ViewManager = instance.web.Widget.extend({ @@ -235,7 +395,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ start: function() { this._super(); var self = this; - this.$element.find('.oe_view_manager_switch a').click(function() { + this.$el.find('.oe_view_manager_switch a').click(function() { self.on_mode_switch($(this).data('view-type')); }).tipsy(); var views_ids = {}; @@ -244,9 +404,9 @@ instance.web.ViewManager = instance.web.Widget.extend({ deferred : $.Deferred(), controller : null, options : _.extend({ - $buttons : self.$element.find('.oe_view_manager_buttons'), - $sidebar : self.flags.sidebar ? self.$element.find('.oe_view_manager_sidebar') : undefined, - $pager : self.$element.find('.oe_view_manager_pager'), + $buttons : self.$el.find('.oe_view_manager_buttons'), + $sidebar : self.flags.sidebar ? self.$el.find('.oe_view_manager_sidebar') : undefined, + $pager : self.$el.find('.oe_view_manager_pager'), action : self.action, action_views_ids : views_ids }, self.flags, self.flags[view.view_type] || {}, view.options || {}) @@ -254,7 +414,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ views_ids[view.view_type] = view.view_id; }); if (this.flags.views_switcher === false) { - this.$element.find('.oe_view_manager_switch').hide(); + this.$el.find('.oe_view_manager_switch').hide(); } // If no default view defined, switch to the first one in sequence var default_view = this.flags.default_view || this.views_src[0].view_type; @@ -271,8 +431,10 @@ instance.web.ViewManager = instance.web.Widget.extend({ var self = this; var view = this.views[view_type]; var view_promise; - if(!view) + var form = this.views['form']; + if (!view || (form && form.controller && !form.controller.can_be_discarded())) { return $.Deferred().reject(); + } if (!no_store) { this.views_history.push(view_type); @@ -280,35 +442,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ this.active_view = view_type; if (!view.controller) { - // Lazy loading of views - var controllerclass = this.registry.get_object(view_type); - var options = _.clone(view.options); - if (view_type === "form" && this.action) { - switch (this.action.target) { - case 'new': - case 'inline': - options.initial_mode = 'edit'; - break; - } - } - var controller = new controllerclass(this, this.dataset, view.view_id, options); - if (view.embedded_view) { - controller.set_embedded_view(view.embedded_view); - } - controller.do_switch_view.add_last(_.bind(this.switch_view, this)); - controller.do_prev_view.add_last(this.on_prev_view); - var container = this.$element.find(".oe_view_manager_view_" + view_type); - view_promise = controller.appendTo(container); - this.views[view_type].controller = controller; - this.views[view_type].deferred.resolve(view_type); - $.when(view_promise).then(function() { - self.on_controller_inited(view_type, controller); - if (self.searchview - && self.flags.auto_search - && view.controller.searchable !== false) { - self.searchview.ready.then(self.searchview.do_search); - } - }); + view_promise = this.do_create_view(view_type); } else if (this.searchview && self.flags.auto_search && view.controller.searchable !== false) { @@ -319,28 +453,128 @@ instance.web.ViewManager = instance.web.Widget.extend({ this.searchview[(view.controller.searchable === false || this.searchview.hidden) ? 'hide' : 'show'](); } - this.$element - .find('.oe_view_manager_switch a').parent().removeClass('active') - this.$element + this.$el + .find('.oe_view_manager_switch a').parent().removeClass('active'); + this.$el .find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]') .parent().addClass('active'); - $.when(view_promise).then(function () { + return $.when(view_promise).then(function () { _.each(_.keys(self.views), function(view_name) { var controller = self.views[view_name].controller; if (controller) { + var container = self.$el.find(".oe_view_manager_view_" + view_name + ":first"); if (view_name === view_type) { + container.show(); controller.do_show(view_options || {}); } else { + container.hide(); controller.do_hide(); } + // put the <footer> in the dialog's buttonpane + if (self.$el.parent('.ui-dialog-content') && self.$el.find('footer')) { + self.$el.parent('.ui-dialog-content').parent().find('div.ui-dialog-buttonset').hide() + self.$el.find('footer').appendTo( + self.$el.parent('.ui-dialog-content').parent().find('div.ui-dialog-buttonpane') + ); + } } }); + }); + }, + do_create_view: function(view_type) { + // Lazy loading of views + var self = this; + var view = this.views[view_type]; + var viewclass = this.registry.get_object(view_type); + var options = _.clone(view.options); + if (view_type === "form" && this.action && (this.action.target == 'new' || this.action.target == 'inline')) { + options.initial_mode = 'edit'; + } + var controller = new viewclass(this, this.dataset, view.view_id, options); + + controller.on('history_back', this, function() { + var am = self.getParent(); + if (am && am.trigger) { + return am.trigger('history_back'); + } + }); - self.$element.find('.oe_view_title_text:first').text( - self.display_title()); + controller.on("change:title", this, function() { + if (self.active_view === view_type) { + self.set_title(controller.get('title')); + } + }); + + if (view.embedded_view) { + controller.set_embedded_view(view.embedded_view); + } + controller.do_switch_view.add_last(_.bind(this.switch_view, this)); + + controller.do_prev_view.add_last(this.on_prev_view); + var container = this.$el.find(".oe_view_manager_view_" + view_type); + var view_promise = controller.appendTo(container); + this.views[view_type].controller = controller; + this.views[view_type].deferred.resolve(view_type); + return $.when(view_promise).then(function() { + self.on_controller_inited(view_type, controller); + if (self.searchview + && self.flags.auto_search + && view.controller.searchable !== false) { + self.searchview.ready.then(self.searchview.do_search); + } + }); + }, + set_title: function(title) { + this.$el.find('.oe_view_title_text:first').text(title); + }, + add_breadcrumb: function() { + var self = this; + var views = [this.active_view || this.views_src[0].view_type]; + this.on_mode_switch.add(function(mode) { + var last = views.slice(-1)[0]; + if (mode !== last) { + if (mode !== 'form') { + views.length = 0; + } + views.push(mode); + } + }); + this.getParent().push_breadcrumb({ + widget: this, + action: this.action, + show: function(index) { + var view_to_select = views[index]; + self.$el.show(); + if (self.active_view !== view_to_select) { + self.on_mode_switch(view_to_select); + } + }, + get_title: function() { + var id; + var currentIndex; + _.each(self.getParent().breadcrumbs, function(bc, i) { + if (bc.widget === self) { + currentIndex = i; + } + }); + var next = self.getParent().breadcrumbs.slice(currentIndex + 1)[0]; + var titles = _.map(views, function(v) { + var controller = self.views[v].controller; + if (v === 'form') { + id = controller.datarecord.id; + } + return controller.get('title'); + }); + if (next && next.action && next.action.res_id && self.dataset && + self.active_view === 'form' && self.dataset.model === next.action.res_model && id === next.action.res_id) { + // If the current active view is a formview and the next item in the breadcrumbs + // is an action on same object (model / res_id), then we omit the current formview's title + titles.pop(); + } + return titles; + } }); - return view_promise; }, /** * Method used internally when a view asks to switch view. This method is meant @@ -388,7 +622,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ this.searchview = new instance.web.SearchView(this, this.dataset, view_id, search_defaults, this.flags.search_view === false); this.searchview.on_search.add(this.do_searchview_search); - return this.searchview.appendTo(this.$element.find(".oe_view_manager_view_search")); + return this.searchview.appendTo(this.$el.find(".oe_view_manager_view_search")); }, do_searchview_search: function(domains, contexts, groupbys) { var self = this, @@ -434,14 +668,6 @@ instance.web.ViewManager = instance.web.Widget.extend({ */ on_action_executed: function () { }, - display_title: function () { - var view = this.views[this.active_view]; - if (view) { - // ick - return view.controller.fields_view.arch.attrs.string; - } - return ''; - } }); instance.web.ViewManagerAction = instance.web.ViewManager.extend({ @@ -486,7 +712,7 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ if (this.session.hidden_menutips) { return; } - this.session.hidden_menutips = {} + this.session.hidden_menutips = {}; }, /** * Initializes the ViewManagerAction: sets up the searchview (if the @@ -513,33 +739,8 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ var manager_ready = $.when(searchview_loaded, main_view_loaded); - this.$element.find('.oe_debug_view').change(this.on_debug_changed); - - if (this.action.help && !this.flags.low_profile) { - var Users = new instance.web.DataSet(self, 'res.users'), - $tips = this.$element.find('.oe_view_manager_menu_tips'); - $tips.delegate('blockquote button', 'click', function() { - var $this = $(this); - //noinspection FallthroughInSwitchStatementJS - switch ($this.attr('name')) { - case 'disable': - Users.write(self.session.uid, {menu_tips:false}); - case 'hide': - $this.closest('blockquote').hide(); - self.session.hidden_menutips[self.action.id] = true; - } - }); - if (!(self.action.id in self.session.hidden_menutips)) { - Users.read_ids([this.session.uid], ['menu_tips']).then(function(users) { - var user = users[0]; - if (!(user && user.id === self.session.uid)) { - return; - } - $tips.find('blockquote').toggle(user.menu_tips); - }); - } - } - + this.$el.find('.oe_debug_view').change(this.on_debug_changed); + this.$el.addClass("oe_view_manager_" + (this.action.target || 'current')); return manager_ready; }, on_debug_changed: function (evt) { @@ -551,7 +752,15 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ switch (val) { case 'fvg': var dialog = new instance.web.Dialog(this, { title: _t("Fields View Get"), width: '95%' }).open(); - $('<pre>').text(instance.web.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$element); + $('<pre>').text(instance.web.json_node_to_xml(current_view.fields_view.arch, true)).appendTo(dialog.$el); + break; + case 'tests': + this.do_action({ + name: "JS Tests", + target: 'new', + type : 'ir.actions.act_url', + url: '/web/static/test/test.html' + }) break; case 'perm_read': var ids = current_view.get_selected_ids(); @@ -570,6 +779,17 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ case 'toggle_layout_outline': current_view.rendering_engine.toggle_layout_debugging(); break; + case 'translate': + this.do_action({ + name: "Technical Translation", + res_model : 'ir.translation', + domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']], + views: [[false, 'list'], [false, 'form']], + type : 'ir.actions.act_window', + view_type : "list", + view_mode : "list" + }); + break; case 'fields': this.dataset.call_and_eval( 'fields_get', [false, {}], null, 1).then(function (fields) { @@ -593,15 +813,6 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ width: '95%'}, $root).open(); }); break; - case 'manage_views': - if (current_view.fields_view && current_view.fields_view.arch) { - var view_editor = new instance.web.ViewEditor(current_view, current_view.$element, this.dataset, current_view.fields_view.arch); - view_editor.start(); - } else { - this.do_warn(_t("Manage Views"), - _t("Could not find current view declaration")); - } - break; case 'edit_workflow': return this.do_action({ res_model : 'workflow', @@ -626,6 +837,21 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ } }); break; + case 'print_workflow': + if (current_view.get_selected_ids && current_view.get_selected_ids().length == 1) { + instance.web.blockUI(); + var action = { + context: { active_ids: current_view.get_selected_ids() }, + report_name: "workflow.instance.graph", + datas: { + model: this.dataset.model, + id: current_view.get_selected_ids()[0], + nested: true, + } + }; + this.session.get_file({ url: '/web/report', data: {action: JSON.stringify(action)}, complete: instance.web.unblockUI }); + } + break; default: if (val) { console.log("No debug handler for ", val); @@ -658,16 +884,22 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ var controller = self.views[self.active_view].controller, fvg = controller.fields_view, view_id = (fvg && fvg.view_id) || '--'; - self.$element.find('.oe_debug_view').html(QWeb.render('ViewManagerDebug', { + self.$el.find('.oe_debug_view').html(QWeb.render('ViewManagerDebug', { view: controller, view_manager: self })); - if (!self.action.name && fvg) { - self.$element.find('.oe_view_title_text').text(fvg.arch.attrs.string || fvg.name); - } - + self.set_title(); }); }, + do_create_view: function(view_type) { + var r = this._super.apply(this, arguments); + var view = this.views[view_type].controller; + view.set({ 'title': this.action.name }); + return r; + }, + set_title: function(title) { + this.$el.find('.oe_breadcrumb_title:first').html(this.getParent().get_title()); + }, do_push_state: function(state) { if (this.getParent() && this.getParent().do_push_state) { state["view_type"] = this.active_view; @@ -689,42 +921,39 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ self.views[self.active_view].controller.do_load_state(state, warm); }); }, - display_title: function () { - return this.action.name; - } }); instance.web.Sidebar = instance.web.Widget.extend({ init: function(parent) { + var self = this; this._super(parent); var view = this.getParent(); this.sections = [ { 'name' : 'print', 'label' : _t('Print'), }, - { 'name' : 'files', 'label' : _t('Attachment'), }, { 'name' : 'other', 'label' : _t('More'), } ]; this.items = { 'print' : [], - 'files' : [], 'other' : [] }; - if (this.session.uid === 1) { - var item = { label: _t("Translate"), callback: view.on_sidebar_translate, title: _t("Technical translation") }; - this.items.other.push(item); - } + this.fileupload_id = _.uniqueId('oe_fileupload'); + $(window).on(this.fileupload_id, function() { + var args = [].slice.call(arguments).slice(1); + if (args[0] && args[0].error) { + alert(args[0].error); + } else { + self.do_attachement_update(self.dataset, self.model_id); + } + instance.web.unblockUI(); + }); }, start: function() { var self = this; this._super(this); this.redraw(); - this.$element.on('click','.oe_dropdown_toggle',function(event) { - $(this).parent().find('ul').toggle(); - return false; - }); - this.$element.on('click','.oe_dropdown_menu li a', function(event) { + this.$el.on('click','.oe_dropdown_menu li a', function(event) { var section = $(this).data('section'); var index = $(this).data('index'); - $(this).closest('ul').hide(); var item = self.items[section][index]; if (item.callback) { item.callback.apply(self, [item]); @@ -733,16 +962,12 @@ instance.web.Sidebar = instance.web.Widget.extend({ } else if (item.url) { return true; } - return false; + event.preventDefault(); }); - //this.$div.html(QWeb.render('FormView.sidebar.attachments', this)); - //this.$element.find('.oe-binary-file').change(this.on_attachment_changed); - //this.$element.find('.oe-sidebar-attachment-delete').click(this.on_attachment_delete); }, redraw: function() { var self = this; - self.$element.html(QWeb.render('Sidebar', {widget: self})); - this.$element.find('ul').hide(); + self.$el.html(QWeb.render('Sidebar', {widget: self})); // Hides Sidebar sections when item list is empty this.$('.oe_form_dropdown_section').each(function() { @@ -826,6 +1051,8 @@ instance.web.Sidebar = instance.web.Widget.extend({ }); }, do_attachement_update: function(dataset, model_id) { + this.dataset = dataset; + this.model_id = model_id; if (!model_id) { this.on_attachments_loaded([]); } else { @@ -837,165 +1064,43 @@ instance.web.Sidebar = instance.web.Widget.extend({ on_attachments_loaded: function(attachments) { var self = this; var items = []; - // TODO: preprend: _s + - var prefix = '/web/binary/saveas?session_id=' + self.session.session_id + '&model=ir.attachment&field=datas&filename_field=name&id='; + var prefix = this.session.origin + '/web/binary/saveas?session_id=' + self.session.session_id + '&model=ir.attachment&field=datas&filename_field=name&id='; _.each(attachments,function(a) { a.label = a.name; if(a.type === "binary") { a.url = prefix + a.id + '&t=' + (new Date().getTime()); } }); - attachments.push( { label: _t("Add..."), callback: self.on_attachment_add } ); self.items['files'] = attachments; self.redraw(); - }, - on_attachment_add: function(e) { - this.$element.find('.oe_sidebar_add').show(); + this.$('.oe_sidebar_add_attachment .oe_form_binary_file').change(this.on_attachment_changed); + this.$el.find('.oe_sidebar_delete_item').click(this.on_attachment_delete); }, on_attachment_changed: function(e) { - return; - window[this.element_id + '_iframe'] = this.do_update; var $e = $(e.target); - if ($e.val() != '') { - this.$element.find('form.oe-binary-form').submit(); + if ($e.val() !== '') { + this.$el.find('form.oe_form_binary_form').submit(); $e.parent().find('input[type=file]').prop('disabled', true); $e.parent().find('button').prop('disabled', true).find('img, span').toggle(); + this.$('.oe_sidebar_add_attachment span').text(_t('Uploading...')); + instance.web.blockUI(); } }, on_attachment_delete: function(e) { - return; - var self = this, $e = $(e.currentTarget); - var name = _.str.trim($e.parent().find('a.oe-sidebar-attachments-link').text()); - if (confirm(_.str.sprintf(_t("Do you really want to delete the attachment %s?"), name))) { - this.rpc('/web/dataset/unlink', { - model: 'ir.attachment', - ids: [parseInt($e.attr('data-id'))] - }, function(r) { - $e.parent().remove(); - self.do_notify("Delete an attachment", "The attachment '" + name + "' has been deleted"); - }); - } - } -}); - -instance.web.TranslateDialog = instance.web.Dialog.extend({ - dialog_title: {toString: function () { return _t("Translations"); }}, - init: function(view) { - // TODO fme: should add the language to fields_view_get because between the fields view get - // and the moment the user opens the translation dialog, the user language could have been changed - this.view_language = view.session.user_context.lang; - this['on_button' + _t("Save")] = this.on_button_Save; - this['on_button' + _t("Close")] = this.on_button_Close; - this._super(view, { - width: '80%', - height: '80%' - }); - this.view = view; - this.view_type = view.fields_view.type || ''; - this.$fields_form = null; - this.$view_form = null; - this.$sidebar_form = null; - this.translatable_fields_keys = _.map(this.view.translatable_fields || [], function(i) { return i.name }); - this.languages = null; - this.languages_loaded = $.Deferred(); - (new instance.web.DataSetSearch(this, 'res.lang', this.view.dataset.get_context(), - [['translatable', '=', '1']])).read_slice(['code', 'name'], { sort: 'id' }).then(this.on_languages_loaded); - }, - start: function() { var self = this; - this._super(); - $.when(this.languages_loaded).then(function() { - self.$element.html(instance.web.qweb.render('TranslateDialog', { widget: self })); - self.$fields_form = self.$element.find('.oe_translation_form'); - self.$fields_form.find('.oe_trad_field').change(function() { - $(this).toggleClass('touched', ($(this).val() != $(this).attr('data-value'))); - }); - }); - return this; - }, - on_languages_loaded: function(langs) { - this.languages = langs; - this.languages_loaded.resolve(); - }, - do_load_fields_values: function(callback) { - var self = this, - deffered = []; - this.$fields_form.find('.oe_trad_field').val('').removeClass('touched'); - _.each(self.languages, function(lg) { - var deff = $.Deferred(); - deffered.push(deff); - var callback = function(values) { - _.each(self.translatable_fields_keys, function(f) { - self.$fields_form.find('.oe_trad_field[name="' + lg.code + '-' + f + '"]').val(values[0][f] || '').attr('data-value', values[0][f] || ''); - }); - deff.resolve(); - }; - if (lg.code === self.view_language) { - var values = {}; - _.each(self.translatable_fields_keys, function(field) { - values[field] = self.view.fields[field].get_value(); - }); - callback([values]); - } else { - self.rpc('/web/dataset/get', { - model: self.view.dataset.model, - ids: [self.view.datarecord.id], - fields: self.translatable_fields_keys, - context: self.view.dataset.get_context({ - 'lang': lg.code - })}, callback); - } - }); - $.when.apply(null, deffered).then(callback); - }, - open: function(field) { - var self = this, - sup = this._super; - $.when(this.languages_loaded).then(function() { - if (self.view.translatable_fields && self.view.translatable_fields.length) { - self.do_load_fields_values(function() { - sup.call(self); - if (field) { - var $field_input = self.$element.find('tr[data-field="' + field.name + '"] td:nth-child(2) *:first-child'); - self.$element.scrollTo($field_input); - $field_input.focus(); - } - }); - } else { - sup.call(self); - } - }); - }, - on_button_Save: function() { - var trads = {}, - self = this, - trads_mutex = new $.Mutex(); - self.$fields_form.find('.oe_trad_field.touched').each(function() { - var field = $(this).attr('name').split('-'); - if (!trads[field[0]]) { - trads[field[0]] = {}; - } - trads[field[0]][field[1]] = $(this).val(); - }); - _.each(trads, function(data, code) { - if (code === self.view_language) { - _.each(data, function(value, field) { - self.view.fields[field].set_value(value); - }); - } - trads_mutex.exec(function() { - return self.view.dataset.write(self.view.datarecord.id, data, { context : { 'lang': code } }); + e.preventDefault(); + e.stopPropagation(); + var self = this; + var $e = $(e.currentTarget); + if (confirm(_t("Do you really want to delete this attachment ?"))) { + (new instance.web.DataSet(this, 'ir.attachment')).unlink([parseInt($e.attr('data-id'), 10)]).then(function() { + self.do_attachement_update(self.dataset, self.model_id); }); - }); - this.close(); - }, - on_button_Close: function() { - this.close(); + } } }); instance.web.View = instance.web.Widget.extend({ - template: "EmptyComponent", // name displayed in view switchers display_name: '', /** @@ -1008,7 +1113,7 @@ instance.web.View = instance.web.Widget.extend({ this.view_id = view_id; this.set_default_options(options); }, - start: function() { + start: function () { return this.load_view(); }, load_view: function() { @@ -1046,12 +1151,6 @@ instance.web.View = instance.web.Widget.extend({ action_views_ids: {} }); }, - open_translate_dialog: function(field) { - if (!this.translate_dialog) { - this.translate_dialog = new instance.web.TranslateDialog(this).start(); - } - this.translate_dialog.open(field); - }, /** * Fetches and executes the action identified by ``action_data``. * @@ -1116,11 +1215,11 @@ instance.web.View = instance.web.Widget.extend({ } } args.push(context); - return dataset.call_button(action_data.name, args, handler); + return dataset.call_button(action_data.name, args).then(handler); } else if (action_data.type=="action") { - return this.rpc('/web/action/load', { action_id: parseInt(action_data.name, 10), context: context, do_not_eval: true}, handler); + return this.rpc('/web/action/load', { action_id: action_data.name, context: context, do_not_eval: true}, handler); } else { - return dataset.exec_workflow(record_id, action_data.name, handler); + return dataset.exec_workflow(record_id, action_data.name).then(handler); } }, /** @@ -1134,10 +1233,10 @@ instance.web.View = instance.web.Widget.extend({ this.embedded_view = embedded_view; }, do_show: function () { - this.$element.show(); + this.$el.show(); }, do_hide: function () { - this.$element.hide(); + this.$el.hide(); }, do_push_state: function(state) { if (this.getParent() && this.getParent().do_push_state) { @@ -1164,23 +1263,8 @@ instance.web.View = instance.web.Widget.extend({ }, do_search: function(view) { }, - on_sidebar_import: function() { - var import_view = new instance.web.DataImport(this, this.dataset); - import_view.start(); - }, on_sidebar_export: function() { - var export_view = new instance.web.DataExport(this, this.dataset); - export_view.start(); - }, - on_sidebar_translate: function() { - return this.do_action({ - res_model : 'ir.translation', - domain : [['type', '!=', 'object'], '|', ['name', '=', this.dataset.model], ['name', 'ilike', this.dataset.model + ',']], - views: [[false, 'list'], [false, 'form']], - type : 'ir.actions.act_window', - view_type : "list", - view_mode : "list" - }); + new instance.web.DataExport(this, this.dataset).open(); }, sidebar_context: function () { return $.when(); @@ -1191,6 +1275,15 @@ instance.web.View = instance.web.Widget.extend({ */ reload: function () { return $.when(); + }, + /** + * Return whether the user can perform the action ('create', 'edit', 'delete') in this view. + * An action is disabled by setting the corresponding attribute in the view's main element, + * like: <form string="" create="false" edit="false" delete="false"> + */ + is_action_enabled: function(action) { + var attrs = this.fields_view.arch.attrs; + return (action in attrs) ? JSON.parse(attrs[action]) : true; } }); @@ -1218,7 +1311,6 @@ instance.web.xml_to_json = function(node) { } instance.web.json_node_to_xml = function(node, human_readable, indent) { // For debugging purpose, this function will convert a json node back to xml - // Maybe useful for xml view editor indent = indent || 0; var sindent = (human_readable ? (new Array(indent + 1).join('\t')) : ''), r = sindent + '<' + node.tag, @@ -1227,7 +1319,9 @@ instance.web.json_node_to_xml = function(node, human_readable, indent) { if (typeof(node) === 'string') { return sindent + node; } else if (typeof(node.tag) !== 'string' || !node.children instanceof Array || !node.attrs instanceof Object) { - throw("Node a json node"); + throw new Error( + _.str.sprintf("Node [%s] is not a JSONified XML node", + JSON.stringify(node))); } for (var attr in node.attrs) { var vattr = node.attrs[attr]; |
