diff options
| author | Harry (OpenERP) <hmo@tinyerp.com> | 2012-08-09 18:52:39 +0530 |
|---|---|---|
| committer | Harry (OpenERP) <hmo@tinyerp.com> | 2012-08-09 18:52:39 +0530 |
| commit | 1b5259b3e374a8f9c385d18706aadbdf0a2403df (patch) | |
| tree | b446fae6a43cba80943121cd3fe55f1fada3a954 /addons/web/static/src/js | |
| parent | 68998d4c827bb080d262da593c7766e25fb3427e (diff) | |
| parent | e46fb9fda8ba93d3eb8ac2907a75012f7a4d920e (diff) | |
[MERGE]
bzr revid: hmo@tinyerp.com-20120809132239-blyhpfxwn2szlqec
Diffstat (limited to 'addons/web/static/src/js')
| -rw-r--r-- | addons/web/static/src/js/boot.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/chrome.js | 453 | ||||
| -rw-r--r-- | addons/web/static/src/js/corelib.js | 171 | ||||
| -rw-r--r-- | addons/web/static/src/js/coresetup.js | 93 | ||||
| -rw-r--r-- | addons/web/static/src/js/data.js | 6 | ||||
| -rw-r--r-- | addons/web/static/src/js/data_export.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/data_import.js | 2 | ||||
| -rw-r--r-- | addons/web/static/src/js/formats.js | 78 | ||||
| -rw-r--r-- | addons/web/static/src/js/search.js | 91 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_editor.js | 1212 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_form.js | 819 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list.js | 502 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list_editable.js | 968 | ||||
| -rw-r--r-- | addons/web/static/src/js/views.js | 272 |
14 files changed, 2234 insertions, 2441 deletions
diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index 86f8077b151..844dd88a0ac 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, @@ -48,7 +48,7 @@ * 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); diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 094e2f184e6..fbf51dac522 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -57,9 +57,7 @@ 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, @@ -83,11 +81,6 @@ instance.web.Dialog = instance.web.Widget.extend({ if (options) { _.extend(this.dialog_options, options); } - if (this.dialog_options.autoOpen) { - this.open(); - } else { - instance.web.dialog(this.$element, this.get_options()); - } }, get_options: function(options) { var self = this, @@ -116,27 +109,44 @@ instance.web.Dialog = instance.web.Widget.extend({ } else if (val.slice(-1) == "%") { return Math.round(available_size / 100 * parseInt(val.slice(0, -1), 10)); } else { - return parseInt(val, 10); + 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'); + instance.web.dialog(this.$element, o).dialog('open'); if (o.height === 'auto' && o.max_height) { this.$element.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.$element, o); + var res = this.start(); + this.dialog_inited = true; + return res; + }, close: function() { this.$element.dialog('close'); }, on_close: function() { + if (this.__tmp_dialog_destroying) + return; if (this.dialog_options.destroy_on_close) { + this.__tmp_dialog_closing = true; this.destroy(); + this.__tmp_dialog_closing = undefined; } }, on_resized: function() { @@ -145,6 +155,11 @@ instance.web.Dialog = instance.web.Widget.extend({ _.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.$element.dialog('destroy'); } @@ -248,13 +263,17 @@ instance.web.Loading = instance.web.Widget.extend({ // 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)); + if (instance.connection.debug) { + this.$element.text(_.str.sprintf( _t("Loading (%d)"), this.count)); + } else { + this.$element.text(_t("Loading")); + } this.$element.show(); this.getParent().$element.addClass('oe_wait'); } else { @@ -263,7 +282,7 @@ instance.web.Loading = instance.web.Widget.extend({ // 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'); @@ -274,7 +293,7 @@ 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")); @@ -341,16 +360,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 @@ -374,8 +393,19 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ var fields = $(form).serializeArray(); self.rpc("/web/database/create", {'fields': fields}, function(result) { var form_obj = self.to_object(fields); - self.getParent().do_login( form_obj['db_name'], 'admin', form_obj['create_admin_pwd']); - self.destroy(); + var client_action = { + type: 'ir.actions.client', + tag: 'login', + params: { + 'db': form_obj['db_name'], + 'login': 'admin', + 'password': form_obj['create_admin_pwd'], + 'login_successful': function() { + instance.webclient.show_application(); + }, + }, + }; + self.do_action(client_action); }); }, @@ -460,19 +490,26 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ }); }, do_exit: function () { + this.do_action("login"); } }); +instance.web.client_actions.add("database_manager", "instance.web.DatabaseManager"); instance.web.Login = instance.web.Widget.extend({ template: "Login", remember_credentials: true, _db_list: null, - init: function(parent) { + init: function(parent, params) { this._super(parent); this.has_local_storage = typeof(localStorage) != 'undefined'; this.selected_db = null; this.selected_login = null; + this.params = params || {}; + + if (this.params.login_successful) { + this.on('login_successful', this, this.params.login_successful); + } if (this.has_local_storage && this.remember_credentials) { this.selected_db = localStorage.getItem('last_db_login_success'); @@ -482,26 +519,17 @@ instance.web.Login = instance.web.Widget.extend({ } } }, - open_db_manager: function(){ - var self = this; - self.$element.find('.oe_login_bottom').hide(); - self.$element.find('.oe_login_pane').hide(); - self.databasemanager = new instance.web.DatabaseManager(self); - self.databasemanager.appendTo(self.$element); - self.databasemanager.do_exit.add_last(function() { - self.databasemanager.destroy(); - self.$element.find('.oe_login_bottom').show(); - self.$element.find('.oe_login_pane').show(); - self.load_db_list(true).then(self.on_db_list_loaded); - }); - }, start: function() { var self = this; self.$element.find("form").submit(self.on_submit); self.$element.find('.oe_login_manage_db').click(function() { - self.open_db_manager(); + self.do_action("database_manager"); + }); + return self.load_db_list().then(self.on_db_list_loaded).then(function() { + if (self.params.db) { + self.do_login(self.params.db, self.params.login, self.params.password); + } }); - return self.load_db_list().then(self.on_db_list_loaded); }, load_db_list: function (force) { var d = $.when(), self = this; @@ -522,7 +550,7 @@ instance.web.Login = instance.web.Widget.extend({ var dbdiv = this.$element.find('div.oe_login_dbpane'); this.$element.find("[name=db]").replaceWith(instance.web.qweb.render('Login.dblist', { db_list: list, selected_db: this.selected_db})); if(list.length === 0) { - self.open_db_manager(); + this.do_action("database_manager"); } else if(list && list.length === 1) { dbdiv.hide(); } else { @@ -569,13 +597,100 @@ instance.web.Login = instance.web.Widget.extend({ localStorage.setItem('last_password_login_success', ''); } } - self.trigger("login"); + self.trigger('login_successful'); },function () { self.$(".oe_login_pane").fadeIn("fast"); self.$element.addClass("oe_login_invalid"); }); + }, + show: function () { + this.$element.show(); + }, + hide: function () { + this.$element.hide(); + } +}); +instance.web.client_actions.add("login", "instance.web.Login"); + +/** + * Client action to reload the whole interface. + * If params has an entry 'menu_id', it opens the given menu entry. + */ +instance.web.Reload = instance.web.Widget.extend({ + init: function(parent, params) { + this._super(parent); + this.menu_id = (params && params.menu_id) || false; + }, + start: function() { + var l = window.location; + var timestamp = new Date().getTime(); + var search = "?ts=" + timestamp; + if (l.search) { + search = l.search + "&ts=" + timestamp; + } + var hash = l.hash; + if (this.menu_id) { + hash = "#menu_id=" + this.menu_id; + } + var url = l.protocol + "//" + l.host + l.pathname + search + hash; + window.location = url; + } +}); +instance.web.client_actions.add("reload", "instance.web.Reload"); + +/** + * Client action to go back in breadcrumb history. + * If can't go back in history stack, will go back to home. + */ +instance.web.HistoryBack = instance.web.Widget.extend({ + init: function(parent, params) { + if (!parent.history_back()) { + window.location = '/' + (window.location.search || ''); + } + } +}); +instance.web.client_actions.add("history_back", "instance.web.HistoryBack"); + +/** + * Client action to go back home. + */ +instance.web.Home = instance.web.Widget.extend({ + init: function(parent, params) { + window.location = '/' + (window.location.search || ''); } }); +instance.web.client_actions.add("home", "instance.web.Home"); + +instance.web.ChangePassword = instance.web.Widget.extend({ + template: "ChangePassword", + start: function() { + var self = this; + self.$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); + }, +}) +instance.web.client_actions.add("change_password", "instance.web.ChangePassword"); instance.web.Menu = instance.web.Widget.extend({ template: 'Menu', @@ -744,37 +859,6 @@ instance.web.UserMenu = instance.web.Widget.extend({ } }); }, - 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() { @@ -790,7 +874,7 @@ instance.web.UserMenu = instance.web.Widget.extend({ 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); + 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); }); }; @@ -802,44 +886,10 @@ instance.web.UserMenu = instance.web.Widget.extend({ }, on_menu_settings: function() { var self = this; - var action_manager = new instance.web.ActionManager(this); - var dataset = new instance.web.DataSet (this,'res.users',this.context); - dataset.call ('action_get','',function (result){ - self.rpc('/web/action/load', {action_id:result}, function(result){ - action_manager.do_action(_.extend(result['result'], { - target: 'inline', - res_id: self.session.uid, - res_model: 'res.users', - flags: { - action_buttons: false, - search_view: false, - sidebar: false, - views_switcher: false, - pager: false - } - })); - }); + self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }, function(result) { + result.result.res_id = instance.connection.uid; + self.getParent().action_manager.do_action(result.result); }); - this.dialog = new instance.web.Dialog(this,{ - title: _t("Preferences"), - width: '700px', - buttons: [ - {text: _t("Change password"), click: function(){ self.change_password(); }}, - {text: _t("Cancel"), click: function(){ $(this).dialog('destroy'); }}, - {text: _t("Save"), click: function(){ - var inner_widget = action_manager.inner_widget; - inner_widget.views[inner_widget.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; @@ -851,56 +901,36 @@ instance.web.UserMenu = instance.web.Widget.extend({ window.location.href, 'debug'); }); instance.web.dialog($help, {autoOpen: true, - modal: true, width: 960, title: _t("About")}); + modal: true, width: 580, height: 290, resizable: false, title: _t("About")}); }); }, }); -instance.web.WebClient = instance.web.Widget.extend({ - init: function(parent) { - var self = this; +instance.web.Client = instance.web.Widget.extend({ + init: function(parent, origin) { + instance.client = instance.webclient = this; this._super(parent); - instance.webclient = this; - this.querystring = '?' + jQuery.param.querystring(); - this._current_state = null; - }, - _get_version_label: function() { - if (this.session.openerp_entreprise) { - return 'OpenERP'; - } else { - return _t("OpenERP - Unsupported/Community Version"); - } - }, - set_title: function(title) { - title = _.str.clean(title); - var sep = _.isEmpty(title) ? '' : ' - '; - document.title = title + sep + 'OpenERP'; + this.origin = origin; }, start: function() { var self = this; - this.$element.addClass("openerp openerp_webclient_container"); - 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">'; - } - } - this.session.session_bind().then(function() { - self.destroy_content(); - self.show_common(); - if (!self.session.session_is_valid()) { - self.show_login(); - } else { - self.show_application(); - } + return instance.connection.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.$element.on('mouseenter', '.oe_systray > div:not([data-tipsy=true])', function() { $(this).attr('data-tipsy', 'true').tipsy().trigger('mouseenter'); }); this.$element.on('click', '.oe_dropdown_toggle', function(ev) { ev.preventDefault(); var $toggle = $(this); - var $menu = $toggle.find('.oe_dropdown_menu'); + 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 @@ -910,21 +940,63 @@ instance.web.WebClient = instance.web.Widget.extend({ var doc_width = $(document).width(); var offset = $menu.offset(); var menu_width = $menu.width(); - var x = doc_width - offset.left - menu_width - 15; + 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() { - self.$element.find('.oe_dropdown_menu.oe_opened').removeClass('oe_opened'); + instance.web.bus.on('click', this, function(ev) { + $.fn.tipsy.clear(); + if (!$(ev.target).is('input[type=file]')) { + self.$element.find('.oe_dropdown_menu.oe_opened').removeClass('oe_opened'); + } }); }, show_common: function() { var self = this; this.crashmanager = new instance.web.CrashManager(); instance.connection.on_rpc_error.add(this.crashmanager.on_rpc_error); + self.notification = new instance.web.Notification(this); + self.notification.appendTo(self.$element); + self.loading = new instance.web.Loading(self); + self.loading.appendTo(self.$element); + self.action_manager = new instance.web.ActionManager(self); + self.action_manager.appendTo(self.$('.oe_application')); + }, +}); + +instance.web.WebClient = instance.web.Client.extend({ + _template: 'WebClient', + init: function(parent) { + this._super(parent); + this._current_state = null; + }, + start: function() { + var self = this; + return $.when(this._super()).pipe(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(); + } + }); + }, + set_title: function(title) { + title = _.str.clean(title); + var sep = _.isEmpty(title) ? '' : ' - '; + document.title = title + sep + 'OpenERP'; + }, + show_common: function() { + var self = this; + this._super(); window.onerror = function (message, file, line) { self.crashmanager.on_traceback({ type: _t("Client Error"), @@ -932,24 +1004,15 @@ instance.web.WebClient = instance.web.Widget.extend({ data: {debug: file + ':' + line} }); }; - self.notification = new instance.web.Notification(this); - self.notification.appendTo(self.$element); - self.loading = new instance.web.Loading(self); - self.loading.appendTo(self.$element); - self.login = new instance.web.Login(self); - self.login.on("login",self,self.show_application); - self.$table = $(QWeb.render("WebClient", {})); - self.action_manager = new instance.web.ActionManager(self); - self.action_manager.appendTo(self.$table.find('.oe_application')); }, show_login: function() { - var self = this; - self.login.appendTo(self.$element); + this.$('.oe_topbar').hide(); + this.action_manager.do_action("login"); + this.action_manager.inner_widget.on('login_successful', this, this.show_application); }, show_application: function() { var self = this; - self.$element.append(self.$table); - self.login.$element.hide(); + self.$('.oe_topbar').show(); self.menu = new instance.web.Menu(self); self.menu.replace(this.$element.find('.oe_menu_placeholder')); self.menu.on('menu_click', this, this.on_menu_action); @@ -960,7 +1023,7 @@ instance.web.WebClient = instance.web.Widget.extend({ self.user_menu.do_update(); self.bind_hashchange(); if (!self.session.openerp_entreprise) { - var version_label = self._get_version_label(); + var version_label = _t("OpenERP - Unsupported/Community Version"); 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)); } self.set_title(); @@ -1001,15 +1064,15 @@ instance.web.WebClient = instance.web.Widget.extend({ $(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"); if(first_menu_id) { self.menu.menu_click(first_menu_id); } }); + } else { + $(window).trigger('hashchange'); } }, on_hashchange: function(event) { @@ -1030,6 +1093,7 @@ instance.web.WebClient = instance.web.Widget.extend({ }, 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); @@ -1056,38 +1120,46 @@ instance.web.WebClient = instance.web.Widget.extend({ } }, set_content_full_screen: function(fullscreen) { - if (fullscreen) + if (fullscreen) { $(".oe_webclient", this.$element).addClass("oe_content_full_screen"); - else + $("body").css({'overflow-y':'hidden'}); + } else { $(".oe_webclient", this.$element).removeClass("oe_content_full_screen"); + $("body").css({'overflow-y':'scroll'}); + } } }); -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.connection.session_authenticate(self.dbname, self.login, self.key, true).pipe(function() { + return self.rpc("/web/action/load", { action_id: self.action_id }, function(result) { + var action = result.result; + action.flags = _.extend({ + //views_switcher : false, + search_view : false, + action_buttons : false, + sidebar : false + //pager : false + }, self.options, action.flags || {}); - self.am.do_action(action); + self.action_manager.do_action(action); + }); + }); }); - } + }, }); instance.web.embed = function (origin, dbname, login, key, action, options) { @@ -1101,13 +1173,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 1ed4c4788e3..a46d6314ebe 100644 --- a/addons/web/static/src/js/corelib.js +++ b/addons/web/static/src/js/corelib.js @@ -378,7 +378,8 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, { instance.web.EventDispatcherMixin.init.call(this); this.__getterSetterInternalMap = {}; }, - set: function(map) { + set: function(map, options) { + options = options || {}; var self = this; var changed = false; _.each(map, function(val, key) { @@ -387,10 +388,11 @@ instance.web.PropertiesMixin = _.extend({}, instance.web.EventDispatcherMixin, { return; changed = true; self.__getterSetterInternalMap[key] = val; - self.trigger("change:" + key, self, { - oldValue: tmp, - newValue: val - }); + if (! options.silent) + self.trigger("change:" + key, self, { + oldValue: tmp, + newValue: val + }); }); if (changed) self.trigger("change", self); @@ -489,24 +491,20 @@ instance.web.CallbackEnabledMixin = _.extend({}, instance.web.PropertiesMixin, { * * The semantics of this precisely replace closing over the method call. * - * @param {String} method_name name of the method to invoke + * @param {String|Function} method function or name of the method to invoke * @returns {Function} proxied method */ - proxy: function (method_name) { + proxy: function (method) { var self = this; return function () { - return self[method_name].apply(self, arguments); + var fn = (typeof method === 'string') ? self[method] : method; + return fn.apply(self, arguments); } } }); instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { /** - * Tag name when creating a default $element. - * @type string - */ - tagName: 'div', - /** * Constructs the widget and sets its parent if a parent is given. * * @constructs instance.web.Widget @@ -515,14 +513,9 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { * @param {instance.web.Widget} parent Binds the current instance to the given Widget instance. * When that widget is destroyed by calling destroy(), the current instance will be * destroyed too. Can be null. - * @param {String} element_id Deprecated. Sets the element_id. Only useful when you want - * to bind the current Widget to an already existing part of the DOM, which is not compatible - * with the DOM insertion methods provided by the current implementation of Widget. So - * for new components this argument should not be provided any more. */ init: function(parent) { instance.web.CallbackEnabledMixin.init.call(this); - this.$element = $(document.createElement(this.tagName)); this.setParent(parent); }, /** @@ -532,7 +525,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { _.each(this.getChildren(), function(el) { el.destroy(); }); - if(this.$element != null) { + if(this.$element) { this.$element.remove(); } instance.web.PropertiesMixin.destroy.call(this); @@ -611,6 +604,7 @@ instance.web.WidgetMixin = _.extend({},instance.web.CallbackEnabledMixin, { * @returns {jQuery.Deferred} */ start: function() { + return $.when(); } }); @@ -671,6 +665,12 @@ instance.web.CallbackEnabled = instance.web.Class.extend(instance.web.CallbackEn * That will kill the widget in a clean way and erase its content from the dom. */ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { + // Backbone-ish API + tagName: 'div', + id: null, + className: null, + attributes: {}, + events: {}, /** * The name of the QWeb template that will be used for rendering. Must be * redefined in subclasses or the default render() method can not be used. @@ -687,13 +687,11 @@ 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); + // FIXME: this should not be + this.setElement(this._make_descriptive()); this.session = instance.connection; }, /** @@ -702,20 +700,120 @@ instance.web.Widget = instance.web.Class.extend(instance.web.WidgetMixin, { * key that references `this`. */ renderElement: function() { - var rendered = null; - if (this.template) - rendered = instance.web.qweb.render(this.template, {widget: this}); - if (_.str.trim(rendered)) { - var elem = $(rendered); - this.$element.replaceWith(elem); - this.$element = elem; + var $el; + if (this.template) { + $el = $(_.str.trim(instance.web.qweb.render( + this.template, {widget: this}))); + } else { + $el = this._make_descriptive(); } + this.replaceElement($el); }, + /** - * Shortcut for $element.find() like backbone + * Re-sets the widget's root element and replaces the old root element + * (if any) by the new one in the DOM. + * + * @param {HTMLElement | jQuery} $el + * @returns {*} this + */ + replaceElement: function ($el) { + var $oldel = this.$element; + this.setElement($el); + if ($oldel && !$oldel.is(this.$element)) { + $oldel.replaceWith(this.$element); + } + return this; + }, + /** + * Re-sets the widget's root element (el/$el/$element). + * + * Includes: + * * re-delegating events + * * re-binding sub-elements + * * if the widget already had a root element, replacing the pre-existing + * element in the DOM + * + * @param {HTMLElement | jQuery} element new root element for the widget + * @return {*} this + */ + setElement: function (element) { + // NB: completely useless, as WidgetMixin#init creates a $element + // always + if (this.$element) { + this.undelegateEvents(); + } + + this.$element = (element instanceof $) ? element : $(element); + this.el = this.$element[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.$element.on(event, method); + } else { + this.$element.on(event, selector, method); + } + } + }, + undelegateEvents: function () { + this.$element.off('.widget_events'); + }, + /** + * Shortcut for ``this.$element.find(selector)`` + * + * @param {String} selector CSS selector, rooted in $el + * @returns {jQuery} selector match */ - "$": function() { - return this.$element.find.apply(this.$element,arguments); + $: function(selector) { + return this.$element.find(selector); }, /** * Informs the action manager to do an action. This supposes that @@ -1037,7 +1135,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']) }; }, /** @@ -1278,7 +1377,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) { @@ -1297,7 +1396,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) { diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js index a5a62b9bcc1..6ba62162e93 100644 --- a/addons/web/static/src/js/coresetup.js +++ b/addons/web/static/src/js/coresetup.js @@ -19,15 +19,14 @@ instance.web.OldWidget = instance.web.Widget.extend({ 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)); + this.setElement(tmp || this._make_descriptive()); }, renderElement: function() { var rendered = this.render(); if (rendered) { - var elem = $(rendered); - this.$element.replaceWith(elem); - this.$element = elem; + this.replaceElement($(rendered)); } return this; }, @@ -394,13 +393,13 @@ 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; + } } }); @@ -416,12 +415,12 @@ instance.web.Bus = instance.web.Class.extend(instance.web.EventDispatcherMixin, // check gtk bindings // http://unixpapa.com/js/key.html _.each('click,dblclick,keydown,keypress,keyup'.split(','), function(evtype) { - $('html').on(evtype, self, function(ev) { + $('html').on(evtype, function(ev) { self.trigger(evtype, ev); }); }); _.each('resize,scroll'.split(','), function(evtype) { - $(window).on(evtype, self, function(ev) { + $(window).on(evtype, function(ev) { self.trigger(evtype, ev); }); }); @@ -541,16 +540,48 @@ $.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.connection.synch) + return $.when.apply(this, arguments); + else + return old_async_when.apply(this, arguments); }; /** Setup blockui */ if ($.blockUI) { $.blockUI.defaults.baseZ = 1100; - $.blockUI.defaults.message = '<img src="/web/static/src/img/throbber2.gif">'; + $.blockUI.defaults.message = '<div class="oe_blockui_spin" style="height: 50px">'; + $.blockUI.defaults.css.border = '0'; + $.blockUI.defaults.css["background-color"] = ''; + $.blockUI.spinners = []; +} +instance.web.blockUI = function() { + var tmp = $.blockUI.apply($, arguments); + var target = $(".oe_blockui_spin")[0]; + 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 + }; + var spinner = new Spinner(opts).spin(target); + $.blockUI.spinners.push(spinner); + return tmp; +} +instance.web.unblockUI = function() { + _.each($.blockUI.spinners, function(el) { + el.stop(); + }); + return $.unblockUI.apply($, arguments); } /** Setup default session */ @@ -641,32 +672,6 @@ instance.connection.on('module_loaded', this, function () { */ instance.web.client_actions = new instance.web.Registry(); -/** - * Client action to reload the whole interface. - * If params has an entry 'menu_id', it opens the given menu entry. - */ -instance.web.Reload = instance.web.Widget.extend({ - init: function(parent, params) { - this._super(parent); - this.menu_id = (params && params.menu_id) || false; - }, - start: function() { - var l = window.location; - var timestamp = new Date().getTime(); - var search = "?ts=" + timestamp; - if (l.search) { - search = l.search + "&ts=" + timestamp; - } - var hash = l.hash; - if (this.menu_id) { - hash = "#menu_id=" + this.menu_id; - } - var url = l.protocol + "//" + l.host + l.pathname + search + hash; - window.location = url; - } -}); -instance.web.client_actions.add("reload", "instance.web.Reload"); - }; // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index fdb15d598af..2589afe5503 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -803,7 +803,7 @@ 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; }, }); instance.web.DataSetStatic = instance.web.DataSet.extend({ @@ -1042,7 +1042,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(); @@ -1067,7 +1067,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ return this._super(method, args, callback, error_callback); }, alter_ids: function(n_ids) { - this._super(n_ids); + this._super(n_ids); this.on_change(); }, }); diff --git a/addons/web/static/src/js/data_export.js b/addons/web/static/src/js/data_export.js index ebe82ddb4b4..f8e669e1b4a 100644 --- a/addons/web/static/src/js/data_export.js +++ b/addons/web/static/src/js/data_export.js @@ -376,7 +376,7 @@ 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(); + instance.web.blockUI(); this.session.get_file({ url: '/web/export/' + export_format, data: {data: JSON.stringify({ @@ -387,7 +387,7 @@ instance.web.DataExport = instance.web.Dialog.extend({ import_compat: Boolean( this.$element.find("#import_compat").val()) })}, - complete: $.unblockUI + complete: instance.web.unblockUI }); }, close: function() { diff --git a/addons/web/static/src/js/data_import.js b/addons/web/static/src/js/data_import.js index 6059f292df9..620878569ce 100644 --- a/addons/web/static/src/js/data_import.js +++ b/addons/web/static/src/js/data_import.js @@ -156,7 +156,7 @@ instance.web.DataImport = instance.web.Dialog.extend({ }); }, toggle_import_button: function (newstate) { - instance.web.dialog(this.$element, 'widget') + instance.web.dialog(this.$element, 'widget') .find('.oe_import_dialog_button') .button('option', 'disabled', !newstate); }, diff --git a/addons/web/static/src/js/formats.js b/addons/web/static/src/js/formats.js index 6ceb7090e44..797e1a2cbdf 100644 --- a/addons/web/static/src/js/formats.js +++ b/addons/web/static/src/js/formats.js @@ -271,82 +271,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_list_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 c8b6f797dba..53daa40ae31 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -330,6 +330,12 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea }); } + // Launch a search on clicking the oe_searchview_search button + this.$element.on('click', 'button.oe_searchview_search', function (e) { + e.stopImmediatePropagation(); + self.do_search(); + }); + this.$element.on('keydown', '.oe_searchview_input, .oe_searchview_facet', function (e) { switch(e.which) { @@ -510,7 +516,16 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea this.$element.addClass('oe_focused'); }, childBlurred: function () { - this.$element.removeClass('oe_focused'); + var val = this.$element.val(); + this.$element.val(''); + var complete = this.$element.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.$element.removeClass('oe_focused') + .trigger('blur'); }, /** * @@ -646,7 +661,7 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea null, _(this.select_for_drawer()).invoke( 'appendTo', this.$element.find('.oe_searchview_drawer'))); - new instance.web.search.AddToDashboard(this).appendTo($('.oe_searchview_drawer', this.$element)); + new instance.web.search.AddToReporting(this).appendTo($('.oe_searchview_drawer', this.$element)); // load defaults var defaults_fetched = $.when.apply(null, _(this.inputs).invoke( @@ -846,13 +861,13 @@ instance.web.search.Invalid = instance.web.Class.extend( /** @lends instance.web ); } }); -instance.web.search.Widget = instance.web.OldWidget.extend( /** @lends instance.web.search.Widget# */{ +instance.web.search.Widget = instance.web.Widget.extend( /** @lends instance.web.search.Widget# */{ template: null, /** * Root class of all search widgets * * @constructs instance.web.search.Widget - * @extends instance.web.OldWidget + * @extends instance.web.Widget * * @param view the ancestor view of this widget */ @@ -1475,7 +1490,17 @@ 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)); + } + if (value.length > 1) { + // more than one search_default m2o id? Should we OR them? + throw new Error( + _("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) { if (_(names).isEmpty()) { return null; } @@ -1653,41 +1678,39 @@ instance.web.search.Filters = instance.web.search.Input.extend({ })); } }); -instance.web.search.AddToDashboard = instance.web.Widget.extend({ - template: 'SearchView.addtodashboard', +instance.web.search.AddToReporting = instance.web.Widget.extend({ + template: 'SearchView.addtoreporting', _in_drawer: true, start: function () { var self = this; - this.data_loaded = $.Deferred(); - this.dashboard_data =[]; this.$element .on('click', 'h4', this.proxy('show_option')) .on('submit', 'form', function (e) { e.preventDefault(); self.add_dashboard(); }); - return $.when(this.load_data(),this.data_loaded).pipe(this.proxy("render_data")); + return this.load_data().then(this.proxy("render_data")); }, load_data:function(){ - var self = this,dashboard_menu = instance.webclient.menu.data.data.children; - var ir_model_data = new instance.web.Model('ir.model.data',{},[['name','=','menu_reporting_dashboard']]).query(['res_id']); - var map_data = function(result){ - _.detect(dashboard_menu, function(dash){ - var id = _.pluck(dash.children, "id"),indexof = _.indexOf(id, result.res_id); - if(indexof !== -1){ - self.dashboard_data = dash.children[indexof].children - self.data_loaded.resolve(); - return; - } - }); - }; - return ir_model_data._execute().done(function(result){map_data(result[0])}); + if (!instance.webclient) { return $.Deferred().reject(); } + var dashboard_menu = instance.webclient.menu.data.data.children; + return new instance.web.Model('ir.model.data') + .query(['res_id']) + .filter([['name','=','menu_reporting_dashboard']]) + .first().pipe(function (result) { + var menu = _(dashboard_menu).chain() + .pluck('children') + .flatten(true) + .find(function (child) { return child.id === result.res_id; }) + .value(); + return menu ? menu.children : []; + }); }, - - render_data: function(){ - var self = this; - var selection = instance.web.qweb.render("SearchView.addtodashboard.selection",{selections:this.dashboard_data}); - this.$element.find("input").before(selection) + render_data: function(dashboard_choices){ + var selection = instance.web.qweb.render( + "SearchView.addtoreporting.selection", { + selections: dashboard_choices}); + this.$("input").before(selection) }, add_dashboard:function(){ var self = this; @@ -1695,11 +1718,11 @@ instance.web.search.AddToDashboard = instance.web.Widget.extend({ var view_parent = this.getParent().getParent(); if (! view_parent.action || ! this.$element.find("select").val()) return this.do_warn("Can't find dashboard action"); - data = getParent.build_search_data(), - context = new instance.web.CompoundContext(getParent.dataset.get_context() || []), - domain = new instance.web.CompoundDomain(getParent.dataset.get_domain() || []); - _.each(data.contexts, function(x) {context.add(x);}); - _.each(data.domains, function(x) {domain.add(x);}); + var data = getParent.build_search_data(); + var context = new instance.web.CompoundContext(getParent.dataset.get_context() || []); + var domain = new instance.web.CompoundDomain(getParent.dataset.get_domain() || []); + _.each(data.contexts, context.add, context); + _.each(data.domains, domain.add, domain); this.rpc('/web/searchview/add_to_dashboard', { menu_id: this.$element.find("select").val(), action_id: view_parent.action.id, @@ -1720,7 +1743,7 @@ instance.web.search.AddToDashboard = instance.web.Widget.extend({ this.$element.toggleClass('oe_opened'); if (! this.$element.hasClass('oe_opened')) return; - this.$element.find("input").val(this.getParent().fields_view.name || "" ); + this.$("input").val(this.getParent().fields_view.name || "" ); } }); 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 6df78679ecb..00000000000 --- a/addons/web/static/src/js/view_editor.js +++ /dev/null @@ -1,1212 +0,0 @@ -openerp.web.view_editor = function(instance) { -var _t = instance.web._t; -var QWeb = instance.web.qweb; -instance.web.ViewEditor = instance.web.OldWidget.extend({ - init: function(parent, element_id, dataset, view, options) { - this._super(parent); - this.element_id = element_id; - this.parent = parent; - this.dataset = new instance.web.DataSetSearch(this, 'ir.ui.view', null, null), - this.model = dataset.model; - this.xml_element_id = 0; - this.property = instance.web.ViewEditor.property_widget; - this.one_object = false; - }, - start: function() { - this.init_view_editor(); - }, - init_view_editor: function() { - var self = this, - action_title = _.str.sprintf(_t("Manage Views (%s)"), this.model); - var action = { - name: action_title, - context: this.session.user_context, - domain: [["model", "=", this.model]], - res_model: 'ir.ui.view', - views: [[false, 'list']], - type: 'ir.actions.act_window', - target: "current", - limit: this.dataset.limit || 80, - auto_search : true, - flags: { - sidebar: false, - deletable: false, - views_switcher: false, - action_buttons: false, - search_view: false, - pager: false, - radio: true, - select_view_id: self.parent.fields_view.view_id - } - }; - this.view_edit_dialog = new instance.web.Dialog(this, { - title: action_title, - width: 850, - buttons: [ - {text: _t("Create"), click: function() { self.on_create_view(); }}, - {text: _t("Edit"), click: function() { self.xml_element_id = 0; self.get_arch(); }}, - {text: _t("Remove"), click: function() { self.do_delete_view(); }}, - {text: _t("Close"), click: function() { self.view_edit_dialog.close(); window.location.reload(); }} - ] - }).open(); - this.view_edit_dialog.on_close.add_last(function(){window.location.reload();}); - this.main_view_id = this.parent.fields_view.view_id; - this.action_manager = new instance.web.ActionManager(this); - $.when(this.action_manager.do_action(action)).then(function() { - var viewmanager = self.action_manager.inner_widget, - controller = viewmanager.views[viewmanager.active_view].controller; - self.action_manager.appendTo(self.view_edit_dialog.$element); - self.action_manager.renderElement(self.view_edit_dialog); - controller.on_loaded.add_last(function(){ - $(controller.groups).bind({ - 'selected': function(e, ids, records) { - self.main_view_id = ids[0]; - } - }) - }); - }); - }, - on_create_view: function() { - var self = this; - this.create_view_dialog = new instance.web.Dialog(this, { - title: _.str.sprintf(_t("Create a view (%s)"), self.model), - buttons: [ - {text: _t("Save"), click: function () { - var view_values = {}; - var warn = false; - _.each(self.create_view_widget, function(widget) { - if (widget.is_invalid) { - warn = true; - return false; - } - if (widget.dirty && !widget.is_invalid) { - view_values[widget.name] = widget.get_value(); - } - }); - if (warn) { - self.on_valid_create_view(self.create_view_widget); - } else { - $.when(self.do_save_view(view_values)).then(function() { - self.create_view_dialog.close(); - var controller = self.action_manager.inner_widget.views[self.action_manager.inner_widget.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_widget.views[this.action_manager.inner_widget.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_widget.views[self.action_manager.inner_widget.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,property_to_check = []; - 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 = self.get_object_by_id(parseInt($(parent_tr).attr('id').replace(/[^0-9]+/g, '')), this.one_object['main_object'], [])[0].att_list[0]; - _.each([tr, parent_tr],function(element) { - var value = _.has(_CHILDREN, element) ? element : _.str.include(html_tag, element)?"html_tag":false; - property_to_check.push(value); - }); - field_dataset.call( 'fields_get', [], function(result) { - var fields = _.keys(result); - fields.push(" "),fields.sort(); - self.on_add_node(property_to_check, fields); - }); - }, - do_node_edit: function(side) { - var self = this; - var result = self.get_object_by_id(this.one_object.clicked_tr_id, this.one_object['main_object'], []); - if (result.length && result[0] && result[0].att_list) { - var properties = _PROPERTIES[result[0].att_list[0]]; - self.on_edit_node(properties); - } - }, - do_node_down: function(cur_tr, img) { - var self = this, next_tr, last_tr, tr_to_move = []; - tr_to_move.push(cur_tr); - if (img) { - while (1) { - next_tr = cur_tr.next(); - if ( parseInt(next_tr.attr('level')) <= this.one_object.clicked_tr_level || next_tr.length == 0) { - last_tr = next_tr; - break; - } else { - tr_to_move.push(next_tr); - cur_tr = next_tr; - } - } - } else { - last_tr = cur_tr.next(); - } - if ((self.edit_xml_dialog.$element.find(last_tr).find('a').text()).search("view_id") != -1) { - return false; - } - if (last_tr.length != 0 && parseInt(last_tr.attr('level')) == this.one_object.clicked_tr_level) { - var last_tr_id = (last_tr.attr('id')).split('-')[1]; - img = last_tr.find("img[id='parentimg-" + last_tr_id + "']").attr('src'); - if (img) { - self.edit_xml_dialog.$element.find("img[id='parentimg-" + last_tr_id + "']"). - attr('src', '/web/static/src/img/expand.gif'); - while (1) { - var next_tr = last_tr.next(); - if (next_tr.attr('level') <= this.one_object.clicked_tr_level || next_tr.length == 0) break; - next_tr.hide(); - last_tr = next_tr; - } - } - tr_to_move.reverse(); - _.each(tr_to_move, function(rec) { - $(last_tr).after(rec); - }); - self.do_save_update_arch("down"); - } - }, - do_node_up: function(cur_tr, img) { - var self = this, side = cur_tr, tr_to_move = []; - tr_to_move.push(side); - while (1) { - var prev_tr = cur_tr.prev(); - if (this.one_object.clicked_tr_level >= parseInt(prev_tr.attr('level')) || prev_tr.length == 0) { - last_tr = prev_tr; - break; - } - cur_tr = prev_tr; - } - if (img) { - self.edit_xml_dialog.$element.find("img[id='parentimg-" + this.one_object.clicked_tr_id + "']"). - attr('src', '/web/static/src/img/expand.gif'); - while (1) { - next_tr = side.next(); - if (parseInt(next_tr.attr('level')) <= this.one_object.clicked_tr_level || next_tr.length == 0) { - break; - } else { - next_tr.hide(); - tr_to_move.push(next_tr); - side = next_tr; - } - } - } - if (last_tr.length != 0 && parseInt(last_tr.attr('level')) == this.one_object.clicked_tr_level && - (self.edit_xml_dialog.$element.find(last_tr).find('a').text()).search("view_id") == -1) { - _.each(tr_to_move, function(rec) { - $(last_tr).before(rec); - }); - self.do_save_update_arch("up"); - } - }, - do_save_update_arch: function(move_direct, update_values) { - var self = this; - var arch = _.detect(self.one_object['arch'], function(element) - {return element.view_id == self.one_object.clicked_tr_view[0]}); - var obj = self.get_object_by_id(this.one_object.clicked_tr_view[1],this.one_object['main_object'], []); - //for finding xpath tag from inherit view - var xml_arch = QWeb.load_xml(arch.arch); - if (xml_arch.childNodes[0].tagName == "data") { - var check_list = _.flatten(obj[0].child_id[0].att_list); - var children = _.filter(xml_arch.childNodes[0].childNodes, function (child) { - return child.nodeType == 1; - }); - arch.arch = _.detect(children, function(xml_child) { - var temp_obj = self.create_View_Node(xml_child), - insert = _.intersection(_.flatten(temp_obj.att_list),_.uniq(check_list)); - if (insert.length == _.uniq(check_list).length ) {return xml_child;} - }); - xml_arch = QWeb.load_xml(arch.arch); - } - return self.do_save_xml(xml_arch.documentElement, obj[0].child_id[0],obj[0].child_id, move_direct, update_values,arch); - }, - get_object_by_id: function(id, one_object, result) { - var self = this; - if (result.length == 0 ) { - var check = _.detect(one_object , function(obj) { - return id == obj.id; - }); - if (check) {result.push(check);} - _.each(one_object, function(obj) { - self.get_object_by_id(id,obj.child_id, result); - }); - } - return result; - }, - create_clone: function(clone, new_node_obj){ - var self = this; - clone.find('a').text(new_node_obj.name); - ($(clone.find('a').parent()).siblings('td')).css( "padding-left", 20 * new_node_obj.level); - clone.attr("id", "viewedit-" + new_node_obj.id); - clone.attr("level", new_node_obj.level); - clone.find("img[id^='parentimg-']").remove(); - clone.bind("click",function(){ - self.do_select_row(this.id.split('-')[1]); - }); - clone.find("img[id^='side-']").click(function() { - self.on_select_img(this); - }); - return clone; - }, - do_save_xml: function(arch1, obj, child_list, move_direct, update_values, arch){ - var self = this, children_list = $(arch1).children(),list_obj_xml; - try{list_obj_xml = _.zip(children_list, obj.child_id);}catch(err){return;} - if (this.one_object.clicked_tr_id) { - if (obj.id == this.one_object.clicked_tr_id) { - var parent = false, index = _.indexOf(child_list, obj); - if (move_direct == "down") { - var next = $(arch1).next(); - $(next).after(arch1); - var re_insert_obj = child_list.splice(index, 1); - child_list.splice(index+1, 0, re_insert_obj[0]); - parent = $(arch1).parents(); - } else if (move_direct == "up") { - var prev = $(arch1).prev(); - $(prev).before(arch1); - var re_insert_obj = child_list.splice(index, 1); - child_list.splice(index-1, 0, re_insert_obj[0]); - parent = $(arch1).parents(); - } else if (move_direct == "update_node") { - _.each(update_values, function(val){ - if (val[1]) $(arch1)[0].setAttribute(val[0], val[1]); - else $(arch1)[0].removeAttribute(val[0]); - }); - var new_obj = self.create_View_Node(arch1); - new_obj.id = obj.id,new_obj.child_id = obj.child_id; - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+this.one_object.clicked_tr_id+"']"). - find('a').text(new_obj.name); - child_list.splice(index, 1, new_obj); - parent = $(arch1).parents(); - } else if(move_direct == "add_node") { - var tr_click = self.edit_xml_dialog.$element.find("tr[id='viewedit-"+self.one_object.clicked_tr_id+"']"), - temp_xml = QWeb.load_xml(update_values[0]), - object_xml = self.create_View_Node(temp_xml.childNodes[0]); - (update_values[1] == "Inside")? object_xml.level = obj.level + 1:object_xml.level = obj.level; - var clone = self.create_clone(tr_click.clone(),object_xml), - after_append = _.detect(self.one_object['parent_child_id'],function(ele){ - return self.one_object.clicked_tr_id == ele.key; - }); - after_append = (after_append)?_.last(after_append.value):self.one_object.clicked_tr_id; - switch (update_values[1]) { - case "After": - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+after_append+"']").after(clone); - $(arch1).after($(update_values[0])); - child_list.splice(index + 1, 0, object_xml); - break; - case "Before": - tr_click.before(clone); - $(arch1).before($(update_values[0])); - child_list.splice(index - 1, 0, object_xml); - break; - case "Inside": - if (tr_click.find("img[id^='parentimg-']").length == 0) { - ($(tr_click.find('a').parent()).siblings('td')) - .append($('<img width="16" height="16"></img>').attr('src', '/web/static/src/img/collapse.gif'). - attr('id','parentimg-'+ self.one_object.clicked_tr_id).click(function(){ - self.do_parent_img_hide_show(this); - })); - } - $(arch1).append($(update_values[0])); - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+after_append+"']").after(clone); - obj.child_id.push(object_xml); - break; - } - self.edit_xml_dialog.$element. - find("tr[id='viewedit-" + object_xml.id + "']").removeClass('ui-selected'); - parent = $(arch1).parents(); - } else if (move_direct == "remove_node") { - parent = $(arch1).parents(); - if (parent.length == 0 || (parent[0].tagName.toLowerCase() == "data")) { - self.one_object.clicked_tr_id = self.one_object.clicked_tr_id -1; - self.one_object.clicked_tr_level = self.one_object.clicked_tr_level - 1; - (parent.length == 0)?parent.push("remove_view"):false; - } - $(arch1).remove(); - child_list.splice(index,1); - var cur_tr = self.edit_xml_dialog.$element. - find("tr[id='viewedit-" + self.one_object.clicked_tr_id + "']"); - _.each(self.get_list_tr(cur_tr,self.one_object.clicked_tr_level), function(tr_element){ - tr_element.remove(); - }); - cur_tr.remove(); - var parent_img = _.detect(self.one_object['parent_child_id'],function(element){ - return _.include(element.value, self.one_object.clicked_tr_id); - }); - if(parent_img.value.length == 1){ - self.edit_xml_dialog.$element. - find("tr[id='viewedit-"+parent_img.key+"']"). - find("img[id^='parentimg-']").remove(); - } - self.one_object['parent_child_id'] = self.parent_child_list(self.one_object['main_object'],[]); - } - var convert_to_utf = (parent.length != 0)? parent[parent.length-1]: arch1; - if (convert_to_utf != "remove_view") { - convert_to_utf = QWeb.tools.xml_node_to_string(convert_to_utf); - convert_to_utf = convert_to_utf.replace('xmlns="http://www.w3.org/1999/xhtml"', ""); - convert_to_utf = '<?xml version="1.0"?>' + convert_to_utf; - arch.arch = convert_to_utf; - this.dataset.write(this.one_object.clicked_tr_view[0] ,{"arch":convert_to_utf}, function(r) {}); - } else { - this.dataset.unlink([this.one_object.clicked_tr_view[0]],function(res) {}); - } - if(move_direct == "add_node"){ - self.add_node_dialog.close(); - self.on_select_img(clone.find("img[id='side-edit']")[0]); - self.one_object['parent_child_id'] = self.parent_child_list(self.one_object['main_object'],[]); - } - } - if (obj.level <= this.one_object.clicked_tr_level) { - _.each(list_obj_xml, function(child_node) { - self.do_save_xml(child_node[0], child_node[1], obj.child_id, move_direct, update_values, arch); - }); - } - } - }, - on_expand: function(expand_img){ - var level = parseInt($(expand_img).closest("tr[id^='viewedit-']").attr('level')); - var cur_tr = $(expand_img).closest("tr[id^='viewedit-']"); - _.each(this.get_list_tr(cur_tr,level), function(tr_element){ - tr_element.hide(); - }); - }, - get_list_tr: function(cur_tr,level){ - tr_list = []; - while (1) { - var nxt_tr = cur_tr.next(); - if (parseInt(nxt_tr.attr('level')) > level) { - cur_tr = nxt_tr; - tr_list.push(nxt_tr); - } else return tr_list; - } - }, - on_collapse: function(collapse_img) { - var self = this, id = collapse_img.id.split('-')[1]; - var datas = _.detect(self.one_object['parent_child_id'] , function(res) { - return res.key == id; - }); - _.each(datas.value, function (rec) { - var tr = self.edit_xml_dialog.$element.find("tr[id='viewedit-" + rec + "']"); - tr.find("img[id='parentimg-" + rec + "']").attr('src', '/web/static/src/img/expand.gif'); - tr.show(); - }); - }, - on_edit_node: function(properties){ - var self = this; - this.edit_node_dialog = new instance.web.Dialog(this,{ - title: _t("Properties"), - width: 450, - buttons: [ - {text: _t("Update"), click: function () { - var warn = false, update_values = []; - _.each(self.edit_widget, function(widget) { - if (widget.is_invalid) { - warn = true; - return false; - } - if (widget.dirty && !widget.is_invalid) { - update_values.push([widget.name, widget.get_value()]); - } - }); - if (warn) { - self.on_valid_create_view(self.edit_widget); - } else { - self.do_save_update_arch("update_node", update_values); - self.edit_node_dialog.close(); - } - }}, - {text: _t("Cancel"), click: function () { self.edit_node_dialog.close(); }} - ] - }).open(); - var _PROPERTIES_ATTRIBUTES = { - 'name' : {'name':'name', 'string': 'Name', 'type': 'char'}, - 'string' : {'name':'string', 'string': 'String', 'type': 'char'}, - 'required' : {'name':'required', 'string': 'Required', 'type': 'boolean'}, - 'readonly' : {'name':'readonly', 'string': 'Readonly', 'type': 'boolean'}, - 'invisible' : {'name':'invisible', 'string': 'Invisible', 'type': 'boolean'}, - 'domain' : {'name':'domain', 'string': 'Domain', 'type': 'char'}, - 'context' : {'name':'context', 'string': 'Context', 'type': 'char'}, - 'limit' : {'name':'limit', 'string': 'Limit', 'type': 'float'}, - 'min_rows' : {'name':'min_rows', 'string': 'Minimum rows', 'type': 'float'}, - 'date_start' : {'name':'date_start', 'string': 'Start date', 'type': 'char'}, - 'date_delay' : {'name':'date_delay', 'string': 'Delay date', 'type': 'char'}, - 'day_length' : {'name':'day_length', 'string': 'Day length', 'type': 'char'}, - 'mode' : {'name':'mode', 'string': 'Mode', 'type': 'char'}, - 'align' : {'name':'align', 'string': 'Alignment ', 'type': 'selection', 'selection': [['', ''], ['0.0', 'Left'], ['0.5', 'Center'], ['1.0', 'Right']]}, - 'icon' : {'name':'icon', 'string': 'Icon', 'type': 'selection', 'selection': _ICONS}, - 'type' : {'name':'type', 'string': 'Type', 'type': 'selection', 'selection': [['', ''], ['action', 'Action'], ['object', 'Object'], ['workflow', 'Workflow'], ['server_action', 'Server Action']]}, - 'special' : {'name':'special', 'string': 'Special', 'type': 'selection', 'selection': [['',''],['save', 'Save Button'], ['cancel', 'Cancel Button'], ['open', 'Open Button']]}, - 'target' : {'name':'target', 'string': 'Target', 'type': 'selection', 'selection': [['', ''], ['new', 'New Window']]}, - 'confirm' : {'name':'confirm', 'string': 'Confirm', 'type': 'char'}, - 'style' : {'name':'style', 'string': 'Style', 'type': 'selection', 'selection':[["",""],["1", "1"],["1-1", "1-1"],["1-2", "1-2"],["2-1", "2-1"],["1-1-1", "1-1-1"]]}, - 'filename' : {'name':'filename', 'string': 'File Name', 'type': 'char'}, - 'width' : {'name':'width', 'string': 'Width', 'type': 'float'}, - 'height' : {'name':'height', 'string': 'Height', 'type': 'float'}, - 'attrs' : {'name':'attrs', 'string': 'Attrs', 'type': 'char'}, - 'col' : {'name':'col', 'string': 'col', 'type': 'float'}, - 'link' : {'name':'link', 'string': 'Link', 'type': 'char'}, - 'position' : {'name':'position', 'string': 'Position', 'type': 'selection', 'selection': [['',''],['after', 'After'],['before', 'Before'],['inside', 'Inside'],['replace', 'Replace']]}, - 'states' : {'name':'states', 'string': 'states', 'type': 'char'}, - 'eval' : {'name':'eval', 'string': 'Eval', 'type': 'char'}, - 'ref' : {'name':'ref', 'string': 'Ref', 'type': 'char'}, - 'on_change' : {'name':'on_change', 'string': 'On change', 'type': 'char'}, - 'nolabel' : {'name':'nolabel', 'string': 'No label', 'type': 'boolean'}, - 'completion' : {'name':'completion', 'string': 'Completion', 'type': 'boolean'}, - 'colspan' : {'name':'colspan', 'string': 'Colspan', 'type': 'float'}, - 'widget' : {'name':'widget', 'string': 'widget', 'type': 'selection'}, - 'colors' : {'name':'colors', 'string': 'Colors', 'type': 'char'}, - 'editable' : {'name':'editable', 'string': 'Editable', 'type': 'selection', 'selection': [["",""],["top","Top"],["bottom", "Bottom"]]}, - 'groups' : {'name':'groups', 'string': 'Groups', 'type': 'selection_multi'}, - 'fonts' : {'name':'fonts', 'string': 'fonts', 'type': 'char'}, - }; - var arch_val = self.get_object_by_id(this.one_object.clicked_tr_id,this.one_object['main_object'], []); - this.edit_node_dialog.$element.append('<table id="rec_table" style="width:400px" class="oe_form"></table>'); - this.edit_widget = []; - self.ready = $.when(self.on_groups(properties)).then(function () { - _PROPERTIES_ATTRIBUTES['groups']['selection'] = self.groups; - var values = _.keys( instance.web.form.widgets.map); - values.push(''); - values.sort(); - _PROPERTIES_ATTRIBUTES['widget']['selection'] = values; - var widgets = _.filter(_PROPERTIES_ATTRIBUTES, function (property) { return _.include(properties, property.name)}); - _.each(widgets, function(widget) { - var type_widget = new (self.property.get_any([widget.type])) (self.edit_node_dialog, widget); - var value = _.detect(arch_val[0]['att_list'],function(res) { - return res instanceof Array? _.include(res, widget.name): false; - }); - - value = value instanceof Array ? value[1] : value; - self.edit_node_dialog.$element.find('table[id=rec_table]').append('<tr><td align="right">' + widget.string + ':</td>' + type_widget.render() + '</tr>'); - type_widget.start(); - type_widget.set_value(value); - self.edit_widget.push(type_widget); - }); - }); - }, - //for getting groups - on_groups: function(properties){ - var self = this, - def = $.Deferred(); - if (!_.include(properties, 'groups')) { - self.groups = false; - def.resolve(); - } - var group_ids = [], group_names = {}, groups = []; - var res_groups = new instance.web.DataSetSearch(this,'res.groups', null, null), - model_data = new instance.web.DataSetSearch(self,'ir.model.data', null, null); - res_groups.read_slice([], {}).done(function (res_grp) { - _.each(res_grp, function (res) { - var key = res.id; - group_names[key]=res.full_name; - group_ids.push(res.id); - }); - model_data.read_slice([], {domain: [ - ['res_id', 'in', group_ids], - ['model', '=', 'res.groups'] - ]}).done(function (model_grp) { - _.each(model_grp, function (res_group) { - groups.push([res_group.module + "." + res_group.name, group_names[res_group.res_id]]); - }); - self.groups = groups; - def.resolve(); - }); - }); - return def.promise(); - }, - on_add_node: function(properties, fields){ - var self = this; - var render_list = [{'name': 'node_type','selection': _.keys(_CHILDREN).sort(), 'value': 'field', 'string': 'Node Type','type': 'selection'}, - {'name': 'field_value','selection': fields, 'value': false, 'string': '','type': 'selection'}, - {'name': 'position','selection': ['After','Before','Inside'], 'value': false, 'string': 'Position','type': 'selection'}]; - this.add_widget = []; - this.add_node_dialog = new instance.web.Dialog(this,{ - title: _t("Properties"), - width: 450, - buttons: [ - {text: _t("Update"), click: function() { - var check_add_node = true, values = {}; - _.each(self.add_widget, function(widget) { - values[widget.name] = widget.get_value() || false; - }); - (values.position == "Inside")? - check_add_node =(_.include(_CHILDREN[properties[0]],values.node_type))?true:false: - check_add_node =(_.include(_CHILDREN[properties[1]],values.node_type))?true:false; - if(values.node_type == "field" && check_add_node ) - {check_add_node = (values.field_value != " ")?true:false; - } - if(check_add_node){ - var tag = (values.node_type == "field")? - _.str.sprintf("<%s name='%s'> </%s>",values.node_type,values.field_value,values.node_type): - _.str.sprintf("<%s> </%s>",values.node_type,values.node_type); - self.do_save_update_arch("add_node", [tag, values.position]); - } else { - alert("Can't Update View"); - } - }}, - {text: _t("Cancel"), click: function() { self.add_node_dialog.close(); }} - ] - }).open(); - this.add_node_dialog.$element.append('<table id="rec_table" style="width:420px" class="oe_form"><tbody><tr></tbody></table>'); - var table_selector = self.add_node_dialog.$element.find('table[id=rec_table] tbody'); - _.each(render_list, function(node) { - type_widget = new (self.property.get_any([node.type])) (self.add_node_dialog, node); - if (node.name == "position") { - table_selector.append('</tr><tr><td align="right" width="100px">' + node.string + '</td>' + type_widget.render() + '</tr>'); - } else { - table_selector.append('<td align="right">' + node.string + '</td>' + type_widget.render() ); - if (node.name == "field_value") { - table_selector.append('<td id="new_field" align="right" width="100px"> <button>New Field</button></td>'); - } - } - type_widget.start(); - type_widget.set_value(node.value); - self.add_widget.push(type_widget); - }); - table_selector.find("td[id^=]").attr("width","100px"); - self.add_node_dialog.$element.find('#new_field').click(function() { - model_data = new instance.web.DataSetSearch(self,'ir.model', null, null); - model_data.read_slice([], {domain: [['model','=', self.model]]}).then(function(result) { - self.render_new_field(result[0]); - }); - }); - }, - render_new_field :function( result ) { - var self = this; - var action = { - context: {'default_model_id': result.id, 'manual': true, 'module' : result.model}, - 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_widget.views['form'].controller; - controller.on_button_cancel.add_last(function(){ - action_manager.destroy() - }); - controller.do_save.add_last(function(){ - action_manager.destroy(); - var value =controller.fields.name.get('value'); - self.add_node_dialog.$element.find('select[id=field_value]').append($("<option selected></option>").attr("value",value).text(value)); - _.detect(self.add_widget,function(widget){ - widget.name == "field_value"? widget.selection.push(value): false; - }); - }); - }); - } -}); -instance.web.ViewEditor.Field = instance.web.Class.extend({ - init: function(view, widget) { - this.$element = view.$element; - this.dirty = false; - this.name = widget.name; - this.selection = widget.selection || []; - this.required = widget.required || false; - this.string = widget.string || ""; - this.type = widget.type; - this.is_invalid = false; - }, - start: function () { - this.update_dom(); - }, - update_dom: function() { - this.$element.find("td[id=" + this.name + "]").toggleClass('invalid', this.is_invalid); - this.$element.find("td[id=" + this.name + "]").toggleClass('required', this.required); - }, - on_ui_change: function() { - this.validate(); - this.dirty = true; - this.update_dom(); - }, - validate: function() { - this.is_invalid = false; - try { - var value = instance.web.parse_value(this.get_value(), this, ''); - this.is_invalid = this.required && value === ''; - } catch(e) { - this.is_invalid = true; - } - }, - render: function() { - return _.str.sprintf("<td id = %s>%s</td>", this.name, QWeb.render(this.template, {widget: this})) - } -}); -instance.web.ViewEditor.FieldBoolean = instance.web.ViewEditor.Field.extend({ - template : "vieweditor_boolean", - start: function() { - var self = this; - this._super(); - this.$element.find("input[id="+ self.name+"]").change(function() { - self.on_ui_change(); - }); - }, - set_value: function(value) { - if (value) { - this.$element.find("input[id=" + this.name+ "]").attr('checked', true); - } - }, - get_value: function() { - return this.$element.find("input[id=" + this.name + "]").is(':checked')? "1" : null; - } -}); -instance.web.ViewEditor.FieldChar = instance.web.ViewEditor.Field.extend({ - template : "vieweditor_char", - start: function () { - var self = this; - this._super(); - this.$element.find("input[id="+ this.name+"]").css('width','100%').change(function() { - self.on_ui_change(); - }); - }, - set_value: function(value) { - this.$element.find("input[id=" + this.name + "]").val(value); - }, - get_value: function() { - return this.$element.find("input[id=" + this.name + "]").val(); - } -}); -instance.web.ViewEditor.FieldSelect = instance.web.ViewEditor.Field.extend({ - template : "vieweditor_selection", - start: function () { - var self = this; - this._super(); - this.$element.find("select[id=" + this.name + "]").css('width', '100%').change(function() { - self.on_ui_change(); - if (self.name == "node_type") { - if (self.get_value() == "field") { - self.$element.find('#new_field').show(); - self.$element.find("select[id=field_value]").show(); - } else { - self.$element.find('#new_field').hide(); - self.$element.find("select[id=field_value]").hide(); - } - } - }); - }, - set_value: function(value) { - var index = 0; - value = value === null? false: value; - for (var i = 0, ii = this.selection.length; i < ii; i++) { - if ((this.selection[i] instanceof Array && this.selection[i][0] === value) || this.selection[i] === value) index = i; - } - this.$element.find("select[id=" + this.name + "]")[0].selectedIndex = index; - }, - get_value: function() { - return this.$element.find("select[id=" + this.name + "]").val(); - } -}); -instance.web.ViewEditor.FieldSelectMulti = instance.web.ViewEditor.FieldSelect.extend({ - start: function () { - this._super(); - this.$element.find("select[id=" + this.name + "]").css('height', '100px').attr("multiple", true); - }, - set_value: function(value) { - var self = this; - self.$element.find("#groups option").attr("selected",false); - if (!value) return false; - _.each(this.selection, function(item) { - if (_.include(value.split(','), item[0])) { - self.$element.find("select[id="+self.name+"] option[value='" + item[0] +"']").attr("selected",1) - } - }); - } -}); -instance.web.ViewEditor.FieldFloat = instance.web.ViewEditor.FieldChar.extend({ -}); - -var _PROPERTIES = { - 'field' : ['name', 'string', 'required', 'readonly','invisible', 'domain', 'context', 'nolabel', 'completion', - 'colspan', 'widget', 'eval', 'ref', 'on_change', 'attrs', 'groups'], - 'form' : ['string', 'col', 'link'], - 'notebook' : ['colspan', 'position', 'groups'], - 'page' : ['string', 'states', 'attrs', 'groups'], - 'group' : ['string', 'col', 'colspan','invisible', 'states', 'attrs', 'groups'], - 'image' : ['filename', 'width', 'height', 'groups'], - 'separator' : ['string', 'colspan', 'groups'], - 'label': ['string', 'align', 'colspan', 'groups'], - 'button': ['name', 'string', 'icon', 'type', 'states', 'readonly', 'special', 'target', 'confirm', 'context', 'attrs', 'colspan', 'groups'], - 'newline' : [], - 'board': ['style'], - 'column' : [], - 'action' : ['name', 'string', 'colspan', 'groups'], - 'tree' : ['string', 'colors', 'editable', 'link', 'limit', 'min_rows', 'fonts'], - 'graph' : ['string', 'type'], - 'calendar' : ['string', 'date_start', 'date_stop', 'date_delay', 'day_length', 'color', 'mode'] -}; -var _CHILDREN = { - 'form': ['notebook', 'group', 'field', 'label', 'button','board', 'newline', 'separator'], - 'tree': ['field'], - 'graph': ['field'], - 'calendar': ['field'], - 'notebook': ['page'], - 'page': ['notebook', 'group', 'field', 'label', 'button', 'newline', 'separator'], - 'group': ['field', 'label', 'button', 'separator', 'newline','group'], - 'board': ['column'], - 'action': [], - 'field': ['form', 'tree', 'graph','field'], - 'label': [], - 'button' : [], - 'newline': [], - 'separator': [], - 'sheet' :['group','field','notebook','label','separator','div','page'], - 'kanban' : ['field'], - 'html_tag':['notebook', 'group', 'field', 'label', 'button','board', 'newline', 'separator'] -//e.g.:xyz 'td' : ['field'] -}; -// Generic html_tag list and can be added html tag in future. It's support above _CHILDREN dict's *html_tag* by default. -// For specific child node one has to define tag above and specify children tag in list. Like above xyz example. -var html_tag = ['div','h1','h2','h3','h4','h5','h6','td','tr']; - -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 b103ce83699..b8acdfea4cd 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -33,6 +33,11 @@ instance.web.form.FieldManagerMixin = { }; instance.web.views.add('form', 'instance.web.FormView'); +/** + * 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 @@ -55,6 +60,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM * @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; @@ -73,6 +79,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM _.defaults(this.options, { "not_interactible_on_create": false, "initial_mode": "view", + "disable_autofocus": false, }); this.is_initialized = $.Deferred(); this.mutating_mutex = new $.Mutex(); @@ -80,15 +87,24 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.reload_mutex = new $.Mutex(); this.__clicked_inside = false; this.__blur_timeout = null; - this.rendering_engine = new instance.web.form.FormRenderingEngineReadonly(this); + this.rendering_engine = new instance.web.form.FormRenderingEngine(this); this.qweb = null; // A QWeb instance will be created if the view is a QWeb template + self.set({actual_mode: self.options.initial_mode}); + this.has_been_loaded.then(function() { + self.on("change:actual_mode", self, self.check_actual_mode); + self.check_actual_mode(); + self.on("change:actual_mode", self, self.init_pager); + self.init_pager(); + }); }, destroy: function() { _.each(this.get_widgets(), function(w) { w.off('focused blurred'); w.destroy(); }); - this.$element.off('.formBlur'); + if (this.$element) { + this.$element.off('.formBlur'); + } this._super(); }, on_loaded: function(data) { @@ -125,17 +141,6 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.$buttons.on('click','.oe_form_button_save',this.on_button_save); this.$buttons.on('click','.oe_form_button_cancel',this.on_button_cancel); - this.$pager = $(QWeb.render("FormView.pager", {'widget':self})); - if (this.options.$pager) { - this.$pager.appendTo(this.options.$pager); - } else { - this.$element.find('.oe_form_pager').replaceWith(this.$pager); - } - this.$pager.on('click','a[data-pager-action]',function() { - var action = $(this).data('pager-action'); - self.on_pager_action(action); - }); - this.$sidebar = this.options.$sidebar || this.$element.find('.oe_form_sidebar'); if (!this.sidebar && this.options.$sidebar) { this.sidebar = new instance.web.Sidebar(this); @@ -146,12 +151,20 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM 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(); } }, + { 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.$element.find(".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) + } + }); + return $.when(); }, extract_qweb_template: function(fvg) { @@ -239,6 +252,13 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } } }, + /** + * + * @param {Object} [options] + * @param {Boolean} [editable=false] whether the form should be switched to edition mode. A value of ``false`` will keep the current mode. + * @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 || {}; @@ -251,25 +271,34 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM if (this.$pager) { this.$pager.show(); } - this.$element.show().css('visibility', 'hidden'); + this.$element.show().css({ + opacity: '0', + filter: 'alpha(opacity = 0)' + }); this.$element.add(this.$buttons).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 } + + 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(self.on_record_loaded); + }); + } + return shown.pipe(function() { + if (options.editable) { + self.to_edit_mode(); } - result.pipe(function() { - if (options.editable) { - self.set({mode: "edit"}); - } - self.$element.css('visibility', 'visible'); + self.$element.css({ + opacity: '1', + filter: 'alpha(opacity = 100)' }); - return result; }); }, do_hide: function () { @@ -292,7 +321,8 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM return $.Deferred().reject(); } this.datarecord = record; - this.set({ 'title' : record.id ? record.name : "New record" }); + this._actualize_mode(); + this.set({ 'title' : record.id ? record.display_name : "New record" }); if (this.qweb) { this.kill_current_form(); @@ -326,10 +356,27 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } if (record.id) { self.do_push_state({id:record.id}); + } else { + self.do_push_state({}); } self.$element.add(self.$buttons).removeClass('oe_form_dirty'); + self.autofocus(); }); }, + /** + * Loads and sets up the default values for the model as the current + * record + * + * @return {$.Deferred} + */ + load_defaults: function () { + var keys = _.keys(this.fields_view.fields); + if (keys.length) { + return this.dataset.default_get(keys) + .pipe(this.on_record_loaded); + } + return this.on_record_loaded({}); + }, on_form_changed: function() { this.trigger("view_content_has_changed"); }, @@ -355,6 +402,24 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM this.reload(); } }, + init_pager: function() { + var self = this; + if (this.$pager) + this.$pager.remove(); + if (this.get("actual_mode") === "create") + return; + this.$pager = $(QWeb.render("FormView.pager", {'widget':self})); + if (this.options.$pager) { + this.$pager.appendTo(this.options.$pager); + } else { + this.$element.find('.oe_form_pager').replaceWith(this.$pager); + } + this.$pager.on('click','a[data-pager-action]',function() { + var action = $(this).data('pager-action'); + self.on_pager_action(action); + }); + this.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() @@ -554,9 +619,33 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM return $.Deferred().reject(); } }, - switch_mode: function() { + /** + * 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"); + }, + /** + * Reactualize actual_mode. + */ + _actualize_mode: function(switch_to) { + var mode = switch_to || this.get("actual_mode"); + if (! this.datarecord.id) { + mode = "create"; + } + 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.$element.removeClass('oe_form_editable').addClass('oe_form_readonly'); self.$buttons.find('.oe_form_buttons_edit').hide(); self.$buttons.find('.oe_form_buttons_view').show(); @@ -572,53 +661,53 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM _.each(this.fields,function(field){ field.set({"force_readonly": false}); }); - var fields_order = self.fields_order.slice(0); - if (self.default_focus_field) { - fields_order.unshift(self.default_focus_field); + 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 = self.fields[fields_order[i]]; - if (!field.get('effective_invisible') && !field.get('effective_readonly') && field.focus() !== false) { - break; + var field = this.fields[fields_order[i]]; + if (!field.get('effective_invisible') && !field.get('effective_readonly')) { + if (field.focus() !== false) { + break; + } } } } }, on_button_save: function() { var self = this; - return this.do_save().then(function(result) { - self.set({mode: "view"}); + return this.do_save().then(function(result) { + self.to_view_mode(); }); }, on_button_cancel: function(event) { if (this.can_be_discarded()) { - this.set({mode: "view"}); - this.on_record_loaded(this.datarecord); + if (this.get('actual_mode') === 'create') { + this.trigger('history_back'); + } else { + this.to_view_mode(); + this.on_record_loaded(this.datarecord); + } } return false; }, on_button_new: function() { var self = this; - this.set({mode: "edit"}); - var def = $.Deferred(); - $.when(this.has_been_loaded).then(function() { + this.to_edit_mode(); + return $.when(this.has_been_loaded).pipe(function() { if (self.can_be_discarded()) { - var keys = _.keys(self.fields_view.fields); - if (keys.length) { - self.dataset.default_get(keys).pipe(self.on_record_loaded).then(function() { - def.resolve(); - }); - } else { - self.on_record_loaded({}).then(function() { - def.resolve(); - }); - } + return self.load_defaults(); } }); - return def.promise(); }, on_button_edit: function() { - return this.set({mode: "edit"}); + return this.to_edit_mode(); }, on_button_create: function() { this.dataset.index = null; @@ -631,7 +720,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM self.dataset.call('copy', [self.datarecord.id, {}, self.dataset.context]).then(function(new_id) { return self.on_created({ result : new_id }); }).then(function() { - return self.set({mode: "edit"}); + return self.to_edit_mode(); }).then(function() { def.resolve(); }); @@ -674,13 +763,14 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM values = {}, first_invalid_field = null; for (var f in self.fields) { + if (!self.fields.hasOwnProperty(f)) { continue; } f = self.fields[f]; if (!f.is_valid()) { form_invalid = true; if (!first_invalid_field) { first_invalid_field = f; } - } else if (f.name !== 'id' && !f.get("readonly") && (!self.datarecord.id || f._dirty_flag)) { + } else if (f.name !== 'id' && (!self.datarecord.id || (!f.get("readonly") && f._dirty_flag))) { // Special case 'id' field, do not save this field // on 'create' : save all non readonly fields // on 'edit' : save non readonly modified fields @@ -689,8 +779,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM } if (form_invalid) { self.set({'display_invalid_fields': true}); - for (var f in self.fields) { - self.fields[f]._check_css_flags(); + for (var g in self.fields) { + if (!self.fields.hasOwnProperty(g)) { continue; } + self.fields[g]._check_css_flags(); } first_invalid_field.focus(); self.on_invalid(); @@ -699,16 +790,16 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM self.set({'display_invalid_fields': false}); var save_deferral; if (!self.datarecord.id) { - //console.log("FormView(", self, ") : About to create", values); + // console.log("FormView(", self, ") : About to create", values); save_deferral = self.dataset.create(values).pipe(function(r) { return self.on_created(r, undefined, prepend_on_create); }, null); } else if (_.isEmpty(values) && ! self.force_dirty) { - //console.log("FormView(", self, ") : Nothing to save"); + // console.log("FormView(", self, ") : Nothing to save"); save_deferral = $.Deferred().resolve({}).promise(); } else { self.force_dirty = false; - //console.log("FormView(", self, ") : About to save", values); + // console.log("FormView(", self, ") : About to save", values); save_deferral = self.dataset.write(self.datarecord.id, values, {}).pipe(function(r) { return self.on_saved(r); }, null); @@ -722,14 +813,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM });}); }, on_invalid: function() { - var msg = "<ul>"; - _.each(this.fields, function(f) { - if (!f.is_valid()) { - msg += "<li>" + f.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) { @@ -790,8 +882,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM if (self.dataset.index == null || self.dataset.index < 0) { return $.when(self.on_button_new()); } else { - return self.dataset.read_index(_.keys(self.fields_view.fields), { - context : { 'bin_size' : true } + var fields = _.keys(self.fields_view.fields); + fields.push('display_name'); + return self.dataset.read_index(fields, { + context : { 'bin_size' : true, 'future_display_name' : true } }).pipe(self.on_record_loaded); } }); @@ -802,15 +896,15 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM }); }, 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; }, @@ -865,6 +959,9 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM return option[0] === value; })[1]; break; + case 'many2one': + displayed = field.get_displayed(); + break; } return { @@ -918,7 +1015,7 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM register_field: function(field, name) { this.fields[name] = field; this.fields_order.push(name); - if (field.node.attrs.default_focus == '1') { + if (JSON.parse(field.node.attrs.default_focus || "0")) { this.default_focus_field = field; } @@ -940,7 +1037,10 @@ instance.web.FormView = instance.web.View.extend(instance.web.form.FieldManagerM return this.fields_view.fields[field_name]; }, is_create_mode: function() { - return !this.datarecord.id; + return this.get("actual_mode") === "create"; + }, + open_translate_dialog: function(field) { + return this._super(field); }, }); @@ -985,7 +1085,6 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt } }); } - selector = 'form[version!="7.0"] page,form[version!="7.0"]'; }, render_to: function($target) { var self = this; @@ -1119,7 +1218,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt if (found) return; - $label = $('<label/>').attr({ + var $label = $('<label/>').attr({ 'for' : name, "modifiers": JSON.stringify({invisible: field_modifiers.invisible}), "string": $field.attr('string'), @@ -1186,8 +1285,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt row_cols = cols; } else if (tagName==='group') { // When <group> <group/><group/> </group>, we need a spacing between the two groups - $child.addClass('oe_group_right') - + $td.addClass('oe_group_right') } row_cols -= colspan; @@ -1213,12 +1311,6 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt $child = $td.children(':first'); switch ($child[0].tagName.toLowerCase()) { case 'separator': - if ($child.attr('orientation') === 'vertical') { - $td.addClass('oe_vertical_separator').attr('width', '1'); - $td.empty(); - row_cols-= $td.attr('colspan') || 1; - total--; - } break; case 'label': if ($child.attr('for')) { @@ -1340,7 +1432,7 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt return $new_label; }, handle_common_properties: function($new_element, $node) { - var str_modifiers = $node.attr("modifiers") || "{}" + var str_modifiers = $node.attr("modifiers") || "{}"; var modifiers = JSON.parse(str_modifiers); var ic = null; if (modifiers.invisible !== undefined) @@ -1351,12 +1443,6 @@ instance.web.form.FormRenderingEngine = instance.web.form.FormRenderingEngineInt }, }); -instance.web.form.FormRenderingEngineReadonly = instance.web.form.FormRenderingEngine.extend({ - alter_field: function(field) { - field.set({"force_readonly": true}); - }, -}); - instance.web.form.FormDialog = instance.web.Dialog.extend({ init: function(parent, options, view_id, dataset) { this._super(parent, options); @@ -1465,24 +1551,27 @@ instance.web.form.compute_domain = function(expr, fields) { */ instance.web.form.InvisibilityChangerMixin = { init: function(field_manager, invisible_domain) { - this._ic_field_manager = field_manager + var self = this; + this._ic_field_manager = field_manager; this._ic_invisible_modifier = invisible_domain; this._ic_field_manager.on("view_content_has_changed", this, function() { - var result = this._ic_invisible_modifier === undefined ? false : - instance.web.form.compute_domain(this._ic_invisible_modifier, this._ic_field_manager.fields); - this.set({"invisible": result}); + var result = self._ic_invisible_modifier === undefined ? false : + instance.web.form.compute_domain( + self._ic_invisible_modifier, + self._ic_field_manager.fields); + self.set({"invisible": result}); }); this.set({invisible: this._ic_invisible_modifier === true, force_invisible: false}); var check = function() { - if (this.get("invisible") || this.get('force_invisible')) { - this.set({"effective_invisible": true}); + if (self.get("invisible") || self.get('force_invisible')) { + self.set({"effective_invisible": true}); } else { - this.set({"effective_invisible": false}); + self.set({"effective_invisible": false}); } }; this.on('change:invisible', this, check); this.on('change:force_invisible', this, check); - _.bind(check, this)(); + check.call(this); }, start: function() { this.on("change:effective_invisible", this, this._check_visibility); @@ -1546,6 +1635,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.Invi var compute_domain = instance.web.form.compute_domain; var to_set = {}; for (var a in this.modifiers) { + if (!this.modifiers.hasOwnProperty(a)) { continue; } if (!_.include(["invisible"], a)) { var val = compute_domain(this.modifiers[a], this.view.fields); to_set[a] = val; @@ -1587,7 +1677,7 @@ instance.web.form.FormWidget = instance.web.Widget.extend(instance.web.form.Invi parent: {} }); if (a_dataset.parent_view) { - fields_values.parent = a_dataset.parent_view.get_fields_values([a_dataset.child_name]); + fields_values.parent = a_dataset.parent_view.get_fields_values([a_dataset.child_name]); } return fields_values; }, @@ -1631,10 +1721,13 @@ instance.web.form.WidgetButton = instance.web.form.FormWidget.extend({ this._super(view, 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() { @@ -1692,11 +1785,7 @@ 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}), @@ -1749,18 +1838,18 @@ instance.web.form.FieldInterface = { /** * 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() {}, @@ -1775,7 +1864,7 @@ instance.web.form.FieldInterface = { */ 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() {}, @@ -1805,6 +1894,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w * @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; @@ -1817,13 +1907,13 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w // some events to make the property "effective_readonly" sync automatically with "readonly" and // "force_readonly" this.set({"readonly": this.modifiers['readonly'] === true}); + this.set({"force_readonly": false}); var test_effective_readonly = function() { - this.set({"effective_readonly": this.get("readonly") || !!this.get("force_readonly")}); + self.set({"effective_readonly": self.get("readonly") || !!self.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)(); - + test_effective_readonly.call(this); this.on("change:value", this, function() { if (! this._inhibit_on_change) this.trigger('changed_value'); @@ -1894,14 +1984,7 @@ instance.web.form.AbstractField = instance.web.form.FormWidget.extend(instance.w } }, 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); + return false; }, /** * Utility method to get the widget options defined in the field xml description. @@ -2000,7 +2083,7 @@ instance.web.form.FieldChar = instance.web.form.AbstractField.extend(instance.we return this.get('value') === '' || this._super(); }, focus: function() { - this.delay_focus(this.$element.find('input:first')); + this.$('input:first').focus(); } }); @@ -2084,6 +2167,9 @@ instance.web.form.FieldFloat = instance.web.form.FieldChar.extend({ value_ = 0; } this._super.apply(this, [value_]); + }, + focus: function () { + this.$('input:first').select(); } }); @@ -2220,8 +2306,9 @@ instance.web.form.FieldDatetime = instance.web.form.AbstractField.extend(instanc 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(); + } } }); @@ -2243,6 +2330,11 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we } else { this.$textarea.attr('disabled', 'disabled'); } + this.$element.keyup(function (e) { + if (e.which === $.ui.keyCode.ENTER) { + e.stopPropagation(); + } + }); this.setupFocus(this.$textarea); }, set_value: function(value_) { @@ -2271,7 +2363,7 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we return this.get('value') === '' || this._super(); }, focus: function($element) { - this.delay_focus(this.$textarea); + this.$textarea.focus(); }, do_resize: function(max_height) { max_height = parseInt(max_height, 10); @@ -2295,9 +2387,58 @@ instance.web.form.FieldText = instance.web.form.AbstractField.extend(instance.we }, }); +/** + * 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.FieldText.extend({ + + initialize_content: function() { + this.$textarea = this.$element.find('textarea'); + var width = ((this.node.attrs || {}).editor_width || 468); + var height = ((this.node.attrs || {}).editor_height || 100); + 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 | size " + + "| removeformat | bullets numbering | outdent " + + "indent | link unlink", + sizes: // sizes in the font size popup + "1,2,3,4,5,6,7", + 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]; + // call super now, because cleditor resets the disable attr + this._super.apply(this, arguments); + // propagate disabled property to cleditor + this.$cleditor.disable(this.$textarea.prop('disabled')); + }, + + set_value: function(value_) { + this._super.apply(this, arguments); + this._dirty_flag = true; + }, + + render_value: function() { + this._super.apply(this, arguments); + this.$cleditor.updateFrame(); + }, + + get_value: function() { + this.$cleditor.updateTextArea(); + return this.$textarea.val(); + }, +}); + instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ template: 'FieldBoolean', start: function() { + var self = this; this._super.apply(this, arguments); this.$checkbox = $("input", this.$element); this.setupFocus(this.$checkbox); @@ -2305,17 +2446,17 @@ instance.web.form.FieldBoolean = instance.web.form.AbstractField.extend({ this.set({'value': this.$checkbox.is(':checked')}); }, this)); var check_readonly = function() { - this.$checkbox.prop('disabled', this.get("effective_readonly")); + self.$checkbox.prop('disabled', self.get("effective_readonly")); }; this.on("change:effective_readonly", this, check_readonly); - _.bind(check_readonly, this)(); + check_readonly.call(this); }, set_value: function(value_) { this._super.apply(this, arguments); this.$checkbox[0].checked = value_; }, focus: function() { - this.delay_focus(this.$checkbox); + this.$checkbox.focus(); } }); @@ -2339,9 +2480,6 @@ instance.web.form.FieldProgressBar = instance.web.form.AbstractField.extend({ } }); -instance.web.form.FieldTextXml = instance.web.form.AbstractField.extend({ -// to replace view editor -}); instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { template: 'FieldSelection', @@ -2410,7 +2548,7 @@ instance.web.form.FieldSelection = instance.web.form.AbstractField.extend(instan return !! value_; }, focus: function() { - this.delay_focus(this.$element.find('select:first')); + this.$element.find('select:first').focus(); } }); @@ -2580,10 +2718,20 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc self.$input.tipsy({ title: function() { - return "No element was selected, you should create or select one from the dropdown list."; + return QWeb.render('Tipsy.alert', { + message: "No element was selected, you should create or select one from the dropdown list." + }); }, trigger:'manual', fade: true, + gravity: 's', + html: true, + opacity: 1, + offset: 4, + }); + + self.$input.on('focus', function() { + self.$input.tipsy("hide"); }); this.$drop_down = this.$element.find(".oe_m2o_drop_down_button"); @@ -2611,7 +2759,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc }); // some behavior for input - this.$input.keydown(function() { + var input_changed = function() { if (self.current_display !== self.$input.val()) { self.current_display = self.$input.val(); if (self.$input.val() === "") { @@ -2621,7 +2769,9 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc 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"); @@ -2637,7 +2787,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc var tip_def = $.Deferred(); var untip_def = $.Deferred(); var tip_delay = 200; - var tip_duration = 3000; + var tip_duration = 15000; var anyoneLoosesFocus = function() { var used = false; if (self.floating) { @@ -2697,6 +2847,8 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc } else if (item.action) { self.floating = true; item.action(); + // Cancel widget blurring, to avoid form blur event + self.trigger('focused'); return false; } }, @@ -2720,7 +2872,6 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc }); this.setupFocus(this.$input.add(this.$follow_button)); }, - render_value: function(no_recurse) { var self = this; if (! this.get("value")) { @@ -2745,12 +2896,24 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc if (!this.get("effective_readonly")) { this.$input.val(str.split("\n")[0]); this.current_display = this.$input.val(); + this.$('.oe_m2o_cm_button').css({'visibility': this.is_false() ? 'hidden' : 'visible'}); } else { - str = _.escape(str).split("\n").join("<br />"); - this.$element.find('a') + var lines = _.escape(str).split("\n"); + var link = ""; + var follow = ""; + if (! this.get_definition_options().highlight_first_line) { + link = lines.join("<br />"); + } else { + link = lines[0]; + follow = _.rest(lines).join("<br />"); + if (follow) + link += "<br />"; + } + var $link = this.$element.find('.oe_form_uri') .unbind('click') - .html(str) - .click(function () { + .html(link); + if (! this.get_definition_options().no_open) + $link.click(function () { self.do_action({ type: 'ir.actions.act_window', res_model: self.field.relation, @@ -2761,6 +2924,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc }); return false; }); + $(".oe_form_m2o_follow", this.$element).html(follow); } }, set_value: function(value_) { @@ -2777,6 +2941,9 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc this._super(value_); this.inhibit_on_change = false; }, + get_displayed: function() { + return this.display_value["" + this.get("value")]; + }, add_id: function(id) { this.display_value = {}; this.set({value: id}); @@ -2785,7 +2952,7 @@ instance.web.form.FieldMany2One = instance.web.form.AbstractField.extend(instanc return ! this.get("value"); }, focus: function () { - this.delay_focus(this.$input); + this.$input.focus(); } }); @@ -2848,7 +3015,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ }, start: function() { this._super.apply(this, arguments); - this.$element.addClass('oe_form_field_one2many'); + this.$element.addClass('oe_form_field oe_form_field_one2many'); var self = this; @@ -2904,13 +3071,14 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ } if(view.view_type === "list") { _.extend(view.options, { + addable: null, selectable: self.multi_selection, sortable: false, import_enabled: false, + deletable: true }); if (self.get("effective_readonly")) { _.extend(view.options, { - addable: null, deletable: null, reorderable: false, }); @@ -2940,7 +3108,6 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ this.views = views; this.viewmanager = new instance.web.form.One2ManyViewManager(this, this.dataset, views, {}); - this.viewmanager.$element.addClass("oe_view_manager_one2many"); this.viewmanager.o2m = self; var once = $.Deferred().then(function() { self.init_form_last_update.resolve(); @@ -2951,8 +3118,11 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ this.viewmanager.on_controller_inited.add_last(function(view_type, controller) { controller.o2m = self; if (view_type == "list") { - if (self.get("effective_readonly")) - controller.set_editable(false); + if (self.get("effective_readonly")) { + controller.on('edit:before', self, function (e) { + e.cancel = true; + }); + } } else if (view_type === "form") { if (self.get("effective_readonly")) { $(".oe_form_buttons", controller.$element).children().remove(); @@ -3084,30 +3254,30 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ save_any_view: function() { if (this.doing_on_change) return false; - return this.session.synchronized_mode(_.bind(function() { - if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view && - this.viewmanager.views[this.viewmanager.active_view] && - this.viewmanager.views[this.viewmanager.active_view].controller) { - var view = this.viewmanager.views[this.viewmanager.active_view].controller; - if (this.viewmanager.active_view === "form") { - if (!view.is_initialized.isResolved()) { - return false; - } - var res = $.when(view.do_save()); - if (!res.isResolved() && !res.isRejected()) { - console.warn("Asynchronous get_value() is not supported in form view."); - } - return res; - } else if (this.viewmanager.active_view === "list") { - var res = $.when(view.ensure_saved()); - if (!res.isResolved() && !res.isRejected()) { - console.warn("Asynchronous get_value() is not supported in list view."); - } - return res; - } - } - return false; - }, this)); + return this.session.synchronized_mode(_.bind(function() { + if (this.viewmanager && this.viewmanager.views && this.viewmanager.active_view && + this.viewmanager.views[this.viewmanager.active_view] && + this.viewmanager.views[this.viewmanager.active_view].controller) { + var view = this.viewmanager.views[this.viewmanager.active_view].controller; + if (this.viewmanager.active_view === "form") { + if (!view.is_initialized.isResolved()) { + return false; + } + var res = $.when(view.do_save()); + if (!res.isResolved() && !res.isRejected()) { + console.warn("Asynchronous get_value() is not supported in form view."); + } + return res; + } else if (this.viewmanager.active_view === "list") { + var res = $.when(view.ensure_saved()); + if (!res.isResolved() && !res.isRejected()) { + console.warn("Asynchronous get_value() is not supported in list view."); + } + return res; + } + } + return false; + }, this)); }, is_syntax_valid: function() { if (!this.viewmanager.views[this.viewmanager.active_view]) @@ -3136,6 +3306,7 @@ instance.web.form.One2ManyViewManager = instance.web.ViewManager.extend({ form: 'instance.web.form.One2ManyFormView', kanban: 'instance.web.form.One2ManyKanbanView', }); + this.__ignore_blur = false; }, switch_view: function(mode, unused) { if (mode !== 'form') { @@ -3185,19 +3356,35 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ this._super(parent, dataset, view_id, _.extend(options || {}, { ListType: instance.web.form.One2ManyList })); + this.on('edit:before', this, this.proxy('_before_edit')); + this.on('edit:after', this, this.proxy('_after_edit')); + this.on('save:before cancel:before', this, this.proxy('_before_unedit')); + + this.records + .bind('add', this.proxy("changed_records")) + .bind('edit', this.proxy("changed_records")) + .bind('remove', this.proxy("changed_records")); + }, + start: function () { + var ret = this._super(); + this.$element + .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; - // A list not being edited is always valid - if (!(form = this.first_edition_form())) { - return true; - } + 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.$element.is('.oe_form_dirty')) { return true; } + this.o2m._dirty_flag = true; // Otherwise validate internal form return _(form.fields).chain() @@ -3208,21 +3395,8 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ .all(_.identity) .value(); }, - first_edition_form: function () { - var get_form = function (group_or_list) { - if (group_or_list.edition) { - return group_or_list.edition_form; - } - return _(group_or_list.children).chain() - .map(get_form) - .compact() - .first() - .value(); - }; - return get_form(this.groups); - }, do_add_record: function () { - if (this.options.editable) { + if (this.editable()) { this._super.apply(this, arguments); } else { var self = this; @@ -3275,55 +3449,134 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ }); }, do_button_action: function (name, id, callback) { - var _super = _.bind(this._super, this); + if (!_.isNumber(id)) { + instance.webclient.notification.warn( + _t("Action Button"), + _t("The o2m record must be saved before an action can be used")); + return; + } + var parent_form = this.o2m.view; + var self = this; + this.ensure_saved().pipe(function () { + return parent_form.do_save(); + }).then(function () { + self.handle_button(name, id, callback); + }); + }, - this.o2m.view.do_save().then(function () { - _super(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.$element.hasClass('oe_form_dirty')) { + this.save_edition(); + return; + } + this.cancel_edition(); + }, + keyup_ENTER: function () { + // blurring caused by hitting the [Return] key, should skip the + // autosave-on-blur and let the handler for [Return] do its thing (save + // the current row *anyway*, then create a new one/edit the next one) + this.__ignore_blur = true; + this._super.apply(this, arguments); + }, + do_delete: function (ids) { + var self = this; + var next = $.when(); + var _super = this._super; + // handle deletion of an item which does not exist + // TODO: better handle that in the editable list? + var false_id_index = _(ids).indexOf(false); + if (false_id_index !== -1) { + ids.splice(false_id_index, 1); + next = this.cancel_edition(true); + } + return next.pipe(function () { + // wheeee + var confirm = window.confirm; + window.confirm = function () { return true; }; + try { + return _super.call(self, ids); + } finally { + window.confirm = confirm; + } }); } }); instance.web.form.One2ManyList = instance.web.ListView.List.extend({ - KEY_RETURN: 13, - // blurring caused by hitting the [Return] key, should skip the - // autosave-on-blur and let the handler for [Return] do its thing - __return_blur: false, - render_row_as_form: function () { - var self = this; - return this._super.apply(this, arguments).then(function () { - // Replace the "Save Row" button with "Cancel Edition" - self.edition_form.$element - .undelegate('button.oe_list_edit_row_save', 'click') - .delegate('button.oe_list_edit_row_save', 'click', function () { - self.cancel_pending_edition(); - }); + pad_table_to: function (count) { + this._super(count > 0 ? count - 1 : 0); - // Overload execute_action on the edition form to perform a simple - // reload_record after the action is done, rather than fully - // reload the parent view (or something) - var _execute_action = self.edition_form.do_execute_action; - self.edition_form.do_execute_action = function (action, dataset, record_id, _callback) { - return _execute_action.call(this, action, dataset, record_id, function () { - self.view.reload_record( - self.view.records.get(record_id)); - }); - }; + // magical invocation of wtf does that do + if (this.view.o2m.get('effective_readonly')) { + return; + } - self.edition_form.on('blurred', null, function () { - if (self.__return_blur) { - delete self.__return_blur; - return; - } - if (!self.edition_form.widget_is_stopped) { - self.view.ensure_saved(); - } - }); - }); - }, - on_row_keyup: function (e) { - if (e.which === this.KEY_RETURN) { - this.__return_blur = true; + 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++; } + var $cell = $('<td>', { + colspan: columns, + 'class': 'oe_form_field_one2many_list_row_add' + }).append( + $('<a>', {href: '#'}).text(_t("Add a row")) + .mousedown(function () { + // FIXME: needs to be an official API somehow + if (self.view.editor.is_editing()) { + self.view.__ignore_blur = true; + } + }) + .click(function (e) { + e.preventDefault(); + e.stopPropagation(); + // FIXME: there should also be an API for that one + if (self.view.editor.form.__blur_timeout) { + clearTimeout(self.view.editor.form.__blur_timeout); + self.view.editor.form.__blur_timeout = false; + } + self.view.ensure_saved().then(function () { + self.view.do_add_record(); + }); + })); + + var $padding = this.$current.find('tr:not([data-id]):first'); + var $newrow = $('<tr>').append($cell); + if ($padding.length) { + $padding.before($newrow); + } else { + this.$current.append($newrow) } - this._super(e); } }); @@ -3431,7 +3684,7 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in $("textarea", this.$element).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(); } }); @@ -3453,12 +3706,13 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in render_value: function() { var self = this; var dataset = new instance.web.DataSetStatic(this, this.field.relation, self.view.dataset.get_context()); + var values = self.get("value") var handle_names = function(data) { var indexed = {}; _.each(data, function(el) { indexed[el[0]] = el; }); - data = _.map(self.get("value"), function(el) { return indexed[el]; }); + data = _.map(values, function(el) { return indexed[el]; }); if (! self.get("effective_readonly")) { self.tags.containerElement().children().remove(); $("textarea", self.$element).css("padding-left", "3px"); @@ -3467,8 +3721,8 @@ instance.web.form.FieldMany2ManyTags = instance.web.form.AbstractField.extend(in self.$element.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([]); } @@ -3492,7 +3746,7 @@ instance.web.form.FieldMany2Many = instance.web.form.AbstractField.extend({ }, start: function() { this._super.apply(this, arguments); - this.$element.addClass('oe_form_field_many2many'); + this.$element.addClass('oe_form_field oe_form_field_many2many'); var self = this; @@ -3867,14 +4121,16 @@ instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ display_popup: function() { var self = this; this.renderElement(); - new instance.web.Dialog(this, { + var dialog = new instance.web.Dialog(this, { min_width: '800px', - dialogClass: 'oe_act_window', + dialogClass: 'oe_act_window', close: function() { self.check_exit(true); }, title: this.options.title || "", + buttons: [{text:"tmp"}], }, this.$element).open(); + this.$buttonpane = dialog.$element.dialog("widget").find(".ui-dialog-buttonpane").html(""); this.start(); }, on_write_completed: function() {}, @@ -3890,16 +4146,18 @@ 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_popup_form")); this.view_form.on_loaded.add_last(function() { - var $buttons = self.view_form.$element.find(".oe_form_buttons"); var multi_select = self.row_id === null && ! self.options.disable_multiple_selection; - $buttons.html(QWeb.render("AbstractFormPopup.buttons", {multi_select: multi_select})); - var $snbutton = $buttons.find(".oe_abstractformpopup-form-save-new"); + self.$buttonpane.html(QWeb.render("AbstractFormPopup.buttons", {multi_select: multi_select})); + var $snbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save-new"); $snbutton.click(function() { $.when(self.view_form.do_save()).then(function() { self.view_form.reload_mutex.exec(function() { @@ -3907,7 +4165,7 @@ instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ }); }); }); - var $sbutton = $buttons.find(".oe_abstractformpopup-form-save"); + var $sbutton = self.$buttonpane.find(".oe_abstractformpopup-form-save"); $sbutton.click(function() { $.when(self.view_form.do_save()).then(function() { self.view_form.reload_mutex.exec(function() { @@ -3915,7 +4173,7 @@ instance.web.form.AbstractFormPopup = instance.web.OldWidget.extend({ }); }); }); - var $cbutton = $buttons.find(".oe_abstractformpopup-form-close"); + var $cbutton = self.$buttonpane.find(".oe_abstractformpopup-form-close"); $cbutton.click(function() { self.check_exit(); }); @@ -4022,9 +4280,12 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend self.dataset, false, _.extend({'deletable': false, 'selectable': !self.options.disable_multiple_selection, - 'read_only': true, 'import_enabled': false, + '$buttons': self.$buttonpane, }, self.options.list_view_options || {})); + self.view_list.on('edit:before', self, function (e) { + e.cancel = true; + }); self.view_list.popup = self; self.view_list.appendTo($(".oe_popup_list", self.$element)).pipe(function() { self.view_list.do_show(); @@ -4032,16 +4293,12 @@ instance.web.form.SelectCreatePopup = instance.web.form.AbstractFormPopup.extend self.searchview.do_search(); }); self.view_list.on_loaded.add_last(function() { - var $buttons = self.view_list.$element.find(".oe-actions"); - $buttons.prepend(QWeb.render("SelectCreatePopup.search.buttons")); - var $cbutton = $buttons.find(".oe_selectcreatepopup-search-close"); + self.$buttonpane.html(QWeb.render("SelectCreatePopup.search.buttons", {widget:self})); + var $cbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-close"); $cbutton.click(function() { self.destroy(); }); - var $sbutton = $buttons.find(".oe_selectcreatepopup-search-select"); - if(self.options.disable_multiple_selection) { - $sbutton.hide(); - } + var $sbutton = self.$buttonpane.find(".oe_selectcreatepopup-search-select"); $sbutton.click(function() { self.on_select_elements(self.selected_ids); self.destroy(); @@ -4127,7 +4384,7 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instan this.selection.view = this.view; this.selection.set({force_readonly: this.get('effective_readonly')}); this.selection.on("change:value", this, this.on_selection_changed); - this.selection.$element = $(".oe_form_view_reference_selection", this.$element); + this.selection.setElement(this.$(".oe_form_view_reference_selection")); this.selection.renderElement(); this.selection.start(); this.selection @@ -4140,7 +4397,7 @@ instance.web.form.FieldReference = instance.web.form.AbstractField.extend(instan this.m2o.view = this.view; this.m2o.set({force_readonly: this.get("effective_readonly")}); this.m2o.on("change:value", this, this.data_changed); - this.m2o.$element = $(".oe_form_view_reference_m2o", this.$element); + this.m2o.setElement(this.$(".oe_form_view_reference_m2o")); this.m2o.renderElement(); this.m2o.start(); this.m2o @@ -4260,7 +4517,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. //link.target = '_blank'; link.href = "data:application/octet-stream;base64," + value; } else { - $.blockUI(); + instance.web.blockUI(); this.session.get_file({ url: '/web/binary/saveas_ajax', data: {data: JSON.stringify({ @@ -4270,7 +4527,7 @@ instance.web.form.FieldBinary = instance.web.form.AbstractField.extend(instance. filename_field: (this.node.attrs.filename || ''), context: this.view.dataset.get_context() })}, - complete: $.unblockUI, + complete: instance.web.unblockUI, error: instance.webclient.crashmanager.on_rpc_error }); ev.stopPropagation(); @@ -4356,7 +4613,7 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ }, render_value: function() { 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')) { url = '/web/binary/image?session_id=' + this.session.session_id + '&model=' + @@ -4385,9 +4642,11 @@ instance.web.form.FieldBinaryImage = instance.web.form.FieldBinary.extend({ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ template: "FieldStatus", + clickable: false, start: function() { this._super(); this.selected_value = null; + this.clickable = !!this.node.attrs.clickable; if (this.$element.parent().is('header')) { this.$element.after('<div class="oe_clear"/>'); } @@ -4492,19 +4751,34 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ * state (given by the key of (key, label)). */ render_elements: function () { + var self = this; var content = instance.web.qweb.render("FieldStatus.content", {widget: this, _:_}); this.$element.html(content); - + if (this.clickable) { + this.$element.addClass("oe_form_steps_clickable"); + $('.oe_form_steps_arrow').remove(); + var elemts = this.$element.find('.oe_form_steps_button'); + _.each(elemts, function(element){ + $item = $(element); + if ($item.attr("data-id") != self.selected_value) { + $item.click(function(event){ + var data_id = parseInt($(this).attr("data-id")) + self.view.dataset.call('stage_set', [[self.view.datarecord.id],data_id]).then(function() { + return self.view.reload(); + }); + }); + } + }); + } else { + this.$element.addClass("oe_form_steps"); + } var colors = JSON.parse((this.node.attrs || {}).statusbar_colors || "{}"); var color = colors[this.selected_value]; - if (color) { + if (color) { var elem = this.$element.find("li.oe_form_steps_active span"); - elem.css("color", color); + elem.css("color", color); } }, - focus: function() { - return false; - }, }); /** @@ -4519,6 +4793,7 @@ instance.web.form.widgets = new instance.web.Registry({ 'email' : 'instance.web.form.FieldEmail', 'url' : 'instance.web.form.FieldUrl', 'text' : 'instance.web.form.FieldText', + 'text_html' : 'instance.web.form.FieldTextHtml', 'date' : 'instance.web.form.FieldDate', 'datetime' : 'instance.web.form.FieldDatetime', 'selection' : 'instance.web.form.FieldSelection', diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index edc53f82042..986b0e49faf 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -21,9 +21,6 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi // whether the view rows can be reordered (via vertical drag & drop) 'reorderable': true, 'action_buttons': true, - // if true, the view can't be editable, ignoring the view's and the context's - // instructions - 'read_only': false, // if true, the 'Import', 'Export', etc... buttons will be shown 'import_enabled': true, }, @@ -142,7 +139,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }); }, /** - * View startup method, the default behavior is to set the ``oe_listw`` + * 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 @@ -288,7 +285,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi } this.$buttons.find('.oe_list_add') .click(this.proxy('do_add_record')) - .prop('disabled', grouped && this.options.editable); + .prop('disabled', grouped); this.$buttons.on('click', '.oe_list_button_import', function() { self.on_sidebar_import(); return false; @@ -357,9 +354,10 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi this.sidebar.add_items('other', [ { label: _t("Import"), callback: this.on_sidebar_import }, { label: _t("Export"), callback: this.on_sidebar_export }, - { label: _t('Delete'), callback: this.do_delete_selected }, + { label: _t('Delete'), callback: this.do_delete_selected } ]); this.sidebar.add_toolbar(this.fields_view.toolbar); + this.sidebar.$element.hide(); } }, /** @@ -378,6 +376,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi var total = dataset.size(); var limit = this.limit() || total; + this.$pager.toggle(total !== 0); this.$pager.toggleClass('oe_list_pager_single_page', (total <= limit)); var spager = '-'; if (total) { @@ -400,68 +399,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 @@ -487,9 +441,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(); } @@ -667,6 +618,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; }); @@ -685,7 +653,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) @@ -801,9 +770,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi } $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() { @@ -862,9 +829,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi } 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)) + $('<div class="oe_view_nocontent">').html(this.options.action.help) ); } }); @@ -911,32 +876,44 @@ 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) { @@ -967,19 +944,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); } @@ -1029,7 +1006,7 @@ instance.web.ListView.List = instance.web.Class.extend( /** @lends instance.web. }); } } - return instance.web.format_cell(record.toForm().data, column, { + return column.format(record.toForm().data, { model: this.dataset.model, id: record.get('id') }); @@ -1076,7 +1053,6 @@ 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 @@ -1139,6 +1115,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, @@ -1147,26 +1124,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); } }); } }); @@ -1333,15 +1291,18 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we _t("Grouping on field '%s' is not possible because that field does not appear in the list view."), group.grouped_on)); } + var group_label; try { - $group_column.html(instance.web.format_cell( - row_data, group_column, { - value_if_empty: _t("Undefined"), - process_modifiers: false - })); + group_label = group_column.format(row_data, { + value_if_empty: _t("Undefined"), + process_modifiers: false + }); } catch (e) { - $group_column.html(row_data[group_column.id].value); + group_label = row_data[group_column.id].value; } + $group_column.text(_.str.sprintf("%s (%d)", + group_label, group.length)); + if (group.length && group.openable) { // Make openable if not terminal group & group_by_no_leaf $group_column.prepend('<span class="ui-icon ui-icon-triangle-1-e" style="float: left;">'); @@ -1353,14 +1314,12 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we } } self.indent($group_column, group.level); - // count column - $('<td>').text(group.length).appendTo($row); if (self.options.selectable) { $row.append('<td>'); } _(self.columns).chain() - .filter(function (column) {return !column.invisible;}) + .filter(function (column) { return column.invisible !== '1'; }) .each(function (column) { if (column.meta) { // do not do anything @@ -1368,8 +1327,7 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we var r = {}; r[column.id] = {value: group.aggregates[column.id]}; $('<td class="oe_number">') - .html(instance.web.format_cell( - r, column, {process_modifiers: false})) + .html(column.format(r, {process_modifiers: false})) .appendTo($row); } else { $row.append('<td>'); @@ -1456,18 +1414,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'), @@ -1485,7 +1456,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 @@ -1495,13 +1466,13 @@ 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(); } }); }, @@ -1831,7 +1802,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); @@ -1847,13 +1818,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)) { @@ -1878,6 +1878,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); @@ -1885,6 +1925,204 @@ 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 (label, fn) { + 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) { + return _.template('<button type="button" title="<%-title%>" <%=additional_attributes%> >' + + '<img src="<%-prefix%>/web/static/src/img/icons/<%-icon%>.png" alt="<%-alt%>"/>' + + '</button>', { + title: this.string || '', + additional_attributes: isNaN(row_data["id"].value) && instance.web.BufferedDataSet.virtual_id_regex.test(row_data["id"].value) ? + 'disabled="disabled" class="oe_list_button_disabled"' : '', + prefix: instance.connection.prefix, + icon: this.icon, + alt: this.string || '' + }); + } +}); +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.connection.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({ + /** + * 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 c370aeaf7fd..9a101a5ac1a 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,29 @@ 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.$element.add(self.$buttons).addClass('oe_editing'); + }); + this.on('save:after cancel:after', this, function () { + self.$element.add(self.$buttons).removeClass('oe_editing'); + }); + }, + destroy: function () { + instance.web.bus.off('resize', this, this.resize_fields); + this._super(); }, /** * Handles the activation of a record in editable mode (making a record @@ -42,25 +69,16 @@ openerp.web.list_editable = function (instance) { do_edit: function (index, id, dataset) { _.extend(this.dataset, dataset); }, - /** - * Sets editability status for the list, based on defaults, view - * architecture and the provided flag, if any. - * - * @param {Boolean} [force] forces the list to editability. Sets new row edition status to "bottom". - */ - set_editable: function (force) { - // If ``force``, set editability to bottom - // otherwise rely on view default - // view' @editable is handled separately as we have not yet - // fetched and processed the view at this point. - this.options.editable = ( - ! this.options.read_only && ((force && "bottom") || this.defaults.editable)); + editable: function () { + return this.fields_view.arch.attrs.editable + || this._context_editable + || this.options.editable; }, /** * Replace do_search to handle editability process */ do_search: function(domain, context, group_by) { - this.set_editable(context['set_editable']); + this._context_editable = !!context.set_editable; this._super.apply(this, arguments); }, /** @@ -68,18 +86,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) { + if (this.editable()) { this.$element.find('table:first').show(); this.$element.find('.oe_view_nocontent').remove(); - this.groups.new_record(); + this.start_edition(); } else { this._super(); } }, on_loaded: function (data, grouped) { + var self = this; // tree/@editable takes priority on everything else if present. - this.options.editable = ! this.options.read_only && (data.arch.attrs.editable || this.options.editable); - return this._super(data, grouped); + var result = this._super(data, grouped); + if (this.editable()) { + this.$element.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.$element + .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.$element) + .then(this.proxy('setup_events')); + + return $.when(result, editor_ready); + } else { + this.$element.removeClass('oe_list_editable'); + } + + return result; + }, + /** + * Builds a new editor object + * + * @return {instance.web.list.Editor} + */ + make_editor: function () { + return new instance.web.list.Editor(this); + }, + do_button_action: function (name, id, callback) { + var self = this, args = arguments; + this.ensure_saved().then(function (done) { + if (!id && done.created) { + id = done.record.get('id'); + } + self.handle_button.call(self, name, id, callback); + }); }, /** * Ensures the editable list is saved (saves any pending edition if @@ -90,354 +156,624 @@ openerp.web.list_editable = function (instance) { * @returns {$.Deferred} */ ensure_saved: function () { - return this.groups.ensure_saved(); - } - }); - - instance.web.ListView.Groups.include(/** @lends instance.web.ListView.Groups# */{ - passtrough_events: instance.web.ListView.Groups.prototype.passtrough_events + " edit saved", - new_record: function () { - // TODO: handle multiple children - this.children[null].new_record(); + if (!this.editor.is_editing()) { + return $.when(); + } + return this.save_edition(); }, /** - * Ensures descendant editable List instances are all saved if they have - * pending editions. + * Set up the edition of a record of the list view "inline" * - * @returns {$.Deferred} + * @param {instance.web.list.Record} [record] record to edit, leave empty to create a new record + * @param {Object} [options] + * @param {String} [options.focus_field] field to focus at start of edition + * @return {jQuery.Deferred} */ - ensure_saved: function () { - return $.when.apply(null, - _.invoke( - _.values(this.children), - 'ensure_saved')); - } - }); - - instance.web.ListView.List.include(/** @lends instance.web.ListView.List# */{ - row_clicked: function (event) { - if (!this.options.editable) { - return this._super.apply(this, arguments); + start_edition: function (record, options) { + var self = this; + var item = false; + if (record) { + item = record.attributes; + } else { + var attrs = {id: false}; + _(this.columns).chain() + .filter(function (x) { return x.tag === 'field'}) + .pluck('name') + .each(function (field) { attrs[field] = false; }); + record = new instance.web.list.Record(attrs); + this.records.add(record, { + at: this.prepends_on_create() ? 0 : null}); } - this.edit_record($(event.currentTarget).data('id')); + var $recordRow = this.groups.get_row_for(record); + var cells = this.get_cells_for($recordRow); + + return this.ensure_saved().pipe(function () { + self.fields_for_resize.splice(0, self.fields_for_resize.length); + return self.with_event('edit', { + record: record.attributes, + cancel: false + }, function () { + return self.editor.edit(item, function (field_name, field) { + var cell = cells[field_name]; + if (!cell || field.get('effective_readonly')) { + // Readonly fields can just remain the list's, + // form's usually don't have backgrounds &al + field.set({invisible: true}); + return; + } + + // FIXME: need better way to get the field back from bubbling (delegated) DOM events somehow + field.$element.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; - if (!this.edition) { - return $.when(); - } - - if (this.edition_id) { - cancelled = this.reload_record(this.records.get(this.edition_id)); - } else { - cancelled = $.when(); + 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; - self.dataset.index = null; - delete self.edition_id; - delete self.edition; - }); - this.pad_table_to(5); - return cancelled; }, /** - * Adapts this list's view description to be suitable to the inner form - * view of a row being edited. + * Resizes a field's root element based on the corresponding cell of + * a listview row * - * @returns {Object} fields_view_get's view section suitable for putting into form view of editable rows. + * @param {instance.web.form.AbstractField} field + * @param {jQuery} cell */ - get_form_fields_view: function () { - // deep copy of view - var view = $.extend(true, {}, this.group.view.fields_view); - _(view.arch.children).each(function (widget) { - widget.attrs.nolabel = true; - if (widget.tag === 'button') { - delete widget.attrs.string; - } + resize_field: function (field, cell) { + var $cell = $(cell); + var position = $cell.position(); + + // jquery does not understand !important + field.$element.attr('style', 'width: '+$cell.outerWidth()+'px !important') + field.$element.css({ + top: position.top, + left: position.left, + minHeight: $cell.outerHeight() }); - view.arch.attrs.col = 2 * view.arch.children.length; - return view; }, - on_row_keyup: function (e) { + /** + * @return {jQuery.Deferred} + */ + save_edition: function () { var self = this; - switch (e.which) { - case KEY_RETURN: - $(e.target).blur(); - e.preventDefault(); - //e.stopImmediatePropagation(); - setTimeout(function () { - self.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'); - } - self.edit_record(next_record_id); - }, 0); + 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); + } + // 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; - return this.ensure_saved().pipe(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') : ''), - click: function (e) {e.stopPropagation();} - }) - .addClass('oe_form oe_form_container') - .delegate('button.oe_list_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(); }) - .keypress(function (e) { - if (e.which === KEY_RETURN) { - return false; + 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 } - }); - - 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')); - } - } 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; - self.dataset.index = _(self.dataset.ids).indexOf(record_id); - if (self.dataset.index === -1) { - self.dataset.index = null; - } - self.edition_form = _.extend(new instance.web.ListEditableFormView(self.view, self.dataset, false), { - $element: $new_row, - editable_list: self - }); - // put in $.when just in case FormView.on_loaded becomes asynchronous - return $.when(self.edition_form.on_loaded(self.get_form_fields_view())).then(function () { - $new_row.find('> td') - .end() - .find('td:last').removeClass('oe_list_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); - } - self.reload_record(record); + 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).each(function (widget) { + var modifiers = JSON.parse(widget.attrs.modifiers || '{}'); + widget.attrs.nolabel = true; + if (modifiers['tree_invisible'] || widget.tag === 'button') { + modifiers.invisible = true; + } + widget.attrs.modifiers = JSON.stringify(modifiers); + }); + return view; + }, + handle_onwrite: function (source_record) { + var self = this; + var on_write_callback = self.fields_view.arch.attrs.on_write; + if (!on_write_callback) { return $.when(); } + return this.dataset.call(on_write_callback, [source_record.get('id')]) + .pipe(function (ids) { + return $.when.apply( + null, _(ids).map( + _.bind(self.handle_onwrite_record, self, source_record))); }); + }, + handle_onwrite_record: function (source_record, id) { + var record = this.records.get(id); + if (!record) { + // insert after the source record + var index = this.records.indexOf(source_record) + 1; + record = new instance.web.list.Record({id: id}); + this.records.add(record, {at: index}); + this.dataset.ids.splice(index, 0, id); + } + return this.reload_record(record); + }, + prepends_on_create: function () { + return this.editable() === 'top'; + }, + setup_events: function () { + var self = this; + this.editor.$element.on('keyup keydown', function (e) { + if (!self.editor.is_editing()) { return; } + var key = _($.ui.keyCode).chain() + .map(function (v, k) { return {name: k, code: v}; }) + .find(function (o) { return o.code === e.which; }) + .value(); + if (!key) { return; } + var method = e.type + '_' + key.name; + if (!(method in self)) { return; } + self[method](e); }); }, /** - * Saves the current row, and returns a Deferred resolving to an object - * with the following properties: - * - * ``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 + _next: function (next_record, options) { + next_record = next_record || 'succ'; var self = this; - 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 - }; - }); - }); + return this.save_edition().pipe(function (saveInfo) { + if (saveInfo.created) { + return self.start_edition(); + } + var record = self.records[next_record]( + saveInfo.record, {wraparound: true}); + return self.start_edition(record, options); + }); + }, + keyup_ENTER: function () { + return this._next(); + }, + keyup_ESCAPE: function () { + return this.cancel_edition(); }, /** - * If the current list is being edited, ensures it's saved + * Gets the selection range (start, end) for the provided element, + * returns ``null`` if it can't get a range. + * + * @private */ - ensure_saved: function () { - if (this.edition) { - // kinda-hack-ish: if the user has entered data in a field, - // oe_form_dirty will be set on the form so save, otherwise - // discard the current (entirely empty) line - if (this.edition_form.$element.is('.oe_form_dirty')) { - return this.save_row(); + _text_selection_range: function (el) { + var selectionStart; + try { + selectionStart = el.selectionStart; + } catch (e) { + // radio or checkbox throw on selectionStart access + return null; + } + if (selectionStart !== undefined) { + return { + start: selectionStart, + end: el.selectionEnd + }; + } else if (document.body.createTextRange) { + throw new Error("Implement text range handling for MSIE"); + var sel = document.body.createTextRange(); + if (sel.parentElement() === el) { + } - return this.cancel_pending_edition(); } - //noinspection JSPotentiallyInvalidConstructorUsage - return $.when(); + // 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.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.$element.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.$element.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.$element.is(':visible'); }) + .last() + .value(); + // tabbed from last field in form + if (last_field && last_field.$element.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.$element).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.$element.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.$element.is(':visible')) { + return false; + } + // Stop as soon as a field got focused + return field.focus() !== false; + }); }, - render_to: function($element) { + edit: function (record, configureField, options) { + // TODO: specify sequence of edit calls var self = this; - - var xml = instance.web.json_node_to_xml(this.fvg.arch); - var $xml = $(xml); - - if (this.view.editable_list.options.selectable) - $("<td>").appendTo($element); - - $xml.children().each(function(i, el) { - var modifiers = JSON.parse($(el).attr("modifiers") || "{}"); - var $td = $("<td>"); - if (modifiers.tree_invisible === true) - $td.hide(); - var tag_name = el.tagName.toLowerCase(); - var w; - if (tag_name === "field") { - var name = $(el).attr("name"); - var key = $(el).attr('widget') || self.fvg.fields[name].type; - var obj = self.view.fields_registry.get_object(key); - w = new (obj)(self.view, instance.web.xml_to_json(el)); - self.view.register_field(w, $(el).attr("name")); - } else { - var obj = self.tags_registry.get_object(tag_name); - w = new (obj)(self.view, instance.web.xml_to_json(el)); - } - w.appendTo($td); - $td.appendTo($element); + var form = self.form; + var loaded = record + ? form.on_record_loaded(_.extend({}, record)) + : form.load_defaults(); + + return loaded.pipe(function () { + return form.do_show({reload: false}); + }).pipe(function () { + self.record = form.datarecord; + _(form.fields).each(function (field, name) { + configureField(name, field); + }); + self._focus_setup(options && options.focus_field); + return form; }); - $(QWeb.render('ListView.row.save')).appendTo($element); }, + save: function () { + var self = this; + return this.form + .do_save(null, 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()) { + return this._super.apply(this, arguments); + } + var record_id = $(event.currentTarget).data('id'); + this.view.start_edition( + record_id ? this.records.get(record_id) : null, { + focus_field: $(event.target).data('field') + }); + }, + /** + * If a row mapping to the record (@data-id matching the record's id or + * no @data-id if the record has no id), returns it. Otherwise returns + * ``null``. + * + * @param {Record} record the record to get a row for + * @return {jQuery|null} + */ + get_row_for: function (record) { + var id; + var $row = this.$current.children('[data-id=' + record.get('id') + ']'); + if ($row.length) { + return $row; + } + return null; + } }); }; diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js index e11adb9d595..2845d9afb73 100644 --- a/addons/web/static/src/js/views.js +++ b/addons/web/static/src/js/views.js @@ -14,6 +14,9 @@ instance.web.ActionManager = instance.web.Widget.extend({ this.dialog = null; this.dialog_widget = null; this.breadcrumbs = []; + this.on('history_back', this, function() { + return this.history_back(); + }); }, start: function() { this._super.apply(this, arguments); @@ -65,21 +68,46 @@ instance.web.ActionManager = instance.web.Widget.extend({ 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 item; + var index; for (var i = this.breadcrumbs.length - 1; i >= 0; i--) { - var it = this.breadcrumbs[i]; - if (it.id == id) { - item = it; + if (this.breadcrumbs[i].id == id) { + index = i; break; } - this.remove_breadcrumb(i); } - var index = $e.parent().find('.oe_breadcrumb_item[data-id=' + $e.data('id') + ']').index($e); - item.show(index, $e); + var subindex = $e.parent().find('.oe_breadcrumb_item[data-id=' + $e.data('id') + ']').index($e); + this.select_breadcrumb(index, subindex); + }, + select_breadcrumb: function(index, subindex) { + for (var i = this.breadcrumbs.length - 1; i >= 0; i--) { + if (i > index) { + this.remove_breadcrumb(i); + } + } + var item = this.breadcrumbs[index]; + item.show(subindex); this.inner_widget = item.widget; + return true; }, clear_breadcrumbs: function() { while (this.breadcrumbs.length) { @@ -114,15 +142,21 @@ instance.web.ActionManager = instance.web.Widget.extend({ } } } - return titles.join(' / '); + 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['title'] = this.inner_action.name; - state['model'] = this.inner_action.res_model; + if(this.inner_action.type == 'ir.actions.act_window') { + state['model'] = this.inner_action.res_model; + } if (this.inner_action.id) { - state['action_id'] = this.inner_action.id; + state['action'] = this.inner_action.id; + } else if (this.inner_action.type == 'ir.actions.client') { + state['action'] = this.inner_action.tag; + //state = _.extend(this.inner_action.params || {}, state); } } this.getParent().do_push_state(state); @@ -131,14 +165,20 @@ instance.web.ActionManager = instance.web.Widget.extend({ do_load_state: function(state, warm) { var self = this, action_loaded; - if (state.action_id) { - var run_action = (!this.inner_widget || !this.inner_widget.action) || this.inner_widget.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 ? @@ -152,23 +192,12 @@ 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(); - var action = state.client_action; - if(_.isString(action)) { - action = { - tag: action, - params: state, - }; - } - this.ir_actions_client(action); } $.when(action_loaded || null).then(function() { @@ -178,7 +207,10 @@ instance.web.ActionManager = instance.web.Widget.extend({ }); }, do_action: function(action, on_close) { - if (_.isNumber(action) || _.isString(action)) { + if (_.isString(action) && instance.web.client_actions.contains(action)) { + var action_client = { type: "ir.actions.client", tag: action }; + return this.do_action(action_client); + } 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); @@ -186,7 +218,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ } if (!action.type) { console.error("No type for action", action); - return; + return null; } var type = action.type.replace(/\./g,'_'); var popup = action.target === 'new'; @@ -201,7 +233,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ }, action.flags || {}); if (!(type in this)) { console.error("Action manager can't handle action of type " + action.type, action); - return; + return null; } return this[type](action, on_close); }, @@ -209,21 +241,33 @@ instance.web.ActionManager = instance.web.Widget.extend({ this.dialog_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); + + do_ir_actions_common: function(action, on_close) { + var self = this, klass, widget, post_process; + if (action.type === 'ir.actions.client') { + var ClientWidget = instance.web.client_actions.get_object(action.tag); + widget = new ClientWidget(this, action.params); + klass = 'oe_act_client'; + post_process = function() { + self.push_breadcrumb({ + widget: widget, + title: action.name + }); + if (action.tag !== 'reload') { + self.do_push_state({}); + } }; + } else { + widget = new instance.web.ViewManagerAction(this, action); + klass = 'oe_act_window'; + post_process = widget.proxy('add_breadcrumb'); } if (action.target === 'new') { if (this.dialog === null) { // These buttons will be overwrited by <footer> if any this.dialog = new instance.web.Dialog(this, { buttons: { "Close": function() { $(this).dialog("close"); }}, - dialogClass: 'oe_act_window' + dialogClass: klass }); if(on_close) this.dialog.on_close.add(on_close); @@ -231,21 +275,39 @@ instance.web.ActionManager = instance.web.Widget.extend({ this.dialog_widget.destroy(); } this.dialog.dialog_title = action.name; - this.dialog_widget = new instance.web.ViewManagerAction(this, action); + this.dialog_widget = widget; this.dialog_widget.appendTo(this.dialog.$element); this.dialog.open(); } else { this.dialog_stop(); + this.inner_action = action; + this.inner_widget = widget; + post_process(); + this.inner_widget.appendTo(this.$element); + } + }, + + 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); + }; + } + if (action.target !== 'new') { if(action.menu_id) { + this.dialog_stop(); return this.getParent().do_action(action, function () { instance.webclient.menu.open_menu(action.menu_id); }); } - this.inner_action = action; - var inner_widget = this.inner_widget = new instance.web.ViewManagerAction(this, action); - inner_widget.add_breadcrumb(); - this.inner_widget.appendTo(this.$element); } + return this.do_ir_actions_common(action, on_close); + }, + ir_actions_client: function (action, on_close) { + return this.do_ir_actions_common(action, on_close); }, ir_actions_act_window_close: function (action, on_closed) { if (!this.dialog && on_closed) { @@ -262,19 +324,9 @@ instance.web.ActionManager = instance.web.Widget.extend({ self.do_action(action, on_closed) }); }, - ir_actions_client: function (action) { - this.dialog_stop(); - var ClientWidget = instance.web.client_actions.get_object(action.tag); - this.inner_widget = new ClientWidget(this, action.params); - this.push_breadcrumb({ - widget: this.inner_widget, - title: action.name - }); - this.inner_widget.appendTo(this.$element); - }, ir_actions_report_xml: function(action, on_closed) { var self = this; - $.blockUI(); + instance.web.blockUI(); self.rpc("/web/session/eval_domain_and_context", { contexts: [action.context], domains: [] @@ -284,7 +336,7 @@ instance.web.ActionManager = instance.web.Widget.extend({ self.session.get_file({ url: '/web/report', data: {action: JSON.stringify(action)}, - complete: $.unblockUI, + complete: instance.web.unblockUI, success: function(){ if (!self.dialog && on_closed) { on_closed(); @@ -396,7 +448,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ .find('.oe_view_manager_switch a').filter('[data-view-type="' + view_type + '"]') .parent().addClass('active'); - $.when(view_promise).then(function () { + return $.when(view_promise).then(function () { _.each(_.keys(self.views), function(view_name) { var controller = self.views[view_name].controller; if (controller) { @@ -408,7 +460,7 @@ instance.web.ViewManager = instance.web.Widget.extend({ container.hide(); controller.do_hide(); } - // put the <footer> in the dialog's buttonpane + // put the <footer> in the dialog's buttonpane if (self.$element.parent('.ui-dialog-content') && self.$element.find('footer')) { self.$element.parent('.ui-dialog-content').parent().find('div.ui-dialog-buttonset').hide() self.$element.find('footer').appendTo( @@ -418,7 +470,6 @@ instance.web.ViewManager = instance.web.Widget.extend({ } }); }); - return view_promise; }, do_create_view: function(view_type) { // Lazy loading of views @@ -436,6 +487,13 @@ instance.web.ViewManager = instance.web.Widget.extend({ } var controller = new controllerclass(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'); + } + }); + controller.on("change:title", this, function() { if (self.active_view === view_type) { self.set_title(controller.get('title')); @@ -478,7 +536,8 @@ instance.web.ViewManager = instance.web.Widget.extend({ }); this.getParent().push_breadcrumb({ widget: this, - show: function(index, $e) { + action: this.action, + show: function(index) { var view_to_select = views[index]; self.$element.show(); if (self.active_view !== view_to_select) { @@ -486,9 +545,27 @@ instance.web.ViewManager = instance.web.Widget.extend({ } }, get_title: function() { - return _.map(views, function(v) { - return self.views[v].controller.get('title'); + 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.active_view === 'form' && self.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; } }); }, @@ -657,32 +734,6 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({ this.$element.find('.oe_debug_view').change(this.on_debug_changed); this.$element.addClass("oe_view_manager_" + (this.action.target || 'current')); - - 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); - }); - } - } - return manager_ready; }, on_debug_changed: function (evt) { @@ -736,15 +787,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', @@ -867,7 +909,7 @@ instance.web.Sidebar = instance.web.Widget.extend({ } else { self.do_attachement_update(self.dataset, self.model_id); } - $.unblockUI(); + instance.web.unblockUI(); }); }, start: function() { @@ -1006,7 +1048,7 @@ instance.web.Sidebar = instance.web.Widget.extend({ $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...')); - $.blockUI(); + instance.web.blockUI(); } }, on_attachment_delete: function(e) { @@ -1033,14 +1075,15 @@ instance.web.TranslateDialog = instance.web.Dialog.extend({ this['on_button_' + _t("Close")] = this.on_btn_close; this._super(view, { width: '80%', - height: '80%' + height: '80%', + destroy_on_close: false, }); 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.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(), @@ -1071,7 +1114,8 @@ instance.web.TranslateDialog = instance.web.Dialog.extend({ 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] || ''); + var value = values[0][f] || ''; + self.$fields_form.find('.oe_trad_field[name="' + lg.code + '-' + f + '"]').val(value).attr('data-value', value); }); deff.resolve(); }; @@ -1082,24 +1126,19 @@ instance.web.TranslateDialog = instance.web.Dialog.extend({ }); 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); + self.view.dataset.read_ids([self.view.datarecord.id], self.translatable_fields_keys, { + context: { 'lang': lg.code } + }).then(callback); } }); $.when.apply(null, deffered).then(callback); }, open: function(field) { - var self = this, - sup = this._super; + var self = this; + 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); @@ -1153,7 +1192,7 @@ instance.web.View = instance.web.Widget.extend({ this.view_id = view_id; this.set_default_options(options); }, - start: function() { + start: function () { return this.load_view(); }, load_view: function() { @@ -1193,7 +1232,7 @@ instance.web.View = instance.web.Widget.extend({ }, open_translate_dialog: function(field) { if (!this.translate_dialog) { - this.translate_dialog = new instance.web.TranslateDialog(this).start(); + this.translate_dialog = new instance.web.TranslateDialog(this); } this.translate_dialog.open(field); }, @@ -1363,7 +1402,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, @@ -1372,7 +1410,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]; |
