diff options
| author | Xavier Morel <xmo@openerp.com> | 2012-11-07 12:25:16 +0100 |
|---|---|---|
| committer | Xavier Morel <xmo@openerp.com> | 2012-11-07 12:25:16 +0100 |
| commit | 36dad4cef994449755d7d01560cd84a4f92b629e (patch) | |
| tree | 4b8aa0e2c8d0ca788856d8f542516b7619a18452 /addons/web/static/src/js | |
| parent | 64dc8880532ab198147a03fe6c78b28bd1af794e (diff) | |
| parent | 1c5e8da2765e51b2c8bb49c6d1bf3c6b55c95dbf (diff) | |
[MERGE] from trunk
bzr revid: xmo@openerp.com-20121107112516-64hqps4jjgmrs3a4
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 | 920 | ||||
| -rw-r--r-- | addons/web/static/src/js/corelib.js | 354 | ||||
| -rw-r--r-- | addons/web/static/src/js/coresetup.js | 409 | ||||
| -rw-r--r-- | addons/web/static/src/js/data.js | 462 | ||||
| -rw-r--r-- | addons/web/static/src/js/data_export.js | 144 | ||||
| -rw-r--r-- | addons/web/static/src/js/data_import.js | 366 | ||||
| -rw-r--r-- | addons/web/static/src/js/formats.js | 90 | ||||
| -rw-r--r-- | addons/web/static/src/js/search.js | 500 | ||||
| -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 | 3577 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list.js | 816 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list_editable.js | 979 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_tree.js | 126 | ||||
| -rw-r--r-- | addons/web/static/src/js/views.js | 1016 |
16 files changed, 5724 insertions, 5269 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 47983ed322d..08ef8c4f9bf 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,22 +57,22 @@ 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(); }, + beforeClose: function () { + self.trigger("closing"); + }, resizeStop: this.on_resized }; for (var f in this) { @@ -82,13 +81,12 @@ 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()); - } + this.on("closing", this, this._closing); }, get_options: function(options) { var self = this, @@ -120,38 +118,72 @@ 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'); + this.$buttons.appendTo($("body")); + instance.web.dialog(this.$el, o).dialog('open'); + this.$el.dialog("widget").find(".ui-dialog-buttonpane").remove(); + 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); + } else { + this.$buttons = this.$el.dialog("widget").find(".ui-dialog-buttonpane"); + } + this.dialog_inited = true; + var res = this.start(); + return res; + }, close: function() { - this.$element.dialog('close'); + this.$el.dialog('close'); }, - on_close: function() { + _closing: 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(); } }); instance.web.CrashManager = instance.web.CallbackEnabled.extend({ - on_rpc_error: function(error) { + rpc_error: function(error) { if (error.data.fault_code) { var split = ("" + error.data.fault_code).split('\n')[0].split(' -- '); if (split.length > 1) { @@ -160,12 +192,12 @@ instance.web.CrashManager = instance.web.CallbackEnabled.extend({ } } if (error.code === 200 && error.type) { - this.on_managed_error(error); + this.show_warning(error); } else { - this.on_traceback(error); + this.show_error(error); } }, - on_managed_error: function(error) { + show_warning: function(error) { instance.web.dialog($('<div>' + QWeb.render('CrashManager.warning', {error: error}) + '</div>'), { title: "OpenERP " + _.str.capitalize(error.type), buttons: [ @@ -173,32 +205,12 @@ instance.web.CrashManager = instance.web.CallbackEnabled.extend({ ] }); }, - on_traceback: function(error) { + show_error: 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 +219,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})); + }, + show_message: function(exception) { + this.show_error({ + type: _t("Client Error"), + message: exception, + data: {debug: ""} + }); + }, }); instance.web.Loading = instance.web.Widget.extend({ @@ -217,47 +236,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("response_failed", 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 +286,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 +309,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 +340,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 +363,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 @@ -363,12 +394,22 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ do_create: function(form) { var self = this; var fields = $(form).serializeArray(); - self.rpc("/web/database/create", {'fields': fields}, function(result) { + self.rpc("/web/database/create", {'fields': fields}).then(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; @@ -379,7 +420,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ if (!db || !confirm("Do you really want to delete the database: " + db + " ?")) { return; } - self.rpc("/web/database/drop", {'fields': fields}, function(result) { + self.rpc("/web/database/drop", {'fields': fields}).then(function(result) { if (result.error) { self.display_error(result); return; @@ -396,7 +437,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(); } @@ -435,26 +483,38 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ var self = this; self.rpc("/web/database/change_password", { 'fields': $(form).serializeArray() - }, function(result) { + }).then(function(result) { if (result.error) { 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, - init: function(parent) { + + init: function(parent, action) { this._super(parent); this.has_local_storage = typeof(localStorage) != 'undefined'; + this.db_list = null; this.selected_db = null; this.selected_login = null; + this.params = action.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'); @@ -466,48 +526,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(); - }); + self.$el.find("form").submit(self.on_submit); + self.$el.find('.oe_login_manage_db').click(function() { + self.do_action("database_manager"); }); - self.load_db_list(); - }, - load_db_list: function () { - var self = this; - self.rpc("/web/database/get_list", {}, function(result) { - self.set_db_list(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; + }, + 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 { + this.$('div.oe_login_dbpane').show(); + } }, - set_db_list: function (list) { - this.$element.find("[name=db]").replaceWith(instance.web.qweb.render('Login.dblist', { db_list: list, selected_db: this.selected_db})); + 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); }, @@ -520,15 +579,9 @@ instance.web.Login = instance.web.Widget.extend({ */ do_login: function (db, login, password) { var self = this; - this.$element.removeClass('oe_login_invalid'); - this.session.on_session_invalid.add({ - callback: function () { - self.$element.addClass("oe_login_invalid"); - }, - unique: true - }); - this.session.session_authenticate(db, login, password).then(function() { - self.$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) { localStorage.setItem('last_db_login_success', db); @@ -542,9 +595,97 @@ instance.web.Login = instance.web.Widget.extend({ localStorage.setItem('last_password_login_success', ''); } } + 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 = function(parent, action) { + var params = action.params || {}; + var menu_id = params.menu_id || false; + 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 (menu_id) { + hash = "#menu_id=" + menu_id; + } + var url = l.protocol + "//" + l.host + l.pathname + search + hash; + window.onerror = function() {}; + 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 = function(parent) { + 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) { + 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() + }).then(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', @@ -556,29 +697,34 @@ 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() { - return this.rpc("/web/menu/load", {}).then(this.on_loaded); + var self = this; + return this.rpc("/web/menu/load", {}).then(function(r) { + self.menu_loaded(r); + }); }, - on_loaded: function(data) { + menu_loaded: function(data) { var self = this; 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) { self.open_menu(self.current_menu); } + this.trigger('menu_loaded', data); this.has_been_loaded.resolve(); }, limit_entries: function() { @@ -588,20 +734,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 @@ -609,20 +752,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 @@ -649,7 +794,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); @@ -659,34 +804,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 @@ -694,9 +842,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); }, }); @@ -709,118 +857,46 @@ 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); }); }; this.update_promise = this.update_promise.pipe(fct, fct); }, - on_action: function() { - }, on_menu_logout: function() { + this.trigger('user_logout'); }, 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.res_id = instance.session.uid; + self.getParent().action_manager.do_action(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; @@ -828,105 +904,160 @@ instance.web.UserMenu = instance.web.Widget.extend({ var $help = $(QWeb.render("UserMenu.about", {version_info: res})); $help.find('a.oe_activate_debug_mode').click(function (e) { e.preventDefault(); - window.location = $.param.querystring( - window.location.href, 'debug'); + window.location = $.param.querystring( 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('error', this.crashmanager, this.crashmanager.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) { - this.$element.addClass("kitten-mode-activated"); - this.$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() { + 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.session.on_session_valid.add(function() { - self.show_application(); - - if(self.action_manager) - self.action_manager.destroy(); - self.action_manager = new instance.web.ActionManager(self); - self.action_manager.appendTo(self.$element.find('.oe_application')); - 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; - } - }); - this.$element.on('mouseenter', '.oe_systray > div:not([data-tipsy=true])', function() { - $(this).attr('data-tipsy', 'true').tipsy().trigger('mouseenter'); - }); }, - show_login: function() { + set_title: function(title) { + title = _.str.clean(title); + var sep = _.isEmpty(title) ? '' : ' - '; + document.title = title + sep + 'OpenERP'; + }, + show_common: function() { var self = this; - this.destroy_content(); - this.show_common(); - self.login = new instance.web.Login(self); - self.login.appendTo(self.$element); + this._super(); + window.onerror = function (message, file, line) { + self.crashmanager.show_error({ + type: _t("Client Error"), + message: message, + data: {debug: file + ':' + line} + }); + }; + }, + show_login: function() { + this.toggle_bars(false); + + var state = $.bbq.getState(true); + var action = { + type: 'ir.actions.client', + tag: 'login', + _push_me: false, + }; + + this.action_manager.do_action(action); + this.action_manager.inner_widget.on('login_successful', this, function() { + this.show_application(); // will load the state we just pushed + }); }, show_application: function() { var self = this; - this.destroy_content(); - this.show_common(); - self.$table = $(QWeb.render("WebClient", {})); - self.$element.append(self.$table); + 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.on_menu_logout.add(this.proxy('on_logout')); - self.user_menu.on_action.add(this.proxy('on_menu_action')); + self.user_menu.replace(this.$el.find('.oe_user_menu_placeholder')); + self.user_menu.on('user_logout', self, self.on_logout); self.user_menu.do_update(); - }, - show_common: function() { - var self = this; - if (!this.crashmanager) { - this.crashmanager = new instance.web.CrashManager(); - instance.connection.on_rpc_error.add(this.crashmanager.on_rpc_error); - window.onerror = function (message, file, line) { - self.crashmanager.on_traceback({ - type: _t("Client Error"), - message: message, - data: {debug: file + ':' + line} - }); - }; - } - this.notification = new instance.web.Notification(this); - this.notification.appendTo(this.$element); - this.loading = new instance.web.Loading(this); - this.loading.appendTo(this.$element); + self.bind_hashchange(); + 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')); }); }, @@ -940,28 +1071,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) { @@ -975,61 +1106,85 @@ instance.web.WebClient = instance.web.Widget.extend({ }); }); } else { + state._push_me = false; // no need to push state back... this.action_manager.do_load_state(state, !!this._current_state); } } 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); - }, - do_action: function(action) { + on_menu_action: function(options) { var self = this; - // TODO replace by client action menuclick - if(action.menu_id) { - this.do_reload().then(function () { - self.menu.menu_click(action.menu_id); + return this.rpc("/web/action/load", { action_id: options.action_id }) + .pipe(function (result) { + var action = result; + if (options.needaction) { + action.context.search_default_message_unread = true; + } + return $.when(self.action_manager.do_action(action, { + clear_breadcrumbs: true, + action_menu_id: self.menu.current_menu, + })).fail(function() { + self.menu.open_menu(options.previous_menu_id); + }); }); - } }, 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 }).then(function(result) { + var action = 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) { @@ -1043,13 +1198,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 93796ab4282..6ecbfdd01ac 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() { @@ -122,6 +121,10 @@ openerp.web.corelib = function(instance) { // The dummy class constructor function Class() { + if(this.constructor !== instance.web.Class){ + throw new Error("You can only instanciate objects with the 'new' operator"); + return null; + } // All construction is actually done in the init method if (!initializing && this.init) { var ret = this.init.apply(this, arguments); @@ -288,6 +291,17 @@ var Events = instance.web.Class.extend({ return this; }, + callbackList: function() { + var lst = []; + _.each(this._callbacks || {}, function(el, eventName) { + var node = el; + while ((node = node.next) && node.next) { + lst.push([eventName, node.callback, node.context]); + } + }); + return lst; + }, + trigger : function(events) { var event, node, calls, tail, args, all, rest; if (!(calls = this._callbacks)) @@ -331,6 +345,9 @@ instance.web.EventDispatcherMixin = _.extend({}, instance.web.ParentedMixin, { }, on: function(events, dest, func) { var self = this; + if (!(func instanceof Function)) { + throw new Error("Event handler must be a function."); + } events = events.split(/\s+/); _.each(events, function(eventName) { self.__edispatcherEvents.on(eventName, func, dest); @@ -363,9 +380,9 @@ instance.web.EventDispatcherMixin = _.extend({}, instance.web.ParentedMixin, { event.source.__edispatcherEvents.off(event.name, event.func, self); }); this.__edispatcherRegisteredEvents = []; - if(!this.__edispatcherEvents) { - debugger; - } + _.each(this.__edispatcherEvents.callbackList(), function(cal) { + this.off(cal[0], cal[2], cal[1]); + }, this); this.__edispatcherEvents.off(); instance.web.ParentedMixin.destroy.call(this); } @@ -376,7 +393,17 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, { instance.web.EventDispatcherMixin.init.call(this); this.__getterSetterInternalMap = {}; }, - set: function(map) { + set: function(arg1, arg2, arg3) { + var map; + var options; + if (typeof arg1 === "string") { + map = {}; + map[arg1] = arg2; + options = arg3 || {}; + } else { + map = arg1; + options = arg2 || {}; + } var self = this; var changed = false; _.each(map, function(val, key) { @@ -385,10 +412,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); @@ -402,71 +430,16 @@ instance.web.CallbackEnabledMixin = _.extend({}, instance.web.PropertiesMixin, { init: function() { instance.web.PropertiesMixin.init.call(this); var self = this; - var callback_maker = function(obj, name, method) { - var callback = function() { - var args = Array.prototype.slice.call(arguments); - self.trigger.apply(self, [name].concat(args)); - var r; - for(var i = 0; i < callback.callback_chain.length; i++) { - var c = callback.callback_chain[i]; - if(c.unique) { - callback.callback_chain.splice(i, 1); - i -= 1; - } - var result = c.callback.apply(c.self, c.args.concat(args)); - if (c.callback === method) { - // return the result of the original method - r = result; - } - // TODO special value to stop the chain - // instance.web.callback_stop - } - return r; - }; - callback.callback_chain = []; - callback.add = function(f) { - if(typeof(f) == 'function') { - f = { callback: f, args: Array.prototype.slice.call(arguments, 1) }; - } - f.self = f.self || null; - f.args = f.args || []; - f.unique = !!f.unique; - if(f.position == 'last') { - callback.callback_chain.push(f); - } else { - callback.callback_chain.unshift(f); - } - return callback; - }; - callback.add_first = function(f) { - return callback.add.apply(null,arguments); - }; - callback.add_last = function(f) { - return callback.add({ - callback: f, - args: Array.prototype.slice.call(arguments, 1), - position: "last" - }); - }; - callback.remove = function(f) { - callback.callback_chain = _.difference(callback.callback_chain, _.filter(callback.callback_chain, function(el) { - return el.callback === f; - })); - return callback; - }; - - return callback.add({ - callback: method, - self:obj, - args:Array.prototype.slice.call(arguments, 3) - }); - }; // Transform on_/do_* methods into callbacks + var callback_maker = function(fn) { + return function() { + return fn.apply(self, arguments); + } + }; for (var name in this) { if(typeof(this[name]) == "function") { - this[name].debug_name = name; if((/^on_|^do_/).test(name)) { - this[name] = callback_maker(this, name, this[name]); + this[name] = callback_maker(this[name]); } } } @@ -487,24 +460,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 @@ -513,14 +482,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); }, /** @@ -530,8 +494,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); }, @@ -543,7 +507,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); }, /** @@ -554,7 +518,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); }, /** @@ -565,7 +529,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); }, /** @@ -576,7 +540,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); }, /** @@ -586,7 +550,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) { @@ -609,6 +573,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { * @returns {jQuery.Deferred} */ start: function() { + return $.when(); } }); @@ -644,8 +609,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() @@ -669,6 +634,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. @@ -685,14 +656,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, @@ -700,29 +669,130 @@ 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); + }, + + /** + * 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 + */ + replaceElement: function ($el) { + var $oldel = this.$el; + this.setElement($el); + if ($oldel && !$oldel.is(this.$el)) { + $oldel.replaceWith(this.$el); } + return this; }, /** - * Shortcut for $element.find() like backbone + * 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 */ - "$": function() { - return this.$element.find.apply(this.$element,arguments); + 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 * the action manager can be found amongst the ancestors of the current widget. * If that's not the case this method will simply return `false`. */ - do_action: function(action, on_finished) { - if (this.getParent()) { - return this.getParent().do_action(action, on_finished); + do_action: function() { + var parent = this.getParent(); + if (parent) { + return parent.do_action.apply(parent, arguments); } return false; }, @@ -741,7 +811,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() { @@ -763,7 +833,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 @@ -872,6 +942,12 @@ instance.web.Registry = instance.web.Class.extend({ }); instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ + triggers: { + 'request': 'Request sent', + 'response': 'Response received', + 'response_failed': 'HTTP Error response or timeout received', + 'error': 'The received response is an JSON-RPC error', + }, /** * @constructs instance.web.JsonRPC * @extends instance.web.CallbackEnabled @@ -1035,7 +1111,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']) }; }, /** @@ -1193,7 +1270,7 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ * @param {Function} error_callback function to execute on RPC call failure * @returns {jQuery.Deferred} jquery-provided ajax deferred */ - rpc: function(url, params, success_callback, error_callback) { + rpc: function(url, params) { var self = this; // url can be an $.ajax option object if (_.isString(url)) { @@ -1209,12 +1286,10 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ id: _.uniqueId('r') }; var deferred = $.Deferred(); - this.on_rpc_request(); - var aborter = params.aborter; - delete params.aborter; + this.trigger('request', url, payload); 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); @@ -1222,18 +1297,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('response_failed', jqXHR); var error = { code: -32098, message: "XmlHttpRequestError " + errorThrown, @@ -1241,24 +1310,14 @@ instance.web.JsonRPC = instance.web.CallbackEnabled.extend({ }; deferred.reject(error, $.Event()); }); - if (aborter) { - aborter.abort_last = function () { - if (!(request.isResolved() || request.isRejected())) { - deferred.fail(function (error, event) { - event.preventDefault(); - }); - request.abort(); - } - }; - } // Allow deferred user to disable on_rpc_error in fail deferred.fail(function() { deferred.fail(function(error, event) { if (!event.isDefaultPrevented()) { - self.on_rpc_error(error, event); + self.trigger('error', error, event); } }); - }).then(success_callback, error_callback).promise(); + }); return deferred; }, /** @@ -1276,7 +1335,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) { @@ -1295,7 +1354,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) { @@ -1305,19 +1364,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(); }; @@ -1341,14 +1400,15 @@ 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) { + return this.prefix + file; }, }); +instance.web.py_eval = function(expr, context) { + return py.eval(expr, _.extend({}, context || {}, {"true": true, "false": false, "null": null})); +}; + } // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js index 5a8a2b50722..220b9b63d5c 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() { @@ -46,9 +19,9 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess this.name = instance._session_id; this.qweb_mutex = new $.Mutex(); }, - rpc: function(url, params, success_callback, error_callback) { + rpc: function(url, params) { params.session_id = this.session_id; - return this._super(url, params, success_callback, error_callback); + return this._super(url, params); }, /** * Setup a sessionm @@ -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) { @@ -85,7 +57,12 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess if(self.session_is_valid()) { return deferred.pipe(function() { return self.load_modules(); }); } - return deferred; + return $.when( + deferred, + self.rpc('/web/webclient/bootstrap_translations', {mods: instance._modules}).pipe(function(trans) { + instance.web._t.database.set_bundle(trans); + }) + ); }); }, /** @@ -105,8 +82,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 }); }); }, @@ -121,13 +97,16 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess var base_location = document.location.protocol + '//' + document.location.host; var params = { db: db, login: login, password: password, base_location: base_location }; return this.rpc("/web/session/authenticate", params).pipe(function(result) { + if (!result.uid) { + return $.Deferred().reject(); + } + _.extend(self, { session_id: result.session_id, 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); @@ -139,20 +118,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 + '='; @@ -186,9 +151,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, @@ -197,31 +161,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'; } }); }); @@ -284,9 +248,6 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess } } }, - get_url: function (file) { - return this.prefix + file; - }, /** * Cooperative file download implementation, for ajaxy APIs. * @@ -332,11 +293,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(); } @@ -389,16 +353,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# */{ /** @@ -453,64 +442,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 = {}; @@ -540,10 +471,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(); @@ -573,38 +500,176 @@ $.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, + 'JSON': JSON, +}; +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 match = /^(\s*)(.+?)(\s*)$/.exec(this.node.data); + if (match) { + this.node.data = match[1] + instance.web._t(match[2]) + match[3]; + } + 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...")], + [3600, _t("Maybe you should consider reloading the application by 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..63c90f53758 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) { @@ -120,6 +120,10 @@ instance.web.Query = instance.web.Class.extend({ if (_.isEmpty(grouping)) { return null; } var self = this; + + // FIXME: when pyeval is merged + var ctx = instance.session.test_eval_contexts( + [this._model.context(this._context)]); return this._model.call('read_group', { groupby: grouping, fields: _.uniq(grouping.concat(this._fields || [])), @@ -130,7 +134,11 @@ 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( + // FIX: querygroup initialization + result.__context = result.__context || {}; + result.__context.group_by = result.__context.group_by || []; + _.defaults(result.__context, ctx); + return new instance.web.QueryGroup( self._model.name, grouping[0], result); }); }); @@ -194,7 +202,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 +288,8 @@ instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ kwargs = args; args = []; } - return instance.connection.rpc('/web/dataset/call_kw', { + var debug = instance.session.debug ? '/'+this.name+':'+method : ''; + return instance.session.rpc('/web/dataset/call_kw' + debug, { model: this.name, method: method, args: args, @@ -256,7 +312,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 +338,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 +348,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,235 +358,12 @@ instance.web.Model = instance.web.Class.extend(/** @lends openerp.web.Model# */{ }, }); -instance.web.Traverser = instance.web.Class.extend(/** @lends openerp.web.Traverser# */{ - /** - * @constructs instance.web.Traverser - * @extends instance.web.Class - * - * @param {instance.web.Model} model instance this traverser is bound to - */ - init: function (model) { - this._model = model; - this._index = 0; - }, - - /** - * Gets and sets the current index - * - * @param {Number} [idx] - * @returns {Number} current index - */ - index: function (idx) { - if (idx) { this._index = idx; } - return this._index; - }, - /** - * Returns the model this traverser is currently bound to - * - * @returns {openerp.web.Model} - */ - model: function () { - return this._model; - }, - /** - * Fetches the size of the backing model's match - * - * @returns {Deferred<Number>} deferred count - */ - size: function () { - return this._model.query().count(); - }, - - /** - * Record at the current index for the collection, fails if there is no - * record at the current index. - * - * @returns {Deferred<>} - */ - current: function (fields) { - return this._model.query(fields).first().pipe(function (record) { - if (record == null) { - return $.Deferred() - .reject('No record at index' + this._index) - .promise(); - } - return record; - }); - }, - next: function (fields) { - var self = this; - this._index++; - return this.size().pipe(function (s) { - if (self._index >= s) { - self._index = 0; - } - return self.current(fields); - }); - }, - previous: function (fields) { - var self = this; - this._index--; - if (this._index < 0) { - return this.size().pipe(function (s) { - self._index = s-1; - return self.current(fields); - }); - } - return this.current(fields); - } - -}); - -/** - * 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# */{ - /** - * Management interface between views and grouped collections of OpenERP - * records. - * - * The root DataGroup is instantiated with the relevant information - * (a session, a model, a domain, a context and a group_by sequence), the - * domain and context may be empty. It is then interacted with via - * :js:func:`~instance.web.DataGroup.list`, which is used to read the - * content of the current grouping level. - * - * @constructs instance.web.DataGroup - * @extends instance.web.OldWidget - * - * @param {instance.web.OldWidget} 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 - * @param {Array} group_by sequence of fields by which to group - * @param {Number} [level=0] nesting level of the group - */ - init: function(parent, model, domain, context, group_by, level) { - this._super(parent, null); - this.model = new instance.web.Model(model, context, domain); - this.group_by = group_by; - this.context = context; - this.domain = domain; - - this.level = level || 0; - }, - 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})); - return; - } - ifGroups(_(groups).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}); - })); - }); - } -}); -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# */ { - /** - * A specialization of groupless data groups, relying on a single static - * dataset as its records provider. - * - * @constructs instance.web.StaticDataGroup - * @extends instance.web.GrouplessDataGroup - * @param {openep.web.DataSetStatic} dataset a static dataset backing the groups - */ - init: function (dataset) { - this.dataset = dataset; - }, - list: function (fields, ifGroups, ifRecords) { - ifRecords(this.dataset); - } -}); - -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?) + * Collection of OpenERP records, used to share records and the current selection between views. * * @constructs instance.web.DataSet - * @extends instance.web.OldWidget + * @extends instance.web.CallbackEnabled * * @param {String} model the OpenERP model this dataset will manage */ @@ -638,21 +471,16 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da default_get: function(fields, options) { options = options || {}; return this._model.call('default_get', - [fields], {context: this._model.context(options.context)}); + [fields], {context: this.get_context(options.context)}); }, /** * Creates a new record in db * * @param {Object} data field values to set on the new record - * @param {Function} callback function called with operation result - * @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.get_context()}); }, /** * Saves the provided data in an existing db record @@ -663,24 +491,17 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da * @param {Function} error_callback function called in case of write error * @returns {$.Deferred} */ - write: function (id, data, options, callback, error_callback) { + write: function (id, data, options) { options = options || {}; - return this._model.call('write', - [[id], data], {context: this._model.context(options.context)}) - .pipe(function (r) { return {result: r}}) - .then(callback, error_callback); + return this._model.call('write', [[id], data], {context: this.get_context(options.context)}).then(this.trigger('dataset_changed', id, data, options)); }, /** * Deletes an existing record from the database * * @param {Number|String} ids identifier of the record to delete - * @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.get_context()}).then(this.trigger('dataset_changed', ids)); }, /** * Calls an arbitrary RPC method @@ -691,53 +512,27 @@ instance.web.DataSet = instance.web.OldWidget.extend( /** @lends openerp.web.Da * @param {Function} error_callback * @returns {$.Deferred} */ - call: function (method, args, callback, error_callback) { - return this._model.call(method, args).then(callback, error_callback); - }, - /** - * Calls an arbitrary method, with more crazy - * - * @param {String} method - * @param {Array} [args] - * @param {Number} [domain_index] index of a domain to evaluate in the args array - * @param {Number} [context_index] index of a context to evaluate in the args array - * @param {Function} callback - * @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', { - 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); + call: function (method, args) { + return this._model.call(method, args); }, /** * Calls a button method, usually returning some sort of action * * @param {String} method * @param {Array} [args] - * @param {Function} callback - * @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 * * @param {Array} ids - * @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.get_context()}); }, /** * @@ -748,28 +543,23 @@ 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.get_context()}); }, - exec_workflow: function (id, signal, callback) { - return this._model.exec_workflow(id, signal) - .pipe(function (result) { return { result: result }; }) - .then(callback); + exec_workflow: function (id, signal) { + return this._model.exec_workflow(id, signal); }, get_context: function(request_context) { return this._model.context(request_context); @@ -803,11 +593,29 @@ 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.get_context(options.context), + }).pipe(function (results) { + return results; + }); }, }); + instance.web.DataSetStatic = instance.web.DataSet.extend({ init: function(parent, model, context, ids) { + var self = this; this._super(parent, model, context); // all local records this.ids = ids || []; @@ -829,14 +637,13 @@ instance.web.DataSetStatic = instance.web.DataSet.extend({ } }, unlink: function(ids) { - this.on_unlink(ids); + this.set_ids(_.without.apply(null, [this.ids].concat(ids))); + this.trigger('unlink', ids); return $.Deferred().resolve({result: true}); }, - on_unlink: function(ids) { - 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,18 +693,17 @@ 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; } - if (this.index !== null) { + if (self.index !== null) { 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); + self.trigger("dataset_changed", ids, callback, error_callback); + }); }, size: function () { if (this._length !== undefined) { @@ -906,6 +712,7 @@ instance.web.DataSetSearch = instance.web.DataSet.extend(/** @lends openerp.web return this._super(); } }); + instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ virtual_id_prefix: "one2many_v_id_", debug_mode: true, @@ -920,16 +727,14 @@ 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(cached.id).promise(); }, - write: function (id, data, options, callback) { + write: function (id, data, options) { var self = this; var record = _.detect(this.to_create, function(x) {return x.id === id;}); record = record || _.detect(this.to_write, function(x) {return x.id === id;}); @@ -954,10 +759,8 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ } $.extend(cached.values, record.values); if (dirty) - this.on_change(); - var to_return = $.Deferred().then(callback); - to_return.resolve({result: true}); - return to_return.promise(); + this.trigger("dataset_changed", id, data, options); + return $.Deferred().resolve(true).promise(); }, unlink: function(ids, callback, error_callback) { var self = this; @@ -970,7 +773,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ this.to_write = _.reject(this.to_write, function(x) { return _.include(ids, x.id);}); this.cache = _.reject(this.cache, function(x) { return _.include(ids, x.id);}); this.set_ids(_.without.apply(_, [this.ids].concat(ids))); - this.on_change(); + this.trigger("dataset_changed", ids, callback, error_callback); return $.async_when({result: true}).then(callback); }, reset_ids: function(ids) { @@ -981,7 +784,6 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ this.cache = []; this.delete_all = false; }, - on_change: function() {}, read_ids: function (ids, fields, options) { var self = this; var to_get = []; @@ -1042,7 +844,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 +855,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,11 +866,11 @@ 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.on_change(); + this._super(n_ids); + this.trigger("dataset_changed", n_ids); }, }); instance.web.BufferedDataSet.virtual_id_regex = /^one2many_v_id_.*$/; @@ -1096,23 +898,23 @@ 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); } }, - write: function (id, data, options, callback, error_callback) { + write: function (id, data, options) { if (this.write_function) { - return this.write_function(id, data, options, this._super).then(callback, error_callback); + return this.write_function(id, data, options, this._super); } else { 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 +967,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..b4beb4c3a58 100644 --- a/addons/web/static/src/js/data_export.js +++ b/addons/web/static/src/js/data_export.js @@ -5,7 +5,15 @@ instance.web.DataExport = instance.web.Dialog.extend({ template: 'ExportTreeView', dialog_title: {toString: function () { return _t("Export Data"); }}, init: function(parent, dataset) { - this._super(parent); + var self = this; + options = { + buttons : [ + {text: _t("Close"), click: function() { self.close(); }}, + {text: _t("Export To File"), click: function() { self.on_click_export_data(); }} + ], + close: function(event, ui){ self.close();} + } + this._super(parent, options); this.records = {}; this.dataset = dataset; this.exports = new instance.web.DataSetSearch( @@ -14,42 +22,36 @@ instance.web.DataExport = instance.web.Dialog.extend({ start: function() { var self = this; this._super.apply(this, arguments); - this.open({ - buttons : [ - {text: _t("Close"), click: function() { self.close(); }}, - {text: _t("Export To File"), click: function() { self.on_click_export_data(); }} - ], - close: function(event, ui){ self.close();} - }); - self.$element.removeClass('ui-dialog-content ui-widget-content'); - self.$element.find('#add_field').click(function() { + this.open(); + 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) - }, function (records) { + }).then(function (records) { got_fields.resolve(); self.on_show_data(records); }); @@ -57,11 +59,11 @@ instance.web.DataExport = instance.web.Dialog.extend({ return $.when( got_fields, - this.rpc('/web/export/formats', {}, this.do_setup_export_formats), + this.rpc('/web/export/formats', {}).then(this.do_setup_export_formats), 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 +78,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 +88,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.rpc('/web/export/namelist', {'model': self.dataset.model, export_id: parseInt(export_id)}).then(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 +152,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 +175,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, @@ -181,7 +183,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ import_compat: Boolean(import_comp), parent_field_type : record['field_type'], exclude: exclude_fields - }, function(results) { + }).then(function(results) { record.loaded = true; self.on_show_data(results, record.id); }); @@ -191,44 +193,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 +240,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 +254,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 +299,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 +321,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 +331,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 +347,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 +365,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 +377,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 +387,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 2b527686ddd..03b2c5b775d 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); @@ -133,7 +135,13 @@ instance.web.format_value = function (value, descriptor, value_if_empty) { Math.round((value % 1) * 60)); case 'many2one': // name_get value format - return value[1]; + return value[1] ? value[1].split("\n")[0] : 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") value = instance.web.auto_str_to_date(value); @@ -266,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 689f315fa63..8da5eb0ca55 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -61,7 +61,7 @@ my.SearchQuery = B.Collection.extend({ this.on('change', function (facet) { if(!facet.values.isEmpty()) { return; } - this.remove(facet); + this.remove(facet, {silent: true}); }, this); }, add: function (values, options) { @@ -124,23 +124,14 @@ function assert(condition, message) { } 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')); - return p; - }, - onFocus: function () { - this.trigger('focused', this); - }, - onBlur: function () { - this.$element.text(''); - this.trigger('blurred', this); + events: { + focus: function () { this.trigger('focused', this); }, + blur: function () { this.$el.text(''); this.trigger('blurred', this); }, + keydown: 'onKeydown' }, 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 +193,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(); } @@ -212,6 +203,27 @@ my.InputView = instance.web.Widget.extend({ }); my.FacetView = instance.web.Widget.extend({ template: 'SearchView.FacetView', + events: { + 'focus': function () { this.trigger('focused', this); }, + 'blur': function () { this.trigger('blurred', this); }, + 'click': function (e) { + if ($(e.target).is('.oe_facet_remove')) { + this.model.destroy(); + return false; + } + this.$el.focus(); + e.stopPropagation(); + }, + 'keydown': function (e) { + var keys = $.ui.keyCode; + switch (e.which) { + case keys.BACKSPACE: + case keys.DELETE: + this.model.destroy(); + return false; + } + } + }, init: function (parent, model) { this._super(parent); this.model = model; @@ -223,37 +235,15 @@ 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) { - if ($(e.target).is('.oe_facet_remove')) { - self.model.destroy(); - return false; - } - self.$element.focus(); - e.stopPropagation(); - }); - this.$element.on('keydown', function (e) { - var keys = $.ui.keyCode; - switch (e.which) { - case keys.BACKSPACE: - case keys.DELETE: - self.model.destroy(); - return false; - } - }); - var $e = self.$element.find('> span:last-child'); - var q = $.when(this._super()); - return q.pipe(function () { - var values = self.model.values.map(function (value) { + var $e = this.$('> span:last-child'); + return $.when(this._super()).pipe(function () { + return $.when.apply(null, self.model.values.map(function (value) { return new my.FacetValueView(self, value).appendTo($e); - }); - - return $.when.apply(null, values); + })); }); }, model_changed: function () { - this.$element.text(this.$element.text() + '*'); + this.$el.text(this.$el.text() + '*'); } }); my.FacetValueView = instance.web.Widget.extend({ @@ -268,12 +258,54 @@ my.FacetValueView = instance.web.Widget.extend({ this._super(); }, model_changed: function () { - this.$element.text(this.$element.text() + '*'); + this.$el.text(this.$el.text() + '*'); } }); instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.SearchView# */{ template: "SearchView", + events: { + // focus last input if view itself is clicked + 'click': function (e) { + if (e.target === this.$('.oe_searchview_facets')[0]) { + this.$('.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 + 'autocompleteopen': function () { + var menu = this.$el.data('autocomplete').menu; + menu.activate( + $.Event({ type: "mouseenter" }), + menu.element.children().first()); + }, + // search button + 'click button.oe_searchview_search': function (e) { + e.stopImmediatePropagation(); + this.do_search(); + }, + 'click .oe_searchview_clear': function (e) { + e.stopImmediatePropagation(); + this.query.reset(); + }, + 'click .oe_searchview_unfold_drawer': function (e) { + e.stopImmediatePropagation(); + this.$el.toggleClass('oe_searchview_open_drawer'); + }, + 'keydown .oe_searchview_input, .oe_searchview_facet': function (e) { + switch(e.which) { + case $.ui.keyCode.LEFT: + this.focusPreceding(this); + e.preventDefault(); + break; + case $.ui.keyCode.RIGHT: + this.focusFollowing(this); + e.preventDefault(); + break; + } + } + }, /** * @constructs instance.web.SearchView * @extends instance.web.Widget @@ -313,7 +345,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(); @@ -324,63 +356,31 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea context: this.dataset.get_context() }); $.when(load_view) - .pipe(this.on_loaded) - .fail(function () { + .pipe(function(r) { + self.search_view_loaded(r) + }).fail(function () { self.ready.reject.apply(null, arguments); }); } - this.$element.on('keydown', - '.oe_searchview_input, .oe_searchview_facet', function (e) { - switch(e.which) { - case $.ui.keyCode.LEFT: - self.focusPreceding(this); - e.preventDefault(); - break; - case $.ui.keyCode.RIGHT: - self.focusFollowing(this); - e.preventDefault(); - break; - } - }); - - this.$element.on('click', '.oe_searchview_clear', function (e) { - e.stopImmediatePropagation(); - self.query.reset(); - }); - this.$element.on('click', '.oe_searchview_unfold_drawer', function (e) { - e.stopImmediatePropagation(); - self.$element.toggleClass('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(); + instance.web.bus.on('click', this, function(ev) { + if ($(ev.target).parents('.oe_searchview').length === 0) { + self.$el.removeClass('oe_searchview_open_drawer'); } }); - // 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; - menu.activate( - $.Event({ type: "mouseenter" }), - menu.element.children().first()); - }); 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 +395,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 +419,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 +468,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.$('div.oe_searchview_input:focus').text(); }, /** * Provide auto-completion result for req.term (an array to `resp`) @@ -498,14 +498,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.$('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 +527,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.$('div.oe_searchview_facets'); _.invoke(this.input_subviews, 'destroy'); this.input_subviews = []; @@ -549,7 +558,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,7 +624,17 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea return null; } }, - on_loaded: function(data) { + + 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 + this.custom_filters = new instance.web.search.CustomFilters(this); + // add Advanced to this.inputs + (new instance.web.search.Advanced(this)); + }, + + search_view_loaded: function(data) { var self = this; this.fields_view = data.fields_view; if (data.fields_view.type !== 'search' || @@ -624,30 +643,28 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea "Got non-search view after asking for a search view: type %s, arch root %s", data.fields_view.type, data.fields_view.arch.tag)); } - this.make_widgets( 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 - this.custom_filters = 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.$('.oe_searchview_drawer'))); + // load defaults var defaults_fetched = $.when.apply(null, _(this.inputs).invoke( - 'facet_for_defaults', this.defaults)) - .pipe(this.proxy('setup_default_query')); + 'facet_for_defaults', this.defaults)).then(function () { + self.query.reset(_(arguments).compact(), {preventSearch: true}); + }); return $.when(drawer_started, defaults_fetched) - .then(function () { self.ready.resolve(); }) + .then(function () { + self.trigger("search_view_loaded", data); + self.ready.resolve(); + }); }, setup_default_query: function () { // Hacky implementation of CustomFilters#facet_for_defaults ensure @@ -678,95 +695,6 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea this.query.reset(_(arguments).compact(), {preventSearch: true}); }, /** - * Handle event when the user make a selection in the filters management select box. - */ - on_filters_management: function(e) { - var self = this; - var select = this.$element.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(); - } - if (val.slice(0, 4) == "get:") { - val = val.slice(4); - val = parseInt(val, 10); - var filter = this.managed_filters[val]; - this.do_clear(false).then(_.bind(function() { - select.val('get:' + val); - - var groupbys = []; - var group_by = filter.context.group_by; - if (group_by) { - groupbys = _.map( - group_by instanceof Array ? group_by : group_by.split(','), - function (el) { return { group_by: el }; }); - } - this.filter_data = { - domains: [filter.domain], - contexts: [filter.context], - groupbys: groupbys - }; - this.do_search(); - }, this)); - } else { - 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. * * Result is an object with 4 (own) properties: @@ -815,7 +743,8 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea groupbys: groupbys, errors: errors }; - }, /** + }, + /** * Performs the search view collection of widget data. * * If the collection went well (all fields are valid), then triggers @@ -824,7 +753,8 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea * If at least one field failed its validation, triggers * :js:func:`instance.web.SearchView.on_invalid` instead. * - * @param e jQuery event object coming from the "Search" button + * @param [_query] + * @param {Object} [options] */ do_search: function (_query, options) { if (options && options.preventSearch) { @@ -835,7 +765,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea this.on_invalid(search.errors); return; } - return this.on_search(search.domains, search.contexts, search.groupbys); + this.trigger('search_data', search.domains, search.contexts, search.groupbys); }, /** * Triggered after the SearchView has collected all relevant domains and @@ -853,8 +783,6 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea * @param {Array} contexts an array of literal contexts or context refs * @param {Array} groupbys ordered contexts which may or may not have group_by keys */ - on_search: function (domains, contexts, groupbys) { - }, /** * Triggered after a validation error in the SearchView fields. * @@ -868,6 +796,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea */ on_invalid: function (errors) { this.do_notify(_t("Invalid Search"), _t("triggered from search view")); + this.trigger('invalid_search', errors); } }); @@ -878,6 +807,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea instance.web.search.fields = new instance.web.Registry({ 'char': 'instance.web.search.CharField', 'text': 'instance.web.search.CharField', + 'html': 'instance.web.search.CharField', 'boolean': 'instance.web.search.BooleanField', 'integer': 'instance.web.search.IntegerField', 'id': 'instance.web.search.IntegerField', @@ -913,13 +843,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 */ @@ -1041,7 +971,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); }, /** @@ -1050,7 +980,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.$('> li').removeClass('oe_selected'); var facet = this.view.query.find(_.bind(this.match_facet, this)); if (!facet) { return; } facet.values.each(function (v) { @@ -1261,7 +1191,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) { @@ -1351,17 +1281,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; } }); @@ -1542,9 +1472,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, + _t("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]); }) @@ -1557,6 +1494,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); } }); @@ -1572,9 +1518,17 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ this.model = new instance.web.Model('ir.filters'); this.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', { @@ -1621,18 +1575,20 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ */ facet_for: function (filter) { return { - category: _("Custom Filter"), + category: _t("Custom Filter"), icon: 'M', field: { get_context: function () { return filter.context; }, get_groupby: function () { return [filter.context]; }, get_domain: function () { return filter.domain; } }, - values: [ - {label: filter.name, value: null} - ] + is_custom_filter: true, + values: [{label: filter.name, value: null}] }; }, + clear_selection: function () { + this.$('li.oe_selected').removeClass('oe_selected'); + }, append_filter: function (filter) { var self = this; var key = this.key_for(filter); @@ -1644,13 +1600,12 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ var id = filter.id; this.filters[key] = filter; $filter = this.$filters[key] = $('<li></li>') - .appendTo(this.$element.find('.oe_searchview_custom_list')) + .appendTo(this.$('.oe_searchview_custom_list')) .addClass(filter.user_id ? 'oe_searchview_custom_private' : 'oe_searchview_custom_public') .text(filter.name); - $('<button type="button" class="oe_searchview_custom_delete">') - .text(_t("Delete")) + $('<a class="oe_searchview_custom_delete">x</a>') .click(function (e) { e.stopPropagation(); self.model.call('unlink', [id]).then(function () { @@ -1664,6 +1619,7 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ $filter.unbind('click').click(function () { self.view.query.reset([self.facet_for(filter)]); + $filter.addClass('oe_selected'); }); }, set_filters: function (filters) { @@ -1671,11 +1627,9 @@ 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#oe_searchview_custom_public').prop('checked'); - var set_as_default = this.$element.find( - 'input#oe_searchview_custom_default').prop('checked'); + var $name = this.$('input:first'); + var private_filter = !this.$('#oe_searchview_custom_public').prop('checked'); + var set_as_default = this.$('#oe_searchview_custom_default').prop('checked'); var search = this.view.build_search_data(); this.rpc('/web/session/eval_domain_and_context', { @@ -1688,7 +1642,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, @@ -1698,7 +1652,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(); }); @@ -1725,7 +1679,7 @@ instance.web.search.Filters = instance.web.search.Input.extend({ var col1 = [], col2 = _(this.view.controls).map(function (inputs, group) { var filters = _(inputs).filter(is_group); return { - name: group === 'null' ? _t("Filters") : group, + name: group === 'null' ? "<span class='oe_i'>q</span> " + _t("Filters") : "<span class='oe_i'>w</span> " + group, filters: filters, length: _(filters).chain().map(function (i) { return i.filters.length; }).sum().value() @@ -1743,26 +1697,27 @@ 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) { - $('<h3>').text(group.name).appendTo($el); + $('<h3>').html(group.name).appendTo($el); return $.when.apply(null, _(group.filters).invoke('appendTo', $el)); })); } }); + 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) { @@ -1771,7 +1726,7 @@ instance.web.search.Advanced = instance.web.search.Input.extend({ }); return $.when( this._super(), - this.rpc("/web/searchview/fields_get", {model: this.view.model}, function(data) { + this.rpc("/web/searchview/fields_get", {model: this.view.model}).then(function(data) { self.fields = _.extend({ id: { string: 'ID', type: 'id' } }, data.fields); @@ -1780,11 +1735,20 @@ instance.web.search.Advanced = instance.web.search.Input.extend({ }); }, append_proposition: function () { + var self = this; return (new instance.web.search.ExtendedSearchProposition(this, this.fields)) - .appendTo(this.$element.find('ul')); + .appendTo(this.$('ul')).then(function () { + self.$('button.oe_apply').prop('disabled', false); + }); + }, + remove_proposition: function (prop) { + // removing last proposition, disable apply button + if (this.getChildren().length <= 1) { + this.$('button.oe_apply').prop('disabled', true); + } + prop.destroy(); }, commit_search: function () { - var self = this; // Get domain sections from all propositions var children = this.getChildren(); var propositions = _.invoke(children, 'get_proposition'); @@ -1808,15 +1772,22 @@ 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', + events: { + 'change .searchview_extended_prop_field': 'changed', + 'click .searchview_extended_delete_prop': function (e) { + e.stopPropagation(); + this.getParent().remove_proposition(this); + } + }, /** * @constructs instance.web.search.ExtendedSearchProposition - * @extends instance.web.OldWidget + * @extends instance.web.Widget * * @param parent * @param fields @@ -1831,17 +1802,10 @@ instance.web.search.ExtendedSearchProposition = instance.web.OldWidget.extend(/* this.value = null; }, start: function () { - var _this = this; - this.$element.find(".searchview_extended_prop_field").change(function() { - _this.changed(); - }); - this.$element.find('.searchview_extended_delete_prop').click(function () { - _this.destroy(); - }); - this.changed(); + return this._super().then(this.proxy('changed')); }, changed: function() { - var nval = this.$element.find(".searchview_extended_prop_field").val(); + var nval = this.$(".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;})); } @@ -1856,7 +1820,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.$('.searchview_extended_prop_op').html(''); } this.attrs.selected = field; if(field == null) { @@ -1872,9 +1836,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.$('.searchview_extended_prop_op')); }); - var $value_loc = this.$element.find('.searchview_extended_prop_value').empty(); + var $value_loc = this.$('.searchview_extended_prop_value').empty(); this.value.appendTo($value_loc); }, @@ -1882,7 +1846,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.$('.searchview_extended_prop_op')[0]; var operator = op.options[op.selectedIndex]; return { label: _.str.sprintf(_t('%(field)s %(operator)s "%(value)s"'), { @@ -1926,7 +1890,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({ @@ -1950,7 +1914,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; } }); @@ -1968,11 +1932,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 ""; } @@ -1992,11 +1956,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 ""; } @@ -2009,12 +1973,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 f169887cc54..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); - this.action_manager.appendTo(this.view_edit_dialog); - $.when(this.action_manager.do_action(action)).then(function() { - var viewmanager = self.action_manager.inner_viewmanager, - controller = viewmanager.views[viewmanager.active_view].controller; - 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;} - }); - } - arch_to_pass = _.filter($(arch.arch), function (child) { - return child.nodeType == 1; - }); - return self.do_save_xml(arch_to_pass[0], 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'}, - '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].id); - }); - }); - }, - render_new_field :function(id){ - var self = this; - var action = { - context: {'default_model_id': id, 'manual': true}, - 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.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', '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', '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 3430f1dd703..bbb2234e3bd 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -9,31 +9,65 @@ 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 = { /** * Must return the asked field as in fields_get. */ - get_field: function(field_name) {}, + get_field_desc: 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) {}, + /** + Computes an OpenERP domain. + + @param (list) expression An OpenERP domain. + @return (boolean) The computed value of the domain. + */ + compute_domain: function(expression) {}, + /** + Builds an evaluation context for the resolution of the fields' contexts. Please note + the field are only supposed to use this context to evualuate their own, they should not + extend it. + + @return (CompoundContext) An OpenERP context. + */ + build_eval_context: function() {}, }; 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 +89,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,135 +103,142 @@ 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, + "footer_to_buttons": 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.qweb = null; // A QWeb instance will be created if the view is a QWeb template + this.__clicked_inside = false; + this.__blur_timeout = null; + this.rendering_engine = new instance.web.form.FormRenderingEngine(this); + 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(); + }); + self.on("load_record", self, self.load_record); + this.on('view_loaded', self, self.load_form); + 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) { + load_form: function(data) { var self = this; if (!data) { throw new Error("No data provided."); } if (this.arch) { - throw "Form view does not support multiple calls to on_loaded"; + throw "Form view does not support multiple calls to load_form"; } this.fields_order = []; this.fields_view = data; this.rendering_engine.set_fields_registry(this.fields_registry); this.rendering_engine.set_tags_registry(this.tags_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'); - this.rendering_engine.render_to($dest); - } + this.rendering_engine.set_widgets_registry(this.widgets_registry); + this.rendering_engine.set_fields_view(data); + 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.$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); + if (this.options.footer_to_buttons) { + this.$el.find('footer').appendTo(this.$buttons); } - 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.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.on("change:mode", this, this.switch_mode); - this.set({mode: this.options.initial_mode}); + 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(); + } + }); + this.trigger('form_view_loaded', data); return $.when(); }, - extract_qweb_template: function(fvg) { - for (var i=0, ii=fvg.arch.children.length; i < ii; i++) { - var child = fvg.arch.children[i]; - if (child.tag === "templates") { - this.qweb = new QWeb2.Engine(); - this.qweb.add_template(instance.web.json_node_to_xml(child)); - if (!this.qweb.has_template('form')) { - throw new Error("No QWeb template found for form view"); - } - return true; - } + 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; } - this.qweb = null; - return false; - }, - get_fvg_from_qweb: function(record) { - var view = this.qweb.render('form', this.get_qweb_context(record)); - var fvg = _.clone(this.fields_view); - fvg.arch = instance.web.xml_to_json(instance.web.str_to_xml(view).firstChild); - return fvg; - }, - get_qweb_context: function(record) { - var self = this, - new_record = {}; - _.each(record, function(value_, name) { - var r = _.clone(self.fields_view.fields[name] || {}); - if ((r.type === 'date' || r.type === 'datetime') && value_) { - r.raw_value = instance.web.auto_str_to_date(value_); - } else { - r.raw_value = value_; - } - r.value = instance.web.format_value(value_, r); - new_record[name] = r; - }); - return { - record : new_record, - new_record : !record.id - }; }, - kill_current_form: function() { - _.each(this.getChildren(), function(el) { - el.destroy(); - }); - this.fields = {}; - this.fields_order = []; - this.default_focus_field = null; - this.default_focus_button = null; - this.translatable_fields = []; - this.$element.find('.oe_form_container').empty(); + 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,39 +250,55 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. } } }, - do_show: function () { + /** + * + * @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() { - self.$element.css('visibility', 'visible'); + 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(); + } + 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(function(r) { + self.trigger('load_record', r); + }); + }); + } + 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(); @@ -250,24 +308,22 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. } this._super(); }, - on_record_loaded: function(record) { + load_record: 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; - - 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'); - this.rendering_engine.render_to($dest); - } + this._actualize_mode(); + this.set({ 'title' : record.id ? record.display_name : "New" }); _(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() { @@ -283,29 +339,44 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. }); } self.on_form_changed(); + self.rendering_engine.init_fields(); self.is_initialized.resolve(); self.do_update_pager(record.id == null); 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 self = this; + var keys = _.keys(this.fields_view.fields); + if (keys.length) { + return this.dataset.default_get(keys).pipe(function(r) { + self.trigger('load_record', r); + }); + } + return self.trigger('load_record', {}); + }, 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) { + execute_pager_action: function(action) { if (this.can_be_discarded()) { switch (action) { case 'first': @@ -322,20 +393,41 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. break; } this.reload(); + this.trigger('pager_action_executed'); + } + }, + 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.execute_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; var onchange = _.str.trim(on_change); var call = onchange.match(/^\s?(.*?)\((.*?)\)\s?$/); if (!call) { - return null; + throw new Error("Wrong on change format: " + onchange); } var method = call[1]; @@ -402,100 +494,74 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. var self = this; return this.on_change_mutex.exec(function() { try { - var response = {}, can_process_onchange = $.Deferred(); + var def; processed = processed || []; processed.push(widget.name); var on_change = widget.node.attrs.on_change; if (on_change) { var change_spec = self.parse_on_change(on_change, widget); - if (change_spec) { - var ajax = { - url: '/web/dataset/onchange', - async: false - }; - can_process_onchange = self.rpc(ajax, { - model: self.dataset.model, - method: change_spec.method, - args: [(self.datarecord.id == null ? [] : [self.datarecord.id])].concat(change_spec.args), - context_id: change_spec.context_index == undefined ? null : change_spec.context_index + 1 - }).then(function(r) { - _.extend(response, r); - }); - } else { - console.warn("Wrong on_change format", on_change); - } - } - // fail if onchange failed - if (can_process_onchange.isRejected()) { - return can_process_onchange; + def = self.rpc('/web/dataset/onchange', { + model: self.dataset.model, + method: change_spec.method, + args: [(self.datarecord.id == null ? [] : [self.datarecord.id])].concat(change_spec.args), + context_id: change_spec.context_index == undefined ? null : change_spec.context_index + 1 + }); + } else { + def = $.when({}); } + return def.pipe(function(response) { + if (widget.field['change_default']) { + var fieldname = widget.name + var value_; + if (response.value && (fieldname in response.value)) { + // Use value from onchange if onchange executed + value_ = response.value[fieldname]; + } else { + // otherwise get form value for field + value_ = self.fields[fieldname].get_value(); + } + var condition = fieldname + '=' + value_; - if (widget.field['change_default']) { - var fieldname = widget.name, value_; - if (response.value && (fieldname in response.value)) { - // Use value from onchange if onchange executed - value_ = response.value[fieldname]; - } else { - // otherwise get form value for field - value_ = self.fields[fieldname].get_value(); - } - var condition = fieldname + '=' + value_; - - if (value_) { - can_process_onchange = self.rpc({ - url: '/web/dataset/call', - async: false - }, { - model: 'ir.values', - method: 'get_defaults', - args: [self.model, condition] - }).then(function (results) { - if (!results.length) { return; } - if (!response.value) { - response.value = {}; - } - for(var i=0; i<results.length; ++i) { - // [whatever, key, value] - var triplet = results[i]; - response.value[triplet[1]] = triplet[2]; - } - }); + if (value_) { + return self.rpc('/web/dataset/call', { + model: 'ir.values', + method: 'get_defaults', + args: [self.model, condition] + }).pipe(function (results) { + if (!results.length) { + return response; + } + if (!response.value) { + response.value = {}; + } + for(var i=0; i<results.length; ++i) { + // [whatever, key, value] + var triplet = results[i]; + response.value[triplet[1]] = triplet[2]; + } + return response; + }); + } } - } - if (can_process_onchange.isRejected()) { - return can_process_onchange; - } - - return self.on_processed_onchange(response, processed); + return response; + }).pipe(function(response) { + return self.on_processed_onchange(response, processed); + }); } catch(e) { console.error(e); + instance.webclient.crashmanager.show_message(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("CrashManagerWarning", result.warning)), { + instance.web.dialog($(QWeb.render("CrashManager.warning", result.warning)), { + title:result.warning.title, modal: true, buttons: [ {text: _t("Ok"), click: function() { $(this).dialog("close"); }} @@ -504,6 +570,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; @@ -515,61 +584,131 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. return $.Deferred().resolve(); } catch(e) { console.error(e); + instance.webclient.crashmanager.show_message(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") { + 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.$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') && field.$label) { + if (field.focus() !== false) { + break; + } + } + } } }, on_button_save: function() { var self = this; - return this.do_save().then(function(result) { - self.set({mode: "view"}); + return this.save().then(function(result) { + self.trigger("save", result); + 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.trigger('load_record', this.datarecord); + } } + this.trigger('on_button_cancel'); 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; @@ -580,9 +719,9 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. var def = $.Deferred(); $.when(this.has_been_loaded).then(function() { self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) { - return self.on_created({ result : new_id }); + return self.record_created(new_id); }).then(function() { - return self.set({mode: "edit"}); + return self.to_edit_mode(); }).then(function() { def.resolve(); }); @@ -595,7 +734,7 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. $.when(this.has_been_loaded).then(function() { if (self.datarecord.id && confirm(_t("Do you really want to delete this record?"))) { self.dataset.unlink([self.datarecord.id]).then(function() { - self.on_pager_action('next'); + self.execute_pager_action('next'); def.resolve(); }); } else { @@ -607,17 +746,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 ``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) { + save: function(prepend_on_create) { var self = this; return this.mutating_mutex.exec(function() { return self.is_initialized.pipe(function() { try { @@ -625,6 +771,7 @@ 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; @@ -647,21 +794,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.record_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); + return self.record_saved(r); }, null); } - return save_deferral.then(success); + return save_deferral; } } catch (e) { console.error(e); @@ -670,22 +817,32 @@ 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 + "</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) { - if (!r.result) { + /** + * Reload the form after saving + * + * @param {Object} r result of the write function. + */ + record_saved: function(r) { + var self = this; + if (!r) { // should not happen in the server, but may happen for internal purpose + this.trigger('record_saved', r); return $.Deferred().reject(); } else { return $.when(this.reload()).pipe(function () { - return $.when(r).then(success); }, null); + self.trigger('record_saved', r); + return r; + }); } }, /** @@ -698,15 +855,17 @@ 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) { - if (!r.result) { + record_created: function(r, prepend_on_create) { + var self = this; + if (!r) { // should not happen in the server, but may happen for internal purpose + this.trigger('record_created', r); return $.Deferred().reject(); } else { - this.datarecord.id = r.result; + this.datarecord.id = r; if (!prepend_on_create) { this.dataset.alter_ids(this.dataset.ids.concat([this.datarecord.id])); this.dataset.index = this.dataset.ids.length - 1; @@ -717,10 +876,12 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. this.do_update_pager(); 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 () { + self.trigger('record_created', r); + return _.extend(r, {created: true}); + }); } }, on_action: function (action) { @@ -730,15 +891,23 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. var self = this; return this.reload_mutex.exec(function() { if (self.dataset.index == null) { - self.do_prev_view(); + self.trigger("previous_view"); return $.Deferred().reject().promise(); } 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 } - }).pipe(self.on_record_loaded); + 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(function(r) { + self.trigger('load_record', r); + }); } }); }, @@ -748,15 +917,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; }, @@ -766,11 +935,20 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. }, recursive_save: function() { var self = this; - return $.when(this.do_save()).pipe(function(res) { + return $.when(this.save()).pipe(function(res) { if (self.dataset.parent_view) 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; @@ -788,10 +966,20 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. return true; }, sidebar_context: function () { - return this.do_save().pipe(_.bind(function() {return this.get_fields_values();}, this)); + return this.save().pipe(_.bind(function() {return this.get_fields_values();}, this)); }, open_defaults_dialog: function () { var self = this; + var display = function (field, value) { + if (field instanceof instance.web.form.FieldSelection) { + return _(field.values).find(function (option) { + return option[0] === value; + })[1]; + } else if (field instanceof instance.web.form.FieldMany2One) { + return field.get_displayed(); + } + return value; + } var fields = _.chain(this.fields) .map(function (field, name) { var value = field.get_value(); @@ -801,32 +989,32 @@ 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': - displayed = _(field.values).find(function (option) { - return option[0] === value; - })[1]; - break; - } return { name: name, - string: field.node.attrs.string || field.field.string, + string: field.string, value: value, - displayed: displayed, - // convert undefined to false - change_default: !!field.field.change_default + displayed: display(field, value), } }) .compact() .sortBy(function (field) { return field.string; }) .value(); - var conditions = _.chain(fields) - .filter(function (field) { return field.change_default; }) + var conditions = _.chain(self.fields) + .filter(function (field) { return field.field.change_default; }) + .map(function (field, name) { + var value = field.get_value(); + return { + name: name, + string: field.string, + value: value, + displayed: display(field, value), + } + }) .value(); var d = new instance.web.Dialog(this, { @@ -838,21 +1026,21 @@ 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, field_to_set, self.fields[field_to_set].get_value(), all_users, - false, + true, condition || false ]).then(function () { d.close(); }); }} @@ -864,10 +1052,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 (this.get_field(name).translate) { + 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_desc(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); @@ -876,13 +1076,35 @@ instance.web.FormView = instance.web.View.extend(_.extend({}, instance.web.form. } }); }, - get_field: function(field_name) { + get_field_desc: 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(); }, -})); + compute_domain: function(expression) { + return instance.web.form.compute_domain(expression, this.fields); + }, + _build_view_fields_values: function(blacklist) { + var a_dataset = this.dataset; + var fields_values = this.get_fields_values(blacklist); + var active_id = a_dataset.ids[a_dataset.index]; + _.extend(fields_values, { + active_id: active_id || false, + active_ids: active_id ? [active_id] : [], + active_model: a_dataset.model, + parent: {} + }); + if (a_dataset.parent_view) { + fields_values.parent = a_dataset.parent_view.get_fields_values([a_dataset.child_name]); + } + return fields_values; + }, + build_eval_context: function(blacklist) { + var a_dataset = this.dataset; + return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values(blacklist)); + }, +}); /** * Interface to be implemented by rendering engines for the form view. @@ -890,12 +1112,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({ @@ -904,6 +1126,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; @@ -911,6 +1137,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; @@ -919,17 +1159,19 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt // but one day, we will have to get rid of xml2json var xml = instance.web.json_node_to_xml(this.fvg.arch); this.$form = $('<div class="oe_form">' + xml + '</div>'); - if (this.fvg.arch.attrs && this.fvg.arch.attrs['layout'] !== 'manual') { - this.$form.attr('layout', 'auto'); - } + + this.process_version(); this.fields_to_init = []; this.tags_to_init = []; + this.widgets_to_init = []; this.labels = {}; this.process(this.$form); this.$form.appendTo(this.$target); + this.to_replace = []; + _.each(this.fields_to_init, function($elem) { var name = $elem.attr("name"); if (!self.fvg.fields[name]) { @@ -937,7 +1179,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } var obj = self.fields_registry.get_any([$elem.attr('widget'), self.fvg.fields[name].type]); if (!obj) { - throw new Error("Widget type '"+ key + "' is not implemented"); + throw new Error("Widget type '"+ $elem.attr('widget') + "' is not implemented"); } var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); var $label = self.labels[$elem.attr("name")]; @@ -946,23 +1188,33 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } self.alter_field(w); self.view.register_field(w, $elem.attr("name")); - w.replace($elem); + self.to_replace.push([w, $elem]); }); _.each(this.tags_to_init, function($elem) { var tag_name = $elem[0].tagName.toLowerCase(); var obj = self.tags_registry.get_object(tag_name); var w = new (obj)(self.view, instance.web.xml_to_json($elem[0])); - w.replace($elem); - }) - // TODO: return a deferred + self.to_replace.push([w, $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])); + self.to_replace.push([w, $elem]); + }); + }, + init_fields: function() { + var defs = []; + _.each(this.to_replace, function(el) { + defs.push(el[0].replace(el[1])); + }); + this.to_replace = []; + return $.when.apply($, defs); }, - render_element: function(template, layout/* dictionaries */) { - var dicts = [].slice.call(arguments).slice(2); - dicts.unshift({ 'layout' : layout }); + render_element: function(template /* dictionaries */) { + var dicts = [].slice.call(arguments).slice(1); var dict = _.extend.apply(_, dicts); dict['classnames'] = dict['class'] || ''; // class is a reserved word and might caused problem to Safari when used from QWeb - var alt_template = template + '.' + layout; - template = QWeb.has_template(alt_template) ? alt_template : template; return $(QWeb.render(template, dict)); }, alter_field: function(field) { @@ -977,10 +1229,8 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } this.$target.toggleClass('oe_layout_debugging'); }, - process: function($tag, layout) { + process: function($tag) { var self = this; - layout = $tag.attr('layout') || layout || 'auto'; - $tag.removeAttr('layout'); var tagname = $tag[0].nodeName.toLowerCase(); if (this.tags_registry.contains(tagname)) { this.tags_to_init.push($tag); @@ -990,39 +1240,49 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt if (fn) { var args = [].slice.call(arguments); args[0] = $tag; - args[1] = layout; return fn.apply(self, args); } else { // generic tag handling, just process children $tag.children().each(function() { - self.process($(this), layout); + self.process($(this)); }); self.handle_common_properties($tag, $tag); $tag.removeAttr("modifiers"); return $tag; } }, - process_sheet: function($sheet, layout) { - var $new_sheet = this.render_element('FormRenderingSheet', layout, $sheet.getAttributes()); + 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); - var $dst = (layout === 'auto') ? $new_sheet.find('group:first') : $new_sheet.find('.oe_form_sheet'); - $sheet.children().appendTo($dst); + var $dst = $new_sheet.find('.oe_form_sheet'); + $sheet.contents().appendTo($dst); $sheet.before($new_sheet).remove(); - this.process($new_sheet, layout); + this.process($new_sheet); }, - process_form: function($form, layout) { - var $new_form = this.render_element('FormRenderingForm', layout, $form.getAttributes()); + process_form: function($form) { + if ($form.find('> sheet').length === 0) { + $form.addClass('oe_form_nosheet'); + } + var $new_form = this.render_element('FormRenderingForm', $form.getAttributes()); this.handle_common_properties($new_form, $form); - var $dst = (layout === 'auto') ? $new_form.find('group:first') : $new_form; - $form.children().appendTo($dst); + $form.contents().appendTo($new_form); if ($form[0] === this.$form[0]) { // If root element, replace it this.$form = $new_form; } else { $form.before($new_form).remove(); } - this.process($new_form, layout); + this.process($new_form); }, + /* + * Used by direct <field> children of a <group> tag only + * This method will add the implicit <label...> for every field + * in the <group> + */ preprocess_field: function($field) { var self = this; var name = $field.attr('name'), @@ -1043,11 +1303,12 @@ 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'), "help": $field.attr('help'), + "class": $field.attr('class'), }); $label.insertBefore($field); if (field_colspan > 1) { @@ -1055,31 +1316,34 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } return $label; }, - process_field: function($field, layout) { - var $label = this.preprocess_field($field); - if ($label) - this.process($label, layout); + process_field: function($field) { + if ($field.parent().is('group')) { + // No implicit labels for normal fields, only for <group> direct children + var $label = this.preprocess_field($field); + if ($label) { + this.process($label); + } + } this.fields_to_init.push($field); return $field; }, - process_group: function($group, layout) { + process_group: function($group) { var self = this; - if ($group.parent().is('.oe_form_group_cell')) { - $group.parent().addClass('oe_form_group_nested'); - } $group.children('field').each(function() { self.preprocess_field($(this)); }); - var $new_group = this.render_element('FormRenderingGroup', layout, $group.getAttributes()), - $table; - if ($new_group.is('table')) { + var $new_group = this.render_element('FormRenderingGroup', $group.getAttributes()); + var $table; + if ($new_group.first().is('table.oe_form_group')) { $table = $new_group; + } else if ($new_group.filter('table.oe_form_group').length) { + $table = $new_group.filter('table.oe_form_group').first(); } else { - $table = $new_group.find('table:first'); + $table = $new_group.find('table.oe_form_group').first(); } - $table.addClass('oe_form_group'); + var $tr, $td, - cols = parseInt($group.attr('col') || 4, 10), + cols = parseInt($group.attr('col') || 2, 10), row_cols = cols; var children = []; @@ -1089,6 +1353,8 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt var tagName = $child[0].tagName.toLowerCase(); var $td = $('<td/>').addClass('oe_form_group_cell').attr('colspan', colspan); var newline = tagName === 'newline'; + + // Note FME: those classes are used in layout debug mode if ($tr && row_cols > 0 && (newline || row_cols < colspan)) { $tr.addClass('oe_form_group_row_incomplete'); if (newline) { @@ -1102,6 +1368,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; @@ -1118,7 +1387,6 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } $group.before($new_group).remove(); - // Now compute width of cells $table.find('> tbody > tr').each(function() { var to_compute = [], row_cols = cols, @@ -1128,16 +1396,11 @@ 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--; - } break; case 'label': if ($child.attr('for')) { $td.attr('width', '1%').addClass('oe_form_group_cell_label'); - row_cols--; + row_cols-= $td.attr('colspan') || 1; total--; } break; @@ -1154,20 +1417,22 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } $td.attr('width', width); $child.removeAttr('width'); - row_cols--; + row_cols-= $td.attr('colspan') || 1; } else { to_compute.push($td); } } }); - var unit = Math.floor(total / row_cols); - if (!$(this).is('.oe_form_group_row_incomplete')) { - _.each(to_compute, function($td, i) { - var width = parseInt($td.attr('colspan'), 10) * unit; - $td.attr('width', ((i == to_compute.length - 1) ? total : width) + '%'); - total -= width; - }); + if (row_cols) { + var unit = Math.floor(total / row_cols); + if (!$(this).is('.oe_form_group_row_incomplete')) { + _.each(to_compute, function($td, i) { + var width = parseInt($td.attr('colspan'), 10) * unit; + $td.attr('width', width + '%'); + total -= width; + }); + } } }); _.each(children, function(el) { @@ -1176,37 +1441,61 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt this.handle_common_properties($new_group, $group); return $new_group; }, - process_notebook: function($notebook, layout) { + process_notebook: function($notebook) { var self = this; var pages = []; $notebook.find('> page').each(function() { var $page = $(this); var page_attrs = $page.getAttributes(); page_attrs.id = _.uniqueId('notebook_page_'); - pages.push(page_attrs); - var $new_page = self.render_element('FormRenderingNotebookPage', layout, page_attrs); - var $dst = (layout === 'auto') ? $new_page.find('group:first') : $new_page; - $page.children().appendTo($dst); + var $new_page = self.render_element('FormRenderingNotebookPage', page_attrs); + $page.contents().appendTo($new_page); $page.before($new_page).remove(); - self.handle_common_properties($new_page, $page); + var ic = self.handle_common_properties($new_page, $page).invisibility_changer; + page_attrs.__page = $new_page; + page_attrs.__ic = ic; + pages.push(page_attrs); + + $new_page.children().each(function() { + self.process($(this)); + }); }); - var $new_notebook = this.render_element('FormRenderingNotebook', layout, { pages : pages }); - $notebook.children().appendTo($new_notebook); + var $new_notebook = this.render_element('FormRenderingNotebook', { pages : pages }); + $notebook.contents().appendTo($new_notebook); $notebook.before($new_notebook).remove(); - $new_notebook.children().each(function() { - self.process($(this)); - }); + self.process($($new_notebook.children()[0])); + //tabs and invisibility handling $new_notebook.tabs(); + _.each(pages, function(page, i) { + 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; + var first_visible = _.find(_.range(pages.length), function(i2) { + return (! pages[i2].__ic) || (! pages[i2].__ic.get("effective_invisible")); + }); + if (first_visible !== undefined) { + $new_notebook.tabs('select', first_visible); + } + }); + }); + this.handle_common_properties($new_notebook, $notebook); return $new_notebook; }, - process_separator: function($separator, layout) { - var $new_separator = this.render_element('FormRenderingSeparator', layout, $separator.getAttributes()); + process_separator: function($separator) { + var $new_separator = this.render_element('FormRenderingSeparator', $separator.getAttributes()); $separator.before($new_separator).remove(); this.handle_common_properties($new_separator, $separator); return $new_separator; }, - process_label: function($label, layout) { + process_label: function($label) { var name = $label.attr("for"), field_orm = this.fvg.fields[name]; var dict = { @@ -1223,7 +1512,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt align = 'center'; } dict.align = align; - var $new_label = this.render_element('FormRenderingLabel', layout, dict); + var $new_label = this.render_element('FormRenderingLabel', dict); $label.before($new_label).remove(); this.handle_common_properties($new_label, $label); if (name) { @@ -1232,52 +1521,66 @@ 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) - new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element); + ic = new instance.web.form.InvisibilityChanger(this.view, this.view, modifiers.invisible, $new_element); $new_element.addClass($node.attr("class") || ""); $new_element.attr('style', $node.attr('style')); + return {invisibility_changer: ic,}; }, }); -instance.web.form.FormRenderingEngineReadonly = instance.web.form.FormRenderingEngine.extend({ - alter_field: function(field) { - field.set({"force_readonly": true}); - }, -}); +/** + Welcome. -instance.web.form.FormDialog = instance.web.Dialog.extend({ - init: function(parent, options, view_id, dataset) { - this._super(parent, options); - this.dataset = dataset; - this.view_id = view_id; - return this; - }, - start: function() { - this._super(); - this.form = new instance.web.FormView(this, this.dataset, this.view_id, { - pager: false + If you read this documentation, it probably means that you were asked to use a form view widget outside of + a form view. Before going further, you must understand that those fields were never really created for + that usage. Don't think that this class will hold the answer to all your problems, at best it will allow + you to hack the system with more style. +*/ +instance.web.form.DefaultFieldManager = instance.web.Widget.extend({ + init: function(parent, eval_context) { + this._super(parent); + this.field_descs = {}; + this.eval_context = eval_context || {}; + this.set({ + display_invalid_fields: false, + actual_mode: 'create', }); - this.form.appendTo(this.$element); - this.form.on_created.add_last(this.on_form_dialog_saved); - this.form.on_saved.add_last(this.on_form_dialog_saved); - return this; }, - select_id: function(id) { - if (this.form.dataset.select_id(id)) { - return this.form.do_show(); - } else { - this.do_warn("Could not find id in dataset"); - return $.Deferred().reject(); + get_field_desc: function(field_name) { + if (this.field_descs[field_name] === undefined) { + this.field_descs[field_name] = { + string: field_name, + }; } + return this.field_descs[field_name]; + }, + extend_field_desc: function(fields) { + var self = this; + _.each(fields, function(v, k) { + _.extend(self.get_field_desc(k), v); + }); + }, + get_field_value: function(field_name) { + return false; + }, + set_values: function(values) { + // nothing + }, + compute_domain: function(expression) { + return instance.web.form.compute_domain(expression, {}); + }, + build_eval_context: function() { + return new instance.web.CompoundContext(this.eval_context); }, - on_form_dialog_saved: function(r) { - this.close(); - } }); instance.web.form.compute_domain = function(expr, fields) { + if (! (expr instanceof Array)) + return !! expr; var stack = []; for (var i = expr.length - 1; i >= 0; i--) { var ex = expr[i]; @@ -1351,83 +1654,118 @@ 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 : + self._ic_field_manager.compute_domain(self._ic_invisible_modifier); + 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() { - var check_visibility = function() { - if (this.get("effective_invisible")) { - this.$element.hide(); - } else { - this.$element.show(); - } - }; - this.on("change:effective_invisible", this, check_visibility); - _.bind(check_visibility, this)(); + this.on("change:effective_invisible", this, this._check_visibility); + this._check_visibility(); + }, + _check_visibility: function() { + 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.field_manager = field_manager; + if (this.field_manager instanceof instance.web.FormView) + this.view = this.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.view.on("view_content_has_changed", this, this.process_modifiers); + this.field_manager.on("view_content_has_changed", this, this.process_modifiers); + + this.set({ + required: false, + readonly: false, + }); + // some events to make the property "effective_readonly" sync automatically with "readonly" and + // "mode" on field_manager + var self = this; + 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.process_modifiers(); 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); + var val = this.field_manager.compute_domain(this.modifiers[a]); to_set[a] = val; } } @@ -1435,7 +1773,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, @@ -1446,7 +1784,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'), @@ -1456,25 +1794,6 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. }, options || {}); $(trigger).tipsy(options); }, - _build_view_fields_values: function(blacklist) { - var a_dataset = this.view.dataset; - var fields_values = this.view.get_fields_values(blacklist); - var active_id = a_dataset.ids[a_dataset.index]; - _.extend(fields_values, { - active_id: active_id || false, - active_ids: active_id ? [active_id] : [], - active_model: a_dataset.model, - parent: {} - }); - if (a_dataset.parent_view) { - fields_values.parent = a_dataset.parent_view.get_fields_values([a_dataset.child_name]); - } - return fields_values; - }, - _build_eval_context: function(blacklist) { - var a_dataset = this.view.dataset; - return new instance.web.CompoundContext(a_dataset.get_context(), this._build_view_fields_values(blacklist)); - }, /** * Builds a new context usable for operations related to fields by merging * the fields'context with the action's context. @@ -1485,9 +1804,9 @@ 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); + var fields_values = this.field_manager.build_eval_context(blacklist); v_context = new instance.web.CompoundContext(v_context).set_eval_context(fields_values); } return v_context; @@ -1498,31 +1817,35 @@ instance.web.form.FormWidget = instance.web.Widget.extend(_.extend({}, instance. // if there is a domain on the node, overrides the model's domain var final_domain = n_domain !== null ? n_domain : f_domain; if (!(final_domain instanceof Array) || true) { //TODO: remove true - var fields_values = this._build_eval_context(); + var fields_values = this.field_manager.build_eval_context(); final_domain = new instance.web.CompoundDomain(final_domain).set_eval_context(fields_values); } 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; @@ -1571,37 +1894,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 @@ -1610,14 +1926,11 @@ 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. - * + * regardless of any asynchronous operation currently running. Calls to set_value() can and will also occur + * before the widget is inserted into the DOM. + * * 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 @@ -1627,19 +1940,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() {}, @@ -1654,28 +1967,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 @@ -1684,41 +1999,30 @@ 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.field = this.field_manager.get_field_desc(this.name); + this.widget = this.node.attrs.widget; + this.string = this.node.attrs.string || this.field.string || this.name; + this.options = instance.web.py_eval(this.node.attrs.options || '{}'); 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.on("change:value", this, function() { - if (! this._inhibit_on_change) - this.trigger('changed_value'); + this.trigger('changed_value'); this._check_css_flags(); }); }, renderElement: function() { 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)); + if (this.field.translate && this.view) { + 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 ? 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; @@ -1730,23 +2034,47 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(_.extend({ this.on("change:required", this, this._set_required); this._set_required(); } + this._check_visibility(); + this.field_manager.off("change:display_invalid_fields", this, this._check_css_flags); + this.field_manager.on("change:display_invalid_fields", this, this._check_css_flags); + this._check_css_flags(); + }, + start: function() { + var tmp = this._super(); + this.on("change:value", this, function() { + if (! this.no_rerender) + this.render_value(); + }); + this.render_value(); }, /** * 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'); }, + /** + Utility method that all implementations should use to change the + value without triggering a re-rendering. + */ + internal_set_value: function(value_) { + var tmp = this.no_render; + this.no_rerender = true; + this.set({'value': value_}); + this.no_rerender = tmp; + }, + /** + This method is called each time the value is modified. + */ + render_value: function() {}, 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; @@ -1758,62 +2086,65 @@ 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); + }); + }, + + set_dimensions: function (height, width) { + // remove width css property + this.$el.css('width', ''); + // extract style (without width) + var old_style = this.$el.attr('style'); + // jQuery doesn't understand/use !important + var style = 'width:' + width + 'px !important;'; + if (old_style) { + style += old_style + } + this.$el.attr('style', style); + this.$el.css('minHeight', height); + } +}); /** - * 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(). + * Default implementation of, you should not override it, use initialize_field() instead. */ start: function() { - this._super(); this.initialize_field(); + this._super(); }, initialize_field: function() { - this.on("change:effective_readonly", this, function() { - this.destroy_content(); - this.renderElement(); - this.initialize_content(); - this.render_value(); - }); + this.on("change:effective_readonly", this, this.reinitialize); + this.initialize_content(); + }, + reinitialize: function() { + this.destroy_content(); + this.renderElement(); this.initialize_content(); - this.render_value(); }, /** * Called to destroy anything that could have been created previously, called before a @@ -1824,43 +2155,49 @@ instance.web.form.ReinitializeFieldMixin = { * Called to initialize the content. */ initialize_content: function() {}, - /** - * 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, { +/** + * 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, { + reinitialize: function() { + instance.web.form.ReinitializeWidgetMixin.reinitialize.call(this); + this.render_value(); + }, +}); + +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) { this._super(field_manager, node); this.password = this.node.attrs.password === 'True' || this.node.attrs.password === '1'; }, 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.internal_set_value(self.parse_value($input.val())); }); - }, - set_value: function(value_) { - this._super(value_); - this.render_value(); + this.setupFocus($input); }, render_value: function() { - var show_value = instance.web.format_value(this.get('value'), this, ''); + var show_value = this.format_value(this.get('value'), ''); 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.$(".oe_form_char_content").text(show_value); } }, is_syntax_valid: function() { - if (!this.get("effective_readonly")) { + if (!this.get("effective_readonly") && this.$("input").size() > 0) { try { - var value_ = instance.web.parse_value(this.$element.find('input').val(), this, ''); + var value_ = this.parse_value(this.$el.find('input').val(), ''); return true; } catch(e) { return false; @@ -1868,31 +2205,39 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(_.extend({} } return true; }, + parse_value: function(val, def) { + return instance.web.parse_value(val, this, def); + }, + format_value: function(val, def) { + return instance.web.format_value(val, this, def); + }, is_false: function() { - return this.get('value') === ''; + 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() { @@ -1908,7 +2253,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")) { @@ -1919,7 +2266,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() { @@ -1936,11 +2283,12 @@ instance.web.form.FieldUrl = instance.web.form.FieldChar.extend({ instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({ is_field_number: true, + widget_class: 'oe_form_field_float', init: function (field_manager, node) { this._super(field_manager, node); - this.set({'value': 0}); + this.internal_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; } @@ -1951,11 +2299,14 @@ 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({ - template: "web.datetimepicker", +instance.web.DateTimeWidget = instance.web.Widget.extend({ + template: "web.datepicker", jqueryui_object: 'datetimepicker', type_of_date: "datetime", init: function(parent) { @@ -1964,23 +2315,30 @@ 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.change(this.on_change); + this.$input = this.$el.find('input.oe_datepicker_master'); + this.$input_picker = this.$el.find('input.oe_datepicker_container'); + this.$input.change(function(){ + self.change_datetime(); + }); + 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}); @@ -1990,7 +2348,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_}); @@ -2006,7 +2367,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(); @@ -2027,9 +2388,10 @@ instance.web.DateTimeWidget = instance.web.OldWidget.extend({ format_client: function(v) { return instance.web.format_value(v, {"widget": this.type_of_date}); }, - on_change: function() { + change_datetime: function() { if (this.is_valid_()) { this.set_value_from_ui_(); + this.trigger("datetime_changed"); } } }); @@ -2039,8 +2401,8 @@ 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, { - template: "EmptyComponent", +instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { + template: "FieldDatetime", build_widget: function() { return new instance.web.DateTimeWidget(this); }, @@ -2053,69 +2415,77 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(_.exten initialize_content: function() { if (!this.get("effective_readonly")) { this.datewidget = this.build_widget(); - this.datewidget.on_change.add_last(_.bind(function() { - this.set({'value': this.datewidget.get_value()}); + this.datewidget.on('datetime_changed', this, _.bind(function() { + this.internal_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_) { - this._super(value_); - this.render_value(); - }, render_value: function() { 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() { - if (!this.get("effective_readonly")) { + if (!this.get("effective_readonly") && this.datewidget) { return this.datewidget.is_valid_(); } return true; }, is_false: function() { - return this.get('value') === ''; + 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", build_widget: function() { return new instance.web.DateWidget(this); } }); -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.internal_set_value(instance.web.parse_value(self.$textarea.val(), self)); }, this)); } else { this.$textarea.attr('disabled', 'disabled'); } - }, - set_value: function(value_) { - this._super.apply(this, arguments); - this.render_value(); + this.$el.keyup(function (e) { + if (e.which === $.ui.keyCode.ENTER) { + e.stopPropagation(); + } + }); + this.setupFocus(this.$textarea); }, render_value: function() { + $(window).resize(); 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")) { + if (!this.get("effective_readonly") && this.$textarea) { try { var value_ = instance.web.parse_value(this.$textarea.val(), this, ''); return true; @@ -2126,53 +2496,91 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(_.extend({} return true; }, is_false: function() { - return this.get('value') === ''; + return this.get('value') === '' || this._super(); }, - focus: function($element) { - this.delay_focus(this.$textarea); + focus: function($el) { + this.$textarea.focus(); }, - 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)); + set_dimensions: function (height, width) { + this._super(); + this.$textarea.css({ + width: width, + minHeight: height }); - $div.appendTo($('body')); - new_height = $div.height(); - if (new_height < 90) { - new_height = 90; + }, +}); + +/** + * 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 || '100%'); + 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.internal_set_value(self.$textarea.val()); + } + }); } - if (!isNaN(max_height) && new_height > max_height) { - new_height = max_height; + }, + 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() { - this._super.apply(this, arguments); - this.$checkbox = $("input", this.$element); - this.$element.click(_.bind(function() { - this.set({'value': this.$checkbox.is(':checked')}); + var self = this; + this.$checkbox = $("input", this.$el); + this.setupFocus(this.$checkbox); + this.$el.click(_.bind(function() { + this.internal_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)(); - }, - set_value: function(value_) { + check_readonly.call(this); this._super.apply(this, arguments); - this.$checkbox[0].checked = value_; + }, + render_value: function() { + this.$checkbox[0].checked = this.get('value'); }, focus: function() { - this.delay_focus(this.$checkbox); + this.$checkbox.focus(); } }); @@ -2180,27 +2588,23 @@ 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") }); }, - set_value: function(value_) { - this._super.apply(this, arguments); - var show_value = Number(value_); + render_value: function() { + var show_value = Number(this.get('value')); if (isNaN(show_value)) { 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; @@ -2226,9 +2630,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.internal_set_value(this.values[this.$el.find('select')[0].selectedIndex][0]); }, this)) .change(function () { ischanging = true; }) .click(function () { ischanging = false; }) @@ -2237,12 +2641,12 @@ 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_; value_ = value_ instanceof Array ? value_[0] : value_; this._super(value_); - this.render_value(); }, render_value: function() { if (!this.get("effective_readonly")) { @@ -2250,27 +2654,20 @@ 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]; - 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; @@ -2297,7 +2694,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); } }); })(); @@ -2317,54 +2715,72 @@ instance.web.form.CompletionFieldMixin = { var self = this; 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, self.build_domain(), 'ilike', this.limit + 1)).pipe(function(data) { + search_val, new instance.web.CompoundDomain(self.build_domain(), [["id", "not in", blacklist]]), + '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; }); }, + get_search_blacklist: function() { + return []; + }, _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(); @@ -2380,7 +2796,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 @@ -2388,18 +2804,53 @@ instance.web.form.CompletionFieldMixin = { self.build_domain(), new instance.web.CompoundContext(self.build_context(), context || {}) ); - pop.on_select_elements.add(function(element_ids) { + pop.on("elements_selected", self, 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); @@ -2408,114 +2859,159 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten this.display_value = {}; this.last_search = []; this.floating = false; - this.inhibit_on_change = false; + this.current_display = null; + this.is_started = false; }, - start: function() { - this._super(); - instance.web.form.ReinitializeFieldMixin.start.call(this); - this.on("change:value", this, function() { - this.floating = false; + reinit_value: function(val) { + this.internal_set_value(val); + this.floating = false; + if (this.is_started) this.render_value(); + }, + initialize_field: function() { + this.is_started = true; + instance.web.bus.on('click', this, function() { + if (!this.get("effective_readonly") && this.$input && this.$input.autocomplete('widget').is(':visible')) { + this.$input.autocomplete("close"); + } }); + instance.web.form.ReinitializeFieldMixin.initialize_field.call(this); }, 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); + var pop = new instance.web.form.FormOpenPopup(self); pop.show_element( self.field.relation, 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() { + pop.on('write_completed', self, 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.internal_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; - var anyoneLoosesFocus = function() { + self.ed_def = $.Deferred(); + self.uned_def = $.Deferred(); + var ed_delay = 200; + var ed_duration = 15000; + var anyoneLoosesFocus = function (e) { + var used = false; if (self.floating) { if (self.last_search.length > 0) { if (self.last_search[0][0] != self.get("value")) { self.display_value = {}; self.display_value["" + self.last_search[0][0]] = self.last_search[0][1]; - self.set({value: self.last_search[0][0]}); + self.reinit_value(self.last_search[0][0]); } else { + used = true; self.render_value(); } } else { - self.set({value: false}); + used = true; + self.reinit_value(false); } + 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(); + ignore_blur = false; + self.trigger('focused'); }); + ignore_blur = true; 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 @@ -2531,10 +3027,11 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(_.exten if (item.id) { self.display_value = {}; self.display_value["" + item.id] = item.name; - self.set({value: item.id}); + self.reinit_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; } }, @@ -2542,21 +3039,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")) { @@ -2569,8 +3067,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); }); @@ -2579,47 +3077,76 @@ 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.$('.oe_m2o_cm_button').css({'display':'inline'}); + } } 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, res_id: self.get("value"), - context: self.build_context(), views: [[false, 'form']], target: 'current' }); 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; - this.inhibit_on_change = true; - this._super(value_); - this.inhibit_on_change = false; + this.reinit_value(value_); + }, + get_displayed: function() { + return this.display_value["" + this.get("value")]; }, add_id: function(id) { this.display_value = {}; - this.set({value: id}); + this.reinit_value(id); }, is_false: function() { return ! this.get("value"); }, focus: function () { - this.delay_focus(this.$input); - } -})); + if (!this.get('effective_readonly')) { + 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 @@ -2674,27 +3201,26 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ lazy_build_o2m_kanban_view(); this.is_loaded = $.Deferred(); this.initial_is_loaded = this.is_loaded; - this.is_setted = $.Deferred(); this.form_last_update = $.Deferred(); this.init_form_last_update = this.form_last_update; - }, - start: function() { - this._super.apply(this, arguments); - - var self = this; - + this.is_started = false; this.dataset = new instance.web.form.One2ManyDataSet(this, this.field.relation); 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() { + var self = this; + this.dataset.on('dataset_changed', this, function() { self.trigger_on_change(); }); + this.set_value([]); + }, + start: function() { + this._super.apply(this, arguments); + this.$el.addClass('oe_form_field oe_form_field_one2many'); - this.is_setted.then(function() { - self.load_views(); - }); + var self = this; + + self.load_views(); this.is_loaded.then(function() { self.on("change:effective_readonly", self, function() { self.is_loaded = self.is_loaded.pipe(function() { @@ -2705,6 +3231,8 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ }); }); }); + this.is_started = true; + this.reload_current_view(); }, trigger_on_change: function() { var tmp = this.doing_on_change; @@ -2714,11 +3242,14 @@ 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)) { + throw new Error(_.str.sprintf("View type '%s' is not supported in One2Many.", mode)); + } var view = { view_id: false, view_type: mode == "tree" ? "list" : mode, @@ -2728,20 +3259,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") { + _.extend(view.options, { + confirm_on_delete: false, + }); if (self.get("effective_readonly")) { - view.options.action_buttons = false; - view.options.quick_creatable = false; + _.extend(view.options, { + action_buttons: false, + quick_creatable: false, + creatable: false, + read_only_mode: true, + }); } } views.push(view); @@ -2756,36 +3304,45 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ var def = $.Deferred().then(function() { self.initial_is_loaded.resolve(); }); - this.viewmanager.on_controller_inited.add_last(function(view_type, controller) { + this.viewmanager.on("controller_inited", self, 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; + }); + _(controller.columns).find(function (column) { + if (!(column instanceof instance.web.list.Handle)) { + return false; + } + column.modifiers.invisible = true; + return 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(); - }); - controller.on_pager_action.add_first(function() { - self.save_any_view(); - }); + controller.on("load_record", self, function(){ + once.resolve(); + }); + controller.on('pager_action_executed',self,self.save_any_view); } else if (view_type == "graph") { self.reload_current_view() } def.resolve(); }); - this.viewmanager.on_mode_switch.add_first(function(n_mode, b, c, d, e) { + this.viewmanager.on("switch_mode", self, function(n_mode, b, c, d, e) { $.when(self.save_any_view()).then(function() { - if(n_mode === "list") - $.async_when().then(function() {self.reload_current_view();}); + if (n_mode === "list") { + $.async_when().then(function() { + self.reload_current_view(); + }); + } }); }); - this.is_setted.then(function() { - $.async_when().then(function () { - self.viewmanager.appendTo(self.$element); - }); + $.async_when().then(function () { + self.viewmanager.appendTo(self.$el); }); return def; }, @@ -2865,8 +3422,12 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ if (this.dataset.index === null && this.dataset.ids.length > 0) { this.dataset.index = 0; } - self.is_setted.resolve(); - return self.reload_current_view(); + this.trigger_on_change(); + if (this.is_started) { + return self.reload_current_view(); + } else { + return $.when(); + } }, get_value: function() { var self = this; @@ -2892,42 +3453,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.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; }, @@ -2942,16 +3505,23 @@ 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) { + switch_mode: function(mode, unused) { if (mode !== 'form') { return this._super(mode, unused); } var self = this; 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); + var pop = new instance.web.form.FormOpenPopup(this); 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])); + self.o2m.dataset.trigger("dataset_changed", r); + }); + }, write_function: function(id, data, options) { return self.o2m.dataset.write(id, data, {}).then(function() { self.o2m.reload_current_view(); @@ -2966,6 +3536,9 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({ form_view_options: {'not_interactible_on_create':true}, readonly: self.o2m.get("effective_readonly") }); + pop.on("elements_selected", self, function() { + self.o2m.reload_current_view(); + }); }, }); @@ -2978,8 +3551,52 @@ 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 || {}, { + GroupsType: instance.web.form.One2ManyGroups, + 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; @@ -2987,13 +3604,13 @@ 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) { return self.o2m.dataset.create(data).then(function(r) { - self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r.result])); - self.o2m.dataset.on_change(); + self.o2m.dataset.set_ids(self.o2m.dataset.ids.concat([r])); + self.o2m.dataset.trigger("dataset_changed", r); }).then(callback, error_callback); }, read_function: function() { @@ -3006,18 +3623,18 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ self.o2m.build_domain(), self.o2m.build_context() ); - pop.on_select_elements.add_last(function() { + pop.on("elements_selected", self, function() { self.o2m.reload_current_view(); }); } }, do_activate_record: function(index, id) { var self = this; - var pop = new instance.web.form.FormOpenPopup(self.o2m.view); + var pop = new instance.web.form.FormOpenPopup(self); 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) { + return self.o2m.dataset.write(id, data, {}).then(function() { self.o2m.reload_current_view(); }); }, @@ -3028,23 +3645,167 @@ 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; - var def = $.Deferred().then(callback).then(function() {self.o2m.view.reload();}); - return this._super(name, id, _.bind(def.resolve, def)); + this.ensure_saved().pipe(function () { + if (parent_form) + return parent_form.save(); + else + return $.when(); + }).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.One2ManyGroups = instance.web.ListView.Groups.extend({ + setup_resequence_rows: function () { + if (!this.view.o2m.get('effective_readonly')) { + this._super.apply(this, arguments); + } + } +}); +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 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 an item")) + .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) + } } }); instance.web.form.One2ManyFormView = instance.web.FormView.extend({ form_template: 'One2Many.formview', - on_loaded: function(data) { + load_form: function(data) { this._super(data); var self = this; this.$buttons.find('button.oe_form_button_create').click(function() { - self.do_save().then(self.on_button_new); + self.save().then(self.on_button_new); }); }, do_notify_change: function() { @@ -3063,8 +3824,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); @@ -3073,16 +3833,11 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. this._display_orderer = new instance.web.DropMisordered(); this._drop_shown = false; }, - start: function() { - this._super(); - instance.web.form.ReinitializeFieldMixin.start.call(this); - this.on("change:value", this, this.render_value); - }, initialize_content: function() { 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: { @@ -3134,29 +3889,16 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. return _.extend(el, {index:i}); })}); }); - }).bind('tagClick', function(e, tag, value, callback) { - var pop = new instance.web.form.FormOpenPopup(self.view); - pop.show_element( - self.field.relation, - value.id, - self.build_context(), - { - title: _t("Open: ") + (self.string || self.name) - } - ); - pop.on_write_completed.add_last(function() { - self.render_value(); - }); }).bind('hideDropdown', function() { self._drop_shown = false; - }).bind('hideDropdown', function() { + }).bind('showDropdown', function() { self._drop_shown = true; }); self.tags = self.$text.textext()[0].tags(); - $("textarea", this.$element).focusout(function() { - $("textarea", this.$element).val(""); + $("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(); } }); @@ -3172,36 +3914,31 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(_. var tmp = [commands.replace_with(this.get("value"))]; return tmp; }, + get_search_blacklist: function() { + return this.get("value"); + }, render_value: function() { var self = this; - var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context()); + var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.build_context()); + var values = self.get("value") var handle_names = function(data) { + if (self.isDestroyed()) + return; 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})); - $(".oe_form_field_many2manytags_box", self.$element).click(function() { - var index = Number($(this).data("index")); - self.do_action({ - type: 'ir.actions.act_window', - res_model: self.field.relation, - res_id: self.get("value")[index], - context: self.build_context(), - views: [[false, 'form']], - target: 'current' - }); - }); + 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([]); } @@ -3209,11 +3946,14 @@ 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 - */ +/** + widget options: + - reload_on_button: Reload the whole form view if click on a button in a list view. + If you see this options, do not use it, it's basically a dirty hack to make one + precise o2m to behave the way we want. +*/ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ multi_selection: false, disable_utility_classes: true, @@ -3221,32 +3961,31 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ this._super(field_manager, node); this.is_loaded = $.Deferred(); this.initial_is_loaded = this.is_loaded; - this.is_setted = $.Deferred(); + this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation); + this.dataset.m2m = this; + var self = this; + this.dataset.on('unlink', self, function(ids) { + self.dataset_changed(); + }); + this.set_value([]); }, start: function() { - this._super.apply(this, arguments); + this.$el.addClass('oe_form_field oe_form_field_many2many'); var self = this; - this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation); - this.dataset.m2m = this; - this.dataset.on_unlink.add_last(function(ids) { - self.dataset_changed(); - }); - - this.is_setted.then(function() { - self.load_view(); - }); + self.load_view(); this.is_loaded.then(function() { self.on("change:effective_readonly", self, function() { self.is_loaded = self.is_loaded.pipe(function() { self.list_view.destroy(); return $.when(self.load_view()).then(function() { - self.reload_content(); + self.render_value(); }); }); }); - }) + }); + this._super.apply(this, arguments); }, set_value: function(value_) { value_ = value_ || []; @@ -3254,10 +3993,12 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ value_ = value_[0][2]; } this._super(value_); - this.dataset.set_ids(value_); - var self = this; - self.reload_content(); - this.is_setted.resolve(); + }, + get_value: function() { + return [commands.replace_with(this.get('value'))]; + }, + is_false: function () { + return _(this.get("value")).isEmpty(); }, load_view: function() { var self = this; @@ -3265,6 +4006,9 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ '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) { @@ -3272,23 +4016,24 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ } this.list_view.m2m_field = this; var loaded = $.Deferred(); - this.list_view.on_loaded.add_last(function() { + this.list_view.on("list_view_loaded", self, function() { self.initial_is_loaded.resolve(); loaded.resolve(); }); $.async_when().then(function () { - self.list_view.appendTo(self.$element); + self.list_view.appendTo(self.$el); }); return loaded; }, - reload_content: function() { + render_value: function() { var self = this; + this.dataset.set_ids(this.get("value")); this.is_loaded = this.is_loaded.pipe(function() { return self.list_view.reload_content(); }); }, dataset_changed: function() { - this.set({'value': [commands.replace_with(this.dataset.ids)]}); + this.internal_set_value(this.dataset.ids); }, }); @@ -3309,39 +4054,257 @@ 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() ); var self = this; - pop.on_select_elements.add(function(element_ids) { - _.each(element_ids, function(one_id) { - if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) { - self.dataset.set_ids([].concat(self.dataset.ids, [one_id])); + pop.on("elements_selected", self, function(element_ids) { + var reload = false; + _(element_ids).each(function (id) { + if(! _.detect(self.dataset.ids, function(x) {return x == id;})) { + self.dataset.set_ids(self.dataset.ids.concat([id])); self.m2m_field.dataset_changed(); - self.reload_content(); + reload = true; } }); + if (reload) { + self.reload_content(); + } }); }, do_activate_record: function(index, id) { 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() { - self.reload_content(); + pop.on('write_completed', self, self.reload_content); + }, + do_button_action: function(name, id, callback) { + var self = this; + var _sup = _.bind(this._super, this); + if (! this.m2m_field.options.reload_on_button) { + return _sup(name, id, callback); + } else { + return this.m2m_field.view.save().pipe(function() { + return _sup(name, id, function() { + self.m2m_field.view.reload(); + }); + }); + } + }, +}); + +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); + instance.web.form.CompletionFieldMixin.init.call(this); + m2m_kanban_lazy_init(); + this.is_loaded = $.Deferred(); + this.initial_is_loaded = this.is_loaded; + + var self = this; + this.dataset = new instance.web.form.Many2ManyDataSet(this, this.field.relation); + this.dataset.m2m = this; + this.dataset.on('unlink', self, function(ids) { + self.dataset_changed(); }); - } + }, + start: function() { + this._super.apply(this, arguments); + + var self = this; + + self.load_view(); + this.is_loaded.then(function() { + self.on("change:effective_readonly", self, function() { + self.is_loaded = self.is_loaded.pipe(function() { + self.kanban_view.destroy(); + return $.when(self.load_view()).then(function() { + self.render_value(); + }); + }); + }); + }) + }, + set_value: function(value_) { + value_ = value_ || []; + if (value_.length >= 1 && value_[0] instanceof Array) { + value_ = value_[0][2]; + } + this._super(value_); + }, + get_value: function() { + return [commands.replace_with(this.get('value'))]; + }, + load_view: function() { + var self = this; + this.kanban_view = new instance.web.form.Many2ManyKanbanView(this, this.dataset, false, { + 'create_text': _t("Add"), + 'creatable': self.get("effective_readonly") ? false : true, + 'quick_creatable': self.get("effective_readonly") ? false : true, + 'read_only_mode': self.get("effective_readonly") ? true : false, + 'confirm_on_delete': false, + }); + var embedded = (this.field.views || {}).kanban; + if (embedded) { + this.kanban_view.set_embedded_view(embedded); + } + this.kanban_view.m2m = this; + var loaded = $.Deferred(); + this.kanban_view.on("kanban_view_loaded",self,function() { + self.initial_is_loaded.resolve(); + loaded.resolve(); + }); + this.kanban_view.on('switch_mode', this, this.open_popup); + $.async_when().then(function () { + self.kanban_view.appendTo(self.$el); + }); + return loaded; + }, + render_value: function() { + var self = this; + this.dataset.set_ids(this.get("value")); + this.is_loaded = this.is_loaded.pipe(function() { + return self.kanban_view.do_search(self.build_domain(), self.dataset.get_context(), []); + }); + }, + dataset_changed: function() { + this.set({'value': this.dataset.ids}); + }, + open_popup: function(type, unused) { + if (type !== "form") + return; + var self = this; + if (this.dataset.index === null) { + var pop = new instance.web.form.SelectCreatePopup(this); + pop.select_element( + this.field.relation, + { + title: _t("Add: ") + this.string + }, + new instance.web.CompoundDomain(this.build_domain(), ["!", ["id", "in", this.dataset.ids]]), + this.build_context() + ); + pop.on("elements_selected", self, function(element_ids) { + _.each(element_ids, function(one_id) { + if(! _.detect(self.dataset.ids, function(x) {return x == one_id;})) { + self.dataset.set_ids([].concat(self.dataset.ids, [one_id])); + self.dataset_changed(); + self.render_value(); + } + }); + }); + } else { + var id = self.dataset.ids[self.dataset.index]; + var pop = new instance.web.form.FormOpenPopup(this); + pop.show_element(self.field.relation, id, self.build_context(), { + title: _t("Open: ") + self.string, + write_function: function(id, data, options) { + return self.dataset.write(id, data, {}).then(function() { + self.render_value(); + }); + }, + alternative_form_view: self.field.views ? self.field.views["form"] : undefined, + parent_view: self.view, + child_name: self.name, + readonly: self.get("effective_readonly") + }); + } + }, + add_id: function(id) { + this.quick_create.add_id(id); + }, +}); + +function m2m_kanban_lazy_init() { +if (instance.web.form.Many2ManyKanbanView) + return; +instance.web.form.Many2ManyKanbanView = instance.web_kanban.KanbanView.extend({ + quick_create_class: 'instance.web.form.Many2ManyQuickCreate', + _is_quick_create_enabled: function() { + return this._super() && ! this.group_by; + }, +}); +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. + */ + init: function(parent, dataset, context, buttons) { + this._super(parent); + this.m2m = this.getParent().view.m2m; + this.m2m.quick_create = this; + this._dataset = dataset; + this._buttons = buttons || false; + this._context = context || {}; + }, + start: function () { + var self = this; + self.$text = this.$el.find('input').css("width", "200px"); + self.$text.textext({ + plugins : 'arrow autocomplete', + autocomplete: { + render: function(suggestion) { + return $('<span class="text-label"/>'). + data('index', suggestion['index']).html(suggestion['label']); + } + }, + ext: { + autocomplete: { + selectFromDropdown: function() { + $(this).trigger('hideDropdown'); + var index = Number(this.selectedSuggestionElement().children().children().data('index')); + var data = self.search_result[index]; + if (data.id) { + self.add_id(data.id); + } else { + data.action(); + } + }, + }, + itemManager: { + itemToString: function(item) { + return item.name; + }, + }, + }, + }).bind('getSuggestions', function(e, data) { + var _this = this; + var str = !!data ? data.query || '' : ''; + self.m2m.get_search_result(str).then(function(result) { + self.search_result = result; + $(_this).trigger('setSuggestions', {result : _.map(result, function(el, i) { + return _.extend(el, {index:i}); + })}); + }); + }); + self.$text.focusout(function() { + self.$text.val(""); + }); + }, + focus: function() { + this.$text.focus(); + }, + add_id: function(id) { + var self = this; + self.$text.val(""); + self.trigger('added', id); + this.m2m.dataset_changed(); + }, }); +} /** * 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: @@ -3371,12 +4334,15 @@ instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ this.dataset.create_function = function(data, sup) { var fct = self.options.create_function || sup; return fct.call(this, data).then(function(r) { - self.created_elements.push(r.result); + self.trigger('create_completed saved', r); + self.created_elements.push(r); }); }; this.dataset.write_function = function(id, data, options, sup) { var fct = self.options.write_function || sup; - return fct.call(this, id, data, options).then(self.on_write_completed); + return fct.call(this, id, data, options).then(function() { + self.trigger('write_completed saved'); + }); }; this.dataset.parent_view = this.options.parent_view; this.dataset.child_name = this.options.child_name; @@ -3384,17 +4350,17 @@ 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(); + self.check_exit(true); }, title: this.options.title || "", - }, this.$element).open(); + }, this.$el).open(); + this.$buttonpane = dialog.$buttons; this.start(); }, - on_write_completed: function() {}, setup_form_view: function() { var self = this; if (this.row_id) { @@ -3407,53 +4373,57 @@ 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.on_loaded.add_last(function() { - var $buttons = self.view_form.$element.find(".oe_form_buttons"); + this.view_form.appendTo(this.$el.find(".oe_popup_form")); + this.view_form.on("form_view_loaded", self, function() { 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() { + $.when(self.view_form.save()).then(function() { self.view_form.reload_mutex.exec(function() { self.view_form.on_button_new(); }); }); }); - 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() { + $.when(self.view_form.save()).then(function() { self.view_form.reload_mutex.exec(function() { self.check_exit(); }); }); }); - 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(); }); }, - on_select_elements: function(element_ids) { + select_elements: function(element_ids) { + this.trigger("elements_selected", element_ids); }, - check_exit: function() { + check_exit: function(no_destroy) { if (this.created_elements.length > 0) { - this.on_select_elements(this.created_elements); + this.select_elements(this.created_elements); + this.created_elements = []; } + this.trigger('closed'); this.destroy(); }, destroy: function () { - this.$element.dialog('close'); + this.trigger('closed'); + this.$el.dialog('close'); this._super(); }, }); @@ -3503,7 +4473,7 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend self.rpc('/web/session/eval_domain_and_context', { domains: [], contexts: [this.context] - }, function (results) { + }).then(function (results) { var search_defaults = {}; _.each(results.context, function (value_, key) { var match = /^search_default_(.*)$/.exec(key); @@ -3524,7 +4494,7 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend } this.searchview = new instance.web.SearchView(this, this.dataset, false, search_defaults); - this.searchview.on_search.add(function(domains, contexts, groupbys) { + this.searchview.on('search_data', self, function(domains, contexts, groupbys) { if (self.initial_ids) { self.do_search(domains.concat([[["id", "in", self.initial_ids]], self.domain]), contexts, groupbys); @@ -3533,36 +4503,41 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend self.do_search(domains.concat([self.domain]), contexts.concat(self.context), groupbys); } }); - this.searchview.on_loaded.add_last(function () { + this.searchview.on("search_view_loaded", self, function() { self.view_list = new instance.web.form.SelectCreateListView(self, self.dataset, false, _.extend({'deletable': false, - 'selectable': !self.options.disable_multiple_selection + 'selectable': !self.options.disable_multiple_selection, + '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.view_list.on("list_view_loaded", self, function() { + 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.select_elements(self.selected_ids); self.destroy(); }); + var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-create"); + $cbutton.click(function() { + self.new_object(); + }); }); }); - this.searchview.appendTo($(".oe-select-create-popup-view-list", self.$element)); + this.searchview.appendTo($(".oe_popup_search", self.$el)); }, do_search: function(domains, contexts, groupbys) { var self = this; @@ -3570,16 +4545,17 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend domains: domains || [], contexts: contexts || [], group_by_seq: groupbys || [] - }, function (results) { + }).then(function (results) { self.view_list.do_search(results.domain, results.context, results.group_by); }); }, 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() { @@ -3587,7 +4563,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(); }, @@ -3598,7 +4574,7 @@ instance.web.form.SelectCreateListView = instance.web.ListView.extend({ this.popup.new_object(); }, select_record: function(index) { - this.popup.on_select_elements([this.dataset.ids[index]]); + this.popup.select_elements([this.dataset.ids[index]]); this.popup.destroy(); }, do_select: function(ids, records) { @@ -3607,110 +4583,110 @@ 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); this.reference_ready = true; }, - on_nop: function() { - }, - on_selection_changed: function() { - if (this.reference_ready) { - var sel = this.selection.get_value(); - this.m2o.field.relation = sel; - this.m2o.set_value(false); - this.m2o.$element.toggle(sel !== false); - } - }, destroy_content: function() { - if (this.selection) { - this.selection.destroy(); - this.selection = undefined; - } - if (this.m2o) { - this.m2o.destroy(); - this.m2o = undefined; + if (this.fm) { + this.fm.destroy(); + this.fm = undefined; } }, initialize_content: function() { - this.selection = new instance.web.form.FieldSelection(this, { attrs: { - name: 'selection' + var self = this; + var fm = new instance.web.form.DefaultFieldManager(this); + this.fm = fm; + fm.extend_field_desc({ + "selection": { + selection: this.field_manager.get_field_desc(this.name).selection, + type: "selection", + }, + "m2o": { + relation: null, + type: "many2one", + }, + }); + this.selection = new instance.web.form.FieldSelection(fm, { attrs: { + 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.renderElement(); - this.selection.start(); + this.selection.appendTo(this.$(".oe_form_view_reference_selection")); + 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' + this.m2o = new instance.web.form.FieldMany2One(fm, { attrs: { + 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.renderElement(); - this.m2o.start(); + this.m2o.appendTo(this.$(".oe_form_view_reference_m2o")); + 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'; + on_selection_changed: function() { + if (this.reference_ready) { + this.internal_set_value([this.selection.get_value(), false]); + this.render_value(); + } }, - set_value: function(value_) { - this._super(value_); - this.render_value(); + data_changed: function() { + if (this.reference_ready) { + this.internal_set_value([this.selection.get_value(), this.m2o.get_value()]); + } + }, + set_value: function(val) { + if (val) { + val = val.split(','); + val[0] = val[0] || false; + val[1] = val[0] ? (val[1] ? parseInt(val[1], 10) : val[1]) : false; + } + this._super(val || [false, false]); + }, + get_value: function() { + return this.get('value')[0] && this.get('value')[1] ? (this.get('value')[0] + ',' + this.get('value')[1]) : false; }, render_value: function() { this.reference_ready = false; - var vals = [], sel_val, m2o_val; - if (typeof(this.get('value')) === 'string') { - vals = this.get('value').split(','); - } - sel_val = vals[0] || false; - m2o_val = vals[1] ? parseInt(vals[1], 10) : vals[1]; if (!this.get("effective_readonly")) { - this.selection.set_value(sel_val); + this.selection.set_value(this.get('value')[0]); } - this.m2o.field.relation = sel_val; - this.m2o.set_value(m2o_val); + this.m2o.field.relation = this.get('value')[0]; + this.m2o.set_value(this.get('value')[1]); + this.m2o.$el.toggle(!!this.get('value')[0]); this.reference_ready = true; }, - data_changed: function() { - var model = this.selection.get_value(), - id = this.m2o.get_value(); - if (typeof(model) === 'string' && typeof(id) === 'number') { - this.set({'value': model + ',' + id}); - } else { - this.set({'value': false}); - } - }, - get_field: function(name) { - if (name === "selection") { - return { - selection: this.view.fields_view.fields[this.name].selection, - type: "selection", - }; - } else if (name === "m2o") { - return { - relation: null, - type: "many2one", - }; - } - 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.useFileAPI = !!window.FileReader; + if (!this.useFileAPI) { + 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() { + if (!this.useFileAPI) { + $(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']; @@ -3722,54 +4698,84 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(_.extend( return size.toFixed(2) + ' ' + units[i]; }, on_file_change: function(e) { - // 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(); + var self = this; + var file_node = e.target; + if ((this.useFileAPI && file_node.files.length) || (!this.useFileAPI && $(file_node).val() !== '')) { + if (this.useFileAPI) { + var file = file_node.files[0]; + var filereader = new FileReader(); + filereader.readAsDataURL(file); + filereader.onloadend = function(upload) { + var data = upload.target.result; + data = data.split(',')[1]; + self.on_file_uploaded(file.size, file.name, file.type, data); + }; + } else { + 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 (filename) { + var tmp = {}; + tmp[filename] = value; + this.field_manager.set_values(tmp); + } }, on_clear: function() { if (this.get('value') !== false) { this.binary_value = false; - this.set({'value': false}); + this.internal_set_value(false); } return false; } -})); +}); instance.web.form.FieldBinaryFile = instance.web.form.FieldBinary.extend({ template: 'FieldBinaryFile', @@ -3777,18 +4783,14 @@ 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(ev) { if (self.get('value')) { - self.on_save_as(); + self.on_save_as(ev); } return false; }); } }, - set_value: function(value_) { - this._super.apply(this, arguments); - this.render_value(); - }, render_value: function() { if (!this.get("effective_readonly")) { var show_value; @@ -3797,146 +4799,334 @@ 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); + var show_value = _t("Download") + if (this.view) + show_value += " " + (this.view.datarecord[this.node.attrs.filename] || ''); + this.$el.find('a').text(show_value); } } }, on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { this.binary_value = true; - this.set({'value': file_base64}); + this.internal_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_) { - var filename = this.node.attrs.filename; - if (this.view.fields[filename]) { - this.view.fields[filename].set({value: value_}); - } - }, 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(); - this.$placeholder = $(".oe_form_field-binary-image-placeholder", this.$element); - if (!this.get("effective_readonly")) - this.$element.find('.oe-binary').show(); - else - this.$element.find('.oe-binary').hide(); - }, - set_value: function(value_) { - this._super.apply(this, arguments); - this.render_value(); - }, + placeholder: "/web/static/src/img/placeholder.png", 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"; + url = this.placeholder; } - var rendered = QWeb.render("FieldBinaryImage-img", {widget: this, url: url});; - this.$placeholder.html(rendered); + 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"); + }); + $img.on('error', function() { + $img.attr('src', self.placeholder); + instance.webclient.notification.warn(_t("Image"), _t("Could not display the selected image.")); + }); }, on_file_change: function() { this.render_value(); this._super.apply(this, arguments); }, on_file_uploaded_and_valid: function(size, name, content_type, file_base64) { - this.set({'value': file_base64}); + this.internal_set_value(file_base64); this.binary_value = true; this.render_value(); + this.set_filename(name); }, on_clear: function() { this._super.apply(this, arguments); this.render_value(); + this.set_filename(''); } }); +/** + * Widget for (one2many field) to upload one or more file in same time and display in list. + * The user can delete his files. + * Options on attribute ; "blockui" {Boolean} block the UI or not + * during the file is uploading + */ +instance.web.form.FieldOne2ManyBinaryMultiFiles = instance.web.form.AbstractField.extend({ + template: "FieldBinaryFileUploader", + init: function(field_manager, node) { + this._super(field_manager, node); + this.field_manager = field_manager; + this.node = node; + if(this.field.type != "one2many" || this.field.relation != 'ir.attachment') { + throw "The type of the field '"+this.field.string+"' must be a one2many field with a relation to 'ir.attachment' model."; + } + this.ds_file = new instance.web.DataSetSearch(this, 'ir.attachment'); + this.fileupload_id = _.uniqueId('oe_fileupload_temp'); + $(window).on(this.fileupload_id, _.bind(this.on_file_loaded, this)); + }, + start: function() { + this._super(this); + this.$el.on('change', 'input.oe_form_binary_file', this.on_file_change ); + }, + get_value: function() { + return _.map(this.get('value'), function (value) { return commands.link_to( value.id ); }); + }, + get_file_url: function (attachment) { + return instance.origin + '/web/binary/saveas?session_id=' + this.session.session_id + '&model=ir.attachment&field=datas&filename_field=datas_fname&id=' + attachment['id']; + }, + render_value: function () { + var render = $(instance.web.qweb.render('FieldBinaryFileUploader.files', {'widget': this})); + render.on('click', '.oe_delete', _.bind(this.on_file_delete, this)); + this.$('.oe_placeholder_files, .oe_attachments').replaceWith( render ); + + // reinit input type file + var $input = this.$('input.oe_form_binary_file'); + $input.after($input.clone(true)).remove(); + this.$(".oe_fileupload").show(); + }, + on_file_change: function (event) { + event.stopPropagation(); + var self = this; + var $target = $(event.target); + if ($target.val() !== '') { + + var filename = $target.val().replace(/.*[\\\/]/,''); + + // if the files is currently uploded, don't send again + if( !isNaN(_.find(this.get('value'), function (file) { return (file.filename || file.name) == filename && file.upload; } )) ) { + return false; + } + + // block UI or not + if(this.node.attrs.blockui) { + instance.web.blockUI(); + } + + // if the files exits for this answer, delete the file before upload + var files = _.filter(this.get('value'), function (file) { + if((file.filename || file.name) == filename) { + self.ds_file.unlink([file.id]); + return false; + } else { + return true; + } + }); + + // TODO : unactivate send on wizard and form + + // submit file + this.$('form.oe_form_binary_form').submit(); + this.$(".oe_fileupload").hide(); + + // add file on result + files.push({ + 'id': 0, + 'name': filename, + 'filename': filename, + 'url': '', + 'upload': true + }); + + this.set({'value': files}); + } + }, + on_file_loaded: function (event, result) { + // unblock UI + if(this.node.attrs.blockui) { + instance.web.unblockUI(); + } + + // TODO : activate send on wizard and form + + var files = this.get('value'); + for(var i in files){ + if(files[i].filename == result.filename && files[i].upload) { + files[i] = { + 'id': result.id, + 'name': result.name, + 'filename': result.filename, + 'url': this.get_file_url(result) + }; + } + } + + this.set({'value': files}); + this.render_value() + }, + on_file_delete: function (event) { + event.stopPropagation(); + var file_id=$(event.target).data("id"); + if (file_id) { + var files=[]; + for(var i in this.get('value')){ + if(file_id != this.get('value')[i].id){ + files.push(this.get('value')[i]); + } + else { + this.ds_file.unlink([file_id]); + } + } + this.set({'value': files}); + } + }, +}); + instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ - template: "EmptyComponent", + 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.set({value: false}); + }, start: function() { + if (this.options.clickable) { + this.$el.on('click','li',this.on_click_stage); + } + if (this.$el.parent().is('header')) { + this.$el.after('<div class="oe_clear"/>'); + } this._super(); - this.selected_value = null; - - this.render_list(); }, set_value: function(value_) { + if (value_ instanceof Array) { + value_ = value_[0]; + } this._super(value_); - this.selected_value = value_; - - this.render_list(); }, - render_list: function() { + render_value: 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.field.selection; + self.get_selection().then(function() { + var content = QWeb.render("FieldStatus.content", {widget: self}); + self.$el.html(content); + var colors = JSON.parse((self.node.attrs || {}).statusbar_colors || "{}"); + var color = colors[self.get('value')]; + if (color) { + self.$("oe_active").css("color", color); + } + }); + }, + /** 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 + */ + get_selection: function() { + var self = this; + self.selection = []; + if (this.field.type == "many2one") { + var domain = []; + if(!_.isEmpty(this.field.domain) || !_.isEmpty(this.node.attrs.domain)) { + domain = new instance.web.CompoundDomain(['|'], self.build_domain(), [['id', '=', self.get('value')]]); + } + var ds = new instance.web.DataSetSearch(this, this.field.relation, self.build_context(), domain); + return ds.read_slice(['name'], {}).pipe(function (records) { + for(var i = 0; i < records.length; i++) { + self.selection.push([records[i].id, records[i].name]); + } + }); } else { - this.to_show = _.select(this.field.selection, function(x) { - return _.indexOf(shown, x[0]) !== -1 || x[0] === self.selected_value; + // 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.get('value') || !this.options.visible || this.options.visible.indexOf(key) != -1) { + this.selection.push(selection[i]); + } + } + return $.when(); + } + }, + on_click_stage: function (ev) { + var self = this; + var $li = $(ev.currentTarget); + var val = parseInt($li.data("id")); + if (val != self.get('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(); + }); }); } + }, +}); - var content = instance.web.qweb.render("FieldStatus.content", {widget: this, _:_}); - this.$element.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-arrow-list-selected span"); - elem.css("border-color", color); - if (this.check_white(color)) - elem.css("color", "white"); - elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-before"); - elem.css("border-left-color", "rgba(0,0,0,0)"); - elem = this.$element.find("li.oe-arrow-list-selected .oe-arrow-list-after"); - elem.css("border-color", "rgba(0,0,0,0)"); - elem.css("border-left-color", color); - } - }, - check_white: function(color) { - var div = $("<div></div>"); - div.css("display", "none"); - div.css("color", color); - div.appendTo($("body")); - var ncolor = div.css("color"); - div.remove(); - var res = /^\s*rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/.exec(ncolor); - if (!res) { - return false; +instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({ + template: "FieldMonetary", + init: function() { + this._super.apply(this, arguments); + this.set({"currency": false}); + if (this.options.currency_field) { + this.field_manager.on("field_changed:" + this.options.currency_field, this, function() { + this.set({"currency": this.field_manager.get_field_value(this.options.currency_field)}); + }); } - var comps = [parseInt(res[1]), parseInt(res[2]), parseInt(res[3])]; - var lum = comps[0] * 0.3 + comps[1] * 0.59 + comps[1] * 0.11; - if (lum < 128) { - return true; + this.on("change:currency", this, this.get_currency_info); + this.get_currency_info(); + this.ci_dm = new instance.web.DropMisordered(); + }, + start: function() { + var tmp = this._super(); + this.on("change:currency_info", this, this.reinitialize); + return tmp; + }, + get_currency_info: function() { + var self = this; + if (this.get("currency") === false) { + this.set({"currency_info": null}); + return; } - return false; - } + return this.ci_dm.add(new instance.web.Model("res.currency").query(["symbol", "position"]) + .filter([["id", "=", self.get("currency")]]).first()).pipe(function(res) { + self.set({"currency_info": res}); + }); + }, + parse_value: function(val, def) { + return instance.web.parse_value(val, {type: "float"}, def); + }, + format_value: function(val, def) { + return instance.web.format_value(val, {type: "float"}, def); + }, }); /** * 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({ @@ -3945,12 +5135,14 @@ 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', 'many2one' : 'instance.web.form.FieldMany2One', 'many2many' : 'instance.web.form.FieldMany2Many', - 'many2manytags' : 'instance.web.form.FieldMany2ManyTags', + 'many2many_tags' : 'instance.web.form.FieldMany2ManyTags', + 'many2many_kanban' : 'instance.web.form.FieldMany2ManyKanban', 'one2many' : 'instance.web.form.FieldOne2Many', 'one2many_list' : 'instance.web.form.FieldOne2Many', 'reference' : 'instance.web.form.FieldReference', @@ -3961,7 +5153,9 @@ instance.web.form.widgets = new instance.web.Registry({ 'progressbar': 'instance.web.form.FieldProgressBar', 'image': 'instance.web.form.FieldBinaryImage', 'binary': 'instance.web.form.FieldBinaryFile', - 'statusbar': 'instance.web.form.FieldStatus' + 'one2many_binary': 'instance.web.form.FieldOne2ManyBinaryMultiFiles', + 'statusbar': 'instance.web.form.FieldStatus', + 'monetary': 'instance.web.form.FieldMonetary', }); /** @@ -3974,6 +5168,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 67c8ce0239e..ab50ae933d9 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -22,6 +22,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi 'reorderable': true, 'action_buttons': true, }, + view_type: 'tree', /** * Core class for list-type displays. * @@ -61,12 +62,12 @@ 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); + this.groups.datagroup = new StaticDataGroup(this.dataset); } else { - this.groups.datagroup = new instance.web.DataGroup( + this.groups.datagroup = new DataGroup( this, this.model, dataset.get_domain(), dataset.get_context()); @@ -83,7 +84,17 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }); this.no_leaf = false; + this.grouped = false; + this.on('view_loaded', self, self.load_list); + }, + 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) * @@ -129,14 +140,14 @@ 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'); - return this.reload_view(null, null, true); + this.$el.addClass('oe_list'); + return this._super(); }, /** * Returns the style for the provided record in the current view (from the @@ -210,7 +221,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi * @param {Object} data.fields_view.arch current list view descriptor * @param {Boolean} grouped Is the list view grouped */ - on_loaded: function(data, grouped) { + load_list: function(data) { var self = this; this.fields_view = data; this.name = "" + this.fields_view.arch.attrs.string; @@ -236,21 +247,23 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }).value(); } - this.setup_columns(this.fields_view.fields, grouped); + this.setup_columns(this.fields_view.fields, this.grouped); + + this.$el.html(QWeb.render(this._template, this)); + this.$el.addClass(this.fields_view.arch.attrs['class']); - this.$element.html(QWeb.render(this._template, this)); // 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')); @@ -259,26 +272,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', this.grouped); } // Pager @@ -287,7 +296,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 @@ -311,7 +320,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); @@ -329,6 +338,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'); }); @@ -338,11 +349,14 @@ 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('Delete'), callback: this.do_delete_selected }, - ]); + this.sidebar.add_items('other', _.compact([ + { label: _t("Export"), callback: this.on_sidebar_export }, + 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(); } + this.trigger('list_view_loaded', data, this.grouped); }, /** * Configures the ListView pager based on the provided dataset's information @@ -359,17 +373,23 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi } var total = dataset.size(); + var limit = this.limit() || total; + if (total == 0) + this.$pager.hide(); + else + this.$pager.css("display", ""); + 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 @@ -380,68 +400,23 @@ 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; 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; + 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'; }); - 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 @@ -467,9 +442,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(); } @@ -479,7 +451,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(); @@ -492,25 +464,12 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi /** * Reloads the list view based on the current settings (dataset & al) * + * @deprecated * @param {Boolean} [grouped] Should the list be displayed grouped * @param {Object} [context] context to send the server while loading the view */ reload_view: function (grouped, context, initial) { - var self = this; - var callback = function (field_view_get) { - self.on_loaded(field_view_get, grouped); - }; - if (this.embedded_view) { - return $.Deferred().then(callback).resolve(this.embedded_view); - } else { - return this.rpc('/web/listview/load', { - model: this.model, - view_id: this.view_id, - view_type: "tree", - context: this.dataset.get_context(context), - toolbar: !!this.options.$sidebar - }, callback); - } + return this.load_view(context); }, /** * re-renders the content of the list view @@ -519,10 +478,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; @@ -543,6 +502,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) { @@ -570,7 +543,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi */ do_search: function (domain, context, group_by) { this.page = 0; - this.groups.datagroup = new instance.web.DataGroup( + this.groups.datagroup = new DataGroup( this, this.model, domain, context, group_by); this.groups.datagroup.sort = this.dataset._sort; @@ -578,8 +551,9 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi group_by = null; } this.no_leaf = !!context['group_by_no_leaf']; + this.grouped = !!group_by; - this.reload_view(!!group_by, context).then( + return this.load_view(context).pipe( this.proxy('reload_content')); }, /** @@ -610,7 +584,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; @@ -618,7 +592,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) { @@ -633,6 +607,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; }); @@ -651,7 +642,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) @@ -760,16 +752,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() { @@ -788,7 +778,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) { @@ -797,10 +787,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 { @@ -811,27 +801,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# */{ @@ -877,52 +869,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'), @@ -933,19 +936,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); } @@ -967,13 +970,13 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. // set) a human-readable version. m2o does not have this issue // because the non-human-readable is just a number, where the // human-readable version is a pair - if (value && (ref_match = /([\w\.]+),(\d+)/.exec(value))) { + if (value && (ref_match = /^([\w\.]+),(\d+)$/.exec(value))) { // reference values are in the shape "$model,$id" (as a // string), we need to split and name_get this pair in order // 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]); }); @@ -989,29 +992,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 || @@ -1020,20 +1040,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>'); @@ -1042,19 +1062,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')); @@ -1081,8 +1100,6 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. }, this); if (!this.$current) { return; } this.$current.remove(); - this.$current = null; - this.$_element.remove(); }, get_records: function () { return this.records.map(function (record) { @@ -1096,17 +1113,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 @@ -1115,6 +1122,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, @@ -1123,26 +1131,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); } }); } }); @@ -1150,7 +1139,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we passtrough_events: 'action deleted row_link', /** * Grouped display for the ListView. Handles basic DOM events and interacts - * with the :js:class:`~instance.web.DataGroup` bound to it. + * with the :js:class:`~DataGroup` bound to it. * * Provides events similar to those of * :js:class:`~instance.web.ListView.List` @@ -1230,8 +1219,9 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we .replaceWith(self.render()); }); this.$row.children().last() + .addClass('oe_list_group_pagination') .append($prev) - .append('<span class="oe-pager-state"></span>') + .append('<span class="oe_list_pager_state"></span>') .append($next); }, open: function (point_insertion) { @@ -1269,7 +1259,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 @@ -1277,7 +1267,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')) { @@ -1297,22 +1287,32 @@ 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 = _.str.escapeHTML(row_data[group_column.id].value); } + // group_label is html-clean (through format or explicit + // escaping if format failed), can inject straight into HTML + $group_column.html(_.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;">'); @@ -1324,30 +1324,27 @@ 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 } else if (column.id in group.aggregates) { var r = {}; r[column.id] = {value: group.aggregates[column.id]}; - $('<td class="oe-number">') - .html(instance.web.format_cell( - r, column, {process_modifiers: false})) + $('<td class="oe_number">') + .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; @@ -1372,7 +1369,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, @@ -1398,21 +1395,23 @@ 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 })) .end() .find('button[data-pager-action=previous]') - .attr('disabled', page === 0) + .css('visibility', + page === 0 ? 'hidden' : '') .end() .find('button[data-pager-action=next]') - .attr('disabled', page === pages - 1); + .css('visibility', + page === pages - 1 ? 'hidden' : ''); } } @@ -1427,18 +1426,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'), @@ -1456,7 +1468,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 @@ -1466,39 +1478,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 @@ -1537,6 +1549,60 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we } }); +var DataGroup = instance.web.CallbackEnabled.extend({ + init: function(parent, model, domain, context, group_by, level) { + this._super(parent, null); + this.model = new instance.web.Model(model, context, domain); + this.group_by = group_by; + this.context = context; + this.domain = domain; + + this.level = level || 0; + }, + list: function (fields, ifGroups, ifRecords) { + var self = this; + 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; + } + // internal node + var child_datagroups = _(querygroups).map(function (group) { + var child_context = _.extend( + {}, self.model.context(), group.model.context()); + var child_dg = new 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); + }); + } +}); +var StaticDataGroup = DataGroup.extend({ + init: function (dataset) { + this.dataset = dataset; + }, + list: function (fields, ifGroups, ifRecords) { + ifRecords(this.dataset); + } +}); + /** * @mixin Events */ @@ -1802,7 +1868,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); @@ -1818,13 +1884,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)) { @@ -1849,6 +1944,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); @@ -1856,6 +1991,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 c918012e60d..021e5549c18 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,26 +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 = ( - (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); }, /** @@ -69,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) { + load_list: function (data, grouped) { + var self = this; // tree/@editable takes priority on everything else if present. - this.options.editable = 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(name, id, callback); + }); }, /** * Ensures the editable list is saved (saves any pending edition if @@ -91,337 +162,639 @@ 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')); + + return this.ensure_saved().pipe(function () { + var $recordRow = self.groups.get_row_for(record); + var cells = self.get_cells_for($recordRow); + 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) { + 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(); + + field.set_dimensions($cell.outerHeight(), $cell.outerWidth()); + field.$el.position({ + my: 'left top', + at: 'left top', + of: $cell }); - 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; + _.each(this.editor.form.fields, function(field, field_name) { + var setting = false; + var set_invisible = function() { + if (!setting && field.get("effective_readonly")) { + setting = true; + field.set({invisible: true}); + setting = false; + } + }; + field.on("change:effective_readonly", self, set_invisible); + field.on("change:invisible", self, set_invisible); + set_invisible(); + }); + + this.editor.$el.on('keyup keydown', function (e) { + if (!self.editor.is_editing()) { return true; } + 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 true; } + var method = e.type + '_' + key.name; + if (!(method in self)) { return true; } + return self[method](e); }); }, /** - * Saves the current row, and returns a Deferred resolving to an object - * with the following properties: - * - * ``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. + * Saves the current record, and goes to the next one (creation or + * edition) * - * @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(); + }, + keydown_ESCAPE: function (e) { + return false; + }, + keyup_ESCAPE: function (e) { + 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; } - //noinspection JSPotentiallyInvalidConstructorUsage - return $.Deferred().resolve().promise(); + 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) { + + } + } + // 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.trigger('load_record', _.extend({}, record)) + : form.load_defaults(); + return $.when(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; }); - $("<td><button class='oe-edit-row-save' type='button'>Save</button></td>").appendTo($element); }, + save: function () { + var self = this; + return this.form + .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'); + return 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..05760420ba8 100644 --- a/addons/web/static/src/js/view_tree.js +++ b/addons/web/static/src/js/view_tree.js @@ -45,7 +45,7 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie view_type: "tree", toolbar: this.view_manager ? !!this.view_manager.sidebar : false, context: this.dataset.get_context() - }, this.on_loaded); + }).then(this.on_loaded); }, /** * Returns the list of fields needed to correctly read objects. @@ -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 6bf7e273eab..a4fe86c516d 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -10,51 +10,208 @@ 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.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(); } - if (this.client_widget) { - this.client_widget.destroy(); - this.client_widget = null; + 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) { + var next_item = this.breadcrumbs[index + 1]; + if (next_item && next_item.on_reverse_breadcrumb) { + next_item.on_reverse_breadcrumb(this.breadcrumbs[index].widget); + } + 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; + this.inner_action = item.action; + 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.inner_action = item.action; + this.breadcrumbs.splice(index, 0, item); + return false; + } else { + item.destroy(); + } + } + } + var last_widget = this.breadcrumbs.slice(-1)[0]; + if (last_widget) { + this.inner_widget = last_widget.widget; + this.inner_action = last_widget.action; + } + }, + 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; + if (this.inner_action._push_me === false) { + // this action has been explicitly marked as not pushable + return; + } + 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.menu_id) { + state['menu_id'] = this.inner_action.menu_id; + } 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; + var params = {}; + _.each(this.inner_action.params, function(v, k) { + if(_.isString(v) || _.isNumber(v)) { + params[k] = v; + } + }); + state = _.extend(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 +225,43 @@ 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, options) { + options = _.defaults(options || {}, { + clear_breadcrumbs: false, + on_reverse_breadcrumb: function() {}, + on_close: function() {}, + action_menu_id: null, + }); + if (_.isString(action) && instance.web.client_actions.contains(action)) { + var action_client = { type: "ir.actions.client", tag: action, params: {} }; + return this.do_action(action_client, options); + } 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, options); }); } 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, @@ -108,74 +270,122 @@ instance.web.ActionManager = instance.web.Widget.extend({ pager : !popup && !inline, display_title : !popup }, action.flags || {}); + action.menu_id = options.action_menu_id; 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, options); }, null_action: function() { this.dialog_stop(); - this.content_stop(); + this.clear_breadcrumbs(); }, - 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); - }; + /** + * + * @param {Object} executor + * @param {Object} executor.action original action + * @param {Function<instance.web.Widget>} executor.widget function used to fetch the widget instance + * @param {String} executor.klass CSS class to add on the dialog root, if action.target=new + * @param {Function<instance.web.Widget, undefined>} executor.post_process cleanup called after a widget has been added as inner_widget + * @param {Object} options + * @return {*} + */ + ir_actions_common: function(executor, options) { + if (this.inner_widget && executor.action.target !== 'new') { + if (this.getParent().has_uncommitted_changes()) { + return $.Deferred().reject(); + } else if (options.clear_breadcrumbs) { + this.clear_breadcrumbs(); + } } - if (action.target === 'new') { - if (this.dialog == null) { - this.dialog = new instance.web.Dialog(this, { width: '80%' }); - if(on_close) - this.dialog.on_close.add(on_close); - } else { - this.dialog_viewmanager.destroy(); + var widget = executor.widget(); + if (executor.action.target === 'new') { + if (this.dialog_widget && ! this.dialog_widget.isDestroyed()) + this.dialog_widget.destroy(); + if (this.dialog === null || this.dialog.isDestroyed()) { + this.dialog = new instance.web.Dialog(this, { + dialogClass: executor.klass, + }); + this.dialog.on("closing", null, options.on_close); + this.dialog.init_dialog(); + } + this.dialog.dialog_title = executor.action.name; + if (widget instanceof instance.web.ViewManager) { + _.extend(widget.flags, { + $buttons: this.dialog.$buttons, + footer_to_buttons: true, + }); } - 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.setParent(this.dialog); + var initialized = this.dialog_widget.appendTo(this.dialog.$el); this.dialog.open(); + return initialized; } else { this.dialog_stop(); - this.content_stop(); - if(action.menu_id) { - return this.getParent().do_action(action, function () { - instance.webclient.menu.open_menu(action.menu_id); - }); + this.inner_action = executor.action; + this.inner_widget = widget; + executor.post_process(widget); + return this.inner_widget.appendTo(this.$el); + } + }, + ir_actions_act_window: function (action, options) { + var self = this; + + return this.ir_actions_common({ + widget: function () { return new instance.web.ViewManagerAction(self, action); }, + action: action, + klass: 'oe_act_window', + post_process: function (widget) { widget.add_breadcrumb(options.on_reverse_breadcrumb); } + }, options); + }, + ir_actions_client: function (action, options) { + var self = this; + var ClientWidget = instance.web.client_actions.get_object(action.tag); + + if (!(ClientWidget.prototype instanceof instance.web.Widget)) { + var next; + if (next = ClientWidget(this, action)) { + return this.do_action(next, options); } - this.inner_action = action; - this.inner_viewmanager = new instance.web.ViewManagerAction(this, action); - this.inner_viewmanager.appendTo(this.$element); + return $.when(); } + + return this.ir_actions_common({ + widget: function () { return new ClientWidget(self, action); }, + action: action, + klass: 'oe_act_client', + post_process: function(widget) { + self.push_breadcrumb({ + widget: widget, + title: action.name, + on_reverse_breadcrumb: options.on_reverse_breadcrumb, + }); + if (action.tag !== 'reload') { + self.do_push_state({}); + } + } + }, options); }, - ir_actions_act_window_close: function (action, on_closed) { - if (!this.dialog && on_closed) { - on_closed(); + ir_actions_act_window_close: function (action, options) { + if (!this.dialog) { + options.on_close(); } this.dialog_stop(); }, - ir_actions_server: function (action, on_closed) { + ir_actions_server: function (action, options) { 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, options) }); }, - 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) { + ir_actions_report_xml: function(action, options) { var self = this; - $.blockUI(); + instance.web.blockUI(); self.rpc("/web/session/eval_domain_and_context", { contexts: [action.context], domains: [] @@ -185,10 +395,10 @@ 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(); + if (!self.dialog) { + options.on_close(); } self.dialog_stop(); }, @@ -199,15 +409,13 @@ 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({ template: "ViewManager", init: function(parent, dataset, views, flags) { this._super(parent); + this.url_states = {}; this.model = dataset ? dataset.model : undefined; this.dataset = dataset; this.searchview = null; @@ -235,8 +443,8 @@ instance.web.ViewManager = instance.web.Widget.extend({ start: function() { this._super(); var self = this; - this.$element.find('.oe_view_manager_switch a').click(function() { - self.on_mode_switch($(this).data('view-type')); + this.$el.find('.oe_view_manager_switch a').click(function() { + self.switch_mode($(this).data('view-type')); }).tipsy(); var views_ids = {}; _.each(this.views_src, function(view) { @@ -244,9 +452,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,61 +462,28 @@ 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; - return this.on_mode_switch(default_view); + return this.switch_mode(default_view); }, - /** - * Asks the view manager to switch visualization mode. - * - * @param {String} view_type type of view to display - * @param {Boolean} [no_store=false] don't store the view being switched to on the switch stack - * @returns {jQuery.Deferred} new view loading promise - */ - on_mode_switch: function(view_type, no_store) { + switch_mode: function(view_type, no_store, view_options) { 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())) { + self.trigger('switch_mode', view_type, no_store, view_options); return $.Deferred().reject(); - + } if (!no_store) { this.views_history.push(view_type); } 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,60 +494,148 @@ 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 () { + r = $.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) { - controller.do_show(); + container.show(); + controller.do_show(view_options || {}); } else { + container.hide(); controller.do_hide(); } } }); + self.trigger('switch_mode', view_type, no_store, view_options); + }); + return r; + }, + 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.on('switch_mode', self, this.switch_mode); + controller.on('previous_view', self, this.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() { + if (self.searchview + && self.flags.auto_search + && view.controller.searchable !== false) { + self.searchview.ready.then(self.searchview.do_search); + } + self.trigger("controller_inited",view_type,controller); }); - return view_promise; }, - /** - * Method used internally when a view asks to switch view. This method is meant - * to be extended by child classes to change the default behavior, which simply - * consist to switch to the asked view. - */ - switch_view: function(view_type, no_store) { - return this.on_mode_switch(view_type, no_store); + set_title: function(title) { + this.$el.find('.oe_view_title_text:first').text(title); + }, + add_breadcrumb: function(on_reverse_breadcrumb) { + var self = this; + var views = [this.active_view || this.views_src[0].view_type]; + this.on('switch_mode', self, 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]; + var state = self.url_states[view_to_select]; + self.do_push_state(state || {}); + $.when(self.switch_mode(view_to_select)).then(function() { + self.$el.show(); + }); + }, + 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; + }, + on_reverse_breadcrumb: on_reverse_breadcrumb, + }); }, /** * Returns to the view preceding the caller view in this manager's * navigation history (the navigation history is appended to via - * on_mode_switch) + * switch_mode) * * @param {Object} [options] * @param {Boolean} [options.created=false] resource was created * @param {String} [options.default=null] view to switch to if no previous view * @returns {$.Deferred} switching end signal */ - on_prev_view: function (options) { + prev_view: function (options) { options = options || {}; var current_view = this.views_history.pop(); var previous_view = this.views_history[this.views_history.length - 1] || options['default']; if (options.created && current_view === 'form' && previous_view === 'list') { // APR special case: "If creation mode from list (and only from a list), // after saving, go to page view (don't come back in list)" - return this.on_mode_switch('form'); + return this.switch_mode('form'); } else if (options.created && !previous_view && this.action && this.action.flags.default_view === 'form') { // APR special case: "If creation from dashboard, we have no previous view - return this.on_mode_switch('form'); + return this.switch_mode('form'); } - return this.on_mode_switch(previous_view, true); + return this.switch_mode(previous_view, true); }, /** * Sets up the current viewmanager's search view. @@ -387,8 +650,8 @@ 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")); + this.searchview.on('search_data', self, this.do_searchview_search); + return this.searchview.appendTo(this.$el.find(".oe_view_manager_view_search")); }, do_searchview_search: function(domains, contexts, groupbys) { var self = this, @@ -398,7 +661,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ domains: [this.action.domain || []].concat(domains || []), contexts: [action_context].concat(contexts || []), group_by_seq: groupbys || [] - }, function (results) { + }).then(function (results) { self.dataset._model = new instance.web.Model( self.dataset.model, results.context, results.domain); var groupby = results.group_by.length @@ -411,14 +674,6 @@ instance.web.ViewManager = instance.web.Widget.extend({ }); }, /** - * Event launched when a controller has been inited. - * - * @param {String} view_type type of view - * @param {String} view the inited controller - */ - on_controller_inited: function(view_type, view) { - }, - /** * Called when one of the view want to execute an action */ on_action: function(action) { @@ -434,14 +689,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 +733,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 +760,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 +773,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,14 +800,23 @@ 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) { + this.dataset.call('fields_get', [false, {}]).then(function (fields) { var $root = $('<dl>'); _(fields).each(function (attributes, name) { $root.append($('<dt>').append($('<h4>').text(name))); - var $attrs = $('<dl>').appendTo( - $('<dd>').appendTo($root)); + var $attrs = $('<dl>').appendTo($('<dd>').appendTo($root)); _(attributes).each(function (def, name) { if (def instanceof Object) { def = JSON.stringify(def); @@ -593,15 +832,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 +856,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); @@ -651,26 +896,40 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ }, action || {}); this.do_action(action); }, - on_mode_switch: function (view_type, no_store) { + switch_mode: function (view_type, no_store, options) { var self = this; - return $.when(this._super(view_type, no_store)).then(function () { - 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', { + return $.when(this._super.apply(this, arguments)).then(function () { + var controller = self.views[self.active_view].controller; + 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; + }, + get_action_manager: function() { + var cur = this; + while (cur = cur.getParent()) { + if (cur instanceof instance.web.ActionManager) { + return cur; + } + } + return undefined; + }, + set_title: function(title) { + this.$el.find('.oe_breadcrumb_title:first').html(this.get_action_manager().get_title()); + }, do_push_state: function(state) { if (this.getParent() && this.getParent().do_push_state) { state["view_type"] = this.active_view; + this.url_states[this.active_view] = state; this.getParent().do_push_state(state); } }, @@ -680,7 +939,7 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ if (state.view_type && state.view_type !== this.active_view) { defs.push( this.views[this.active_view].deferred.pipe(function() { - return self.on_mode_switch(state.view_type, true); + return self.switch_mode(state.view_type, true); }) ); } @@ -689,45 +948,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('Attachement'), }, { 'name' : 'other', 'label' : _t('More'), } ]; this.items = { 'print' : [], - 'files' : [], - 'other' : [ - { label: _t("Import"), callback: view.on_sidebar_import }, - { label: _t("Export"), callback: view.on_sidebar_export } - ] - } - if (this.session.uid === 1) { - var item = { label: _t("Translate"), callback: view.on_sidebar_translate, title: _t("Technical translation") }; - this.items.other.push(item); - } + 'other' : [] + }; + 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]); @@ -736,16 +989,17 @@ 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() { + $(this).toggle(!!$(this).find('li').length); + }); }, /** * For each item added to the section: @@ -811,19 +1065,23 @@ instance.web.Sidebar = instance.web.Widget.extend({ self.rpc("/web/action/load", { action_id: item.action.id, context: additional_context - }, function(result) { - result.result.context = _.extend(result.result.context || {}, + }).then(function(result) { + result.context = _.extend(result.context || {}, additional_context); - result.result.flags = result.result.flags || {}; - result.result.flags.new_window = true; - self.do_action(result.result, function () { - // reload view - self.getParent().reload(); + result.flags = result.flags || {}; + result.flags.new_window = true; + self.do_action(result, { + on_close: function() { + // reload view + self.getParent().reload(); + }, }); }); }); }, do_attachement_update: function(dataset, model_id) { + this.dataset = dataset; + this.model_id = model_id; if (!model_id) { this.on_attachments_loaded([]); } else { @@ -835,165 +1093,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: '', /** @@ -1006,33 +1142,36 @@ 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() { + load_view: function(context) { + var self = this; + var view_loaded; if (this.embedded_view) { - var def = $.Deferred(); - var self = this; - $.async_when().then(function() {def.resolve(self.embedded_view);}); - return def.pipe(this.on_loaded); + view_loaded = $.Deferred(); + $.async_when().then(function() { + view_loaded.resolve(self.embedded_view); + }); } else { - var context = new instance.web.CompoundContext(this.dataset.get_context()); if (! this.view_type) console.warn("view_type is not defined", this); - return this.rpc("/web/view/load", { + view_loaded = this.rpc("/web/view/load", { "model": this.dataset.model, "view_id": this.view_id, "view_type": this.view_type, toolbar: !!this.options.$sidebar, - context: context - }).pipe(this.on_loaded); + context: this.dataset.get_context(context) + }); } - }, - /** - * Called after a successful call to fields_view_get. - * Must return a promise. - */ - on_loaded: function(fields_view_get) { + return view_loaded.pipe(function(r) { + self.trigger('view_loaded', r); + // add css classes that reflect the (absence of) access rights + self.$el.addClass('oe_view') + .toggleClass('oe_cannot_create', !self.is_action_enabled('create')) + .toggleClass('oe_cannot_edit', !self.is_action_enabled('edit')) + .toggleClass('oe_cannot_delete', !self.is_action_enabled('delete')); + }); }, set_default_options: function(options) { this.options = options || {}; @@ -1044,12 +1183,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``. * @@ -1073,7 +1206,7 @@ instance.web.View = instance.web.Widget.extend({ var context = new instance.web.CompoundContext(dataset.get_context(), action_data.context || {}); var handler = function (r) { - var action = r.result; + var action = r; if (action && action.constructor == Object) { var ncontext = new instance.web.CompoundContext(context); if (record_id) { @@ -1092,7 +1225,9 @@ instance.web.View = instance.web.Widget.extend({ /* niv: previously we were overriding once more with action_data.context, * I assumed this was not a correct behavior and removed it */ - return self.do_action(action, result_handler); + return self.do_action(action, { + on_close: result_handler, + }); }, null); } else { return result_handler(); @@ -1100,7 +1235,7 @@ instance.web.View = instance.web.Widget.extend({ }; if (action_data.special) { - return handler({result: {"type":"ir.actions.act_window_close"}}); + return handler({"type":"ir.actions.act_window_close"}); } else if (action_data.type=="object") { var args = [[record_id]], additional_args = []; if (action_data.args) { @@ -1114,11 +1249,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}).then(handler); } else { - return dataset.exec_workflow(record_id, action_data.name, handler); + return dataset.exec_workflow(record_id, action_data.name).then(handler); } }, /** @@ -1132,10 +1267,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) { @@ -1146,10 +1281,9 @@ instance.web.View = instance.web.Widget.extend({ }, /** * Switches to a specific view type - * - * @param {String} view view type to switch to */ - do_switch_view: function(view) { + do_switch_view: function() { + this.trigger.apply(this, ['switch_mode'].concat(_.toArray(arguments))); }, /** * Cancels the switch to the current view, switches to the previous one @@ -1158,27 +1292,11 @@ instance.web.View = instance.web.Widget.extend({ * @param {Boolean} [options.created=false] resource was created * @param {String} [options.default=null] view to switch to if no previous view */ - do_prev_view: function (options) { - }, + 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(); @@ -1189,6 +1307,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; } }); @@ -1216,7 +1343,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, @@ -1225,7 +1351,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]; |
