diff options
Diffstat (limited to 'addons/web/static/src')
| -rw-r--r-- | addons/web/static/src/css/base.css | 38 | ||||
| -rw-r--r-- | addons/web/static/src/css/base.sass | 39 | ||||
| -rw-r--r-- | addons/web/static/src/js/chrome.js | 13 | ||||
| -rw-r--r-- | addons/web/static/src/js/core.js | 25 | ||||
| -rw-r--r-- | addons/web/static/src/js/data_export.js | 48 | ||||
| -rw-r--r-- | addons/web/static/src/js/openerpframework.js | 29 | ||||
| -rw-r--r-- | addons/web/static/src/js/search.js | 2 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_form.js | 131 | ||||
| -rw-r--r-- | addons/web/static/src/js/view_list.js | 16 | ||||
| -rw-r--r-- | addons/web/static/src/xml/base.xml | 30 |
10 files changed, 255 insertions, 116 deletions
diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 97bbb2b207c..87e129bb211 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -700,10 +700,8 @@ .openerp .oe_dropdown_menu { display: none; position: absolute; - top: 26px; z-index: 3; margin: 0; - padding: 0; border: 1px solid #afafb6; background: white; padding: 4px 0; @@ -1116,7 +1114,7 @@ border-color: #999999; border-color: rgba(0, 0, 0, 0.2); border-style: solid; - border-width: 0 1px 1px; + border-width: 0 2px 1px; -moz-border-radius: 0 0 6px 6px; -webkit-border-radius: 0 0 6px 6px; border-radius: 0 0 6px 6px; @@ -1455,7 +1453,7 @@ display: table-row; height: inherit; } -.openerp .oe_view_manager .oe_view_manager_view_kanban { +.openerp .oe_view_manager .oe_view_manager_view_kanban:not(:empty) { height: inherit; } .openerp .oe_view_manager table.oe_view_manager_header { @@ -2736,31 +2734,20 @@ -o-transform: rotate(45deg); transform: rotate(45deg); } -.openerp ul.oe_form_status .oe_folded .label, .openerp ul.oe_form_status_clickable .oe_folded .label { - padding-bottom: 8px; -} -.openerp ul.oe_form_status .oe_folded ul, .openerp ul.oe_form_status_clickable .oe_folded ul { - position: absolute; - border-top: 1px solid #cacaca; - float: none; - top: 30px; - right: 0; +.openerp ul.oe_form_status ul.oe_dropdown_menu, .openerp ul.oe_form_status_clickable ul.oe_dropdown_menu { display: none; + padding: 0; + min-width: 0; } -.openerp ul.oe_form_status .oe_folded ul li, .openerp ul.oe_form_status_clickable .oe_folded ul li { - margin: 0; - border-left: 1px solid #cacaca; +.openerp ul.oe_form_status ul.oe_dropdown_menu.oe_opened, .openerp ul.oe_form_status_clickable ul.oe_dropdown_menu.oe_opened { display: block; } -.openerp ul.oe_form_status .oe_folded ul li.label, .openerp ul.oe_form_status_clickable .oe_folded ul li.label { - border: 0; - padding-left: 24px; -} -.openerp ul.oe_form_status .oe_folded ul li:last-child, .openerp ul.oe_form_status_clickable .oe_folded ul li:last-child { - border-bottom: 1px solid #cacaca; +.openerp ul.oe_form_status ul.oe_dropdown_menu li, .openerp ul.oe_form_status_clickable ul.oe_dropdown_menu li { + margin: 0; + width: 100%; } -.openerp ul.oe_form_status .oe_folded:hover ul, .openerp ul.oe_form_status_clickable .oe_folded:hover ul { - display: block; +.openerp ul.oe_form_status ul.oe_dropdown_menu li span.label, .openerp ul.oe_form_status_clickable ul.oe_dropdown_menu li span.label { + padding-left: 14px; } .openerp ul.oe_form_status li.oe_active, .openerp ul.oe_form_status_clickable li.oe_active { background-color: #5382b9; @@ -3211,9 +3198,6 @@ } @-moz-document url-prefix() { - .openerp .oe_view_manager .oe_view_manager_switch li { - line-height: 21px; - } .openerp .oe_searchview .oe_searchview_search { top: -1px; } diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index e41a182407d..e8eab4544a6 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -606,10 +606,8 @@ $sheet-padding: 16px .oe_dropdown_menu display: none position: absolute - top: 26px z-index: 3 margin: 0 - padding: 0 border: 1px solid $tag-border background: white padding: 4px 0 @@ -914,7 +912,7 @@ $sheet-padding: 16px border-color: #999 border-color: rgba(0, 0, 0, 0.2) border-style: solid - border-width: 0 1px 1px + border-width: 0 2px 1px @include radius(0 0 6px 6px) @include background-clip() li @@ -1171,7 +1169,7 @@ $sheet-padding: 16px .oe_view_manager_body display: table-row height: inherit - .oe_view_manager_view_kanban + .oe_view_manager_view_kanban:not(:empty) height: inherit table.oe_view_manager_header @@ -2174,27 +2172,18 @@ $sheet-padding: 16px @include skew-gradient(#fcfcfc, #dedede) @include radius(3px) @include transform(rotate(45deg)) - .oe_folded - .label - padding-bottom: 8px - ul - position: absolute - border-top: 1px solid #cacaca - float: none - top: 30px - right: 0 - display: none - li - margin: 0 - border-left: 1px solid #cacaca - display: block - li.label - border: 0 - padding-left: 24px - li:last-child - border-bottom: 1px solid #cacaca - &:hover ul + ul.oe_dropdown_menu + display: none + padding: 0 + min-width: 0 + &.oe_opened display: block + li + margin: 0 + width: 100% + span.label + padding-left: 14px + li.oe_active @include vertical-gradient(#729fcf, #3465a4) > .arrow span @@ -2532,8 +2521,6 @@ $sheet-padding: 16px // }}} @-moz-document url-prefix() .openerp - .oe_view_manager .oe_view_manager_switch li - line-height: 21px .oe_searchview .oe_searchview_search top: -1px .oe_form_field_many2one .oe_m2o_cm_button diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 55ab5ec073d..0cdffb0d6b0 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -534,7 +534,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ 'login': 'admin', 'password': form_obj['create_admin_pwd'], 'login_successful': function() { - var url = '/?db=' + form_obj['db_name']; + var url = '/web?db=' + form_obj['db_name']; if (self.session.debug) { url += '&debug'; } @@ -805,10 +805,11 @@ instance.web.redirect = function(url, wait) { var load = function() { var old = "" + window.location; - if (old === url) { - window.location.reload(); - } else { - window.location = url; + var old_no_hash = old.split("#")[0]; + var url_no_hash = url.split("#")[0]; + location.assign(url); + if (old_no_hash === url_no_hash) { + location.reload(true); } }; @@ -1246,6 +1247,7 @@ instance.web.Client = instance.web.Widget.extend({ this.$el.on('click', '.oe_dropdown_toggle', function(ev) { ev.preventDefault(); var $toggle = $(this); + var doc_width = $(document).width(); var $menu = $toggle.siblings('.oe_dropdown_menu'); $menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu'); var state = $menu.is('.oe_opened'); @@ -1254,7 +1256,6 @@ instance.web.Client = instance.web.Widget.extend({ $toggle.add($menu).toggleClass('oe_opened', !state); if (!state) { // Move $menu if outside window's edge - var doc_width = $(document).width(); var offset = $menu.offset(); var menu_width = $menu.width(); var x = doc_width - offset.left - menu_width - 2; diff --git a/addons/web/static/src/js/core.js b/addons/web/static/src/js/core.js index 8c69f86c47e..37a217a8664 100644 --- a/addons/web/static/src/js/core.js +++ b/addons/web/static/src/js/core.js @@ -11,31 +11,6 @@ if (typeof(console) === "undefined") { }); } -// shim provided by mozilla for function.bind -if (!Function.prototype.bind) { - Function.prototype.bind = function (oThis) { - if (typeof this !== "function") { - // closest thing possible to the ECMAScript 5 internal IsCallable function - throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); - } - - var aArgs = Array.prototype.slice.call(arguments, 1), - fToBind = this, - fNOP = function () {}, - fBound = function () { - return fToBind.apply(this instanceof fNOP && oThis - ? this - : oThis, - aArgs.concat(Array.prototype.slice.call(arguments))); - }; - - fNOP.prototype = this.prototype; - fBound.prototype = new fNOP(); - - return fBound; - }; -} - var instance = openerp; openerp.web.core = {}; diff --git a/addons/web/static/src/js/data_export.js b/addons/web/static/src/js/data_export.js index f642f0a334d..2725e18ea6e 100644 --- a/addons/web/static/src/js/data_export.js +++ b/addons/web/static/src/js/data_export.js @@ -62,8 +62,21 @@ instance.web.DataExport = instance.web.Dialog.extend({ }); }).change(); + var got_domain = this.getParent().get_active_domain().then(function (domain) { + if (domain === undefined) { + self.ids_to_export = self.getParent().get_selected_ids(); + self.domain = self.dataset.domain; + } + else { + self.ids_to_export = false; + self.domain = domain; + } + self.on_show_domain(); + }); + return $.when( got_fields, + got_domain, this.rpc('/web/export/formats', {}).done(this.do_setup_export_formats), this.show_exports_list()); }, @@ -196,6 +209,9 @@ instance.web.DataExport = instance.web.Dialog.extend({ self.showcontent(record.id); } }, + on_show_domain: function() { + this.$el.find('tr').first().find('td').append(QWeb.render('ExportTreeView-Domain', {'record': this})); + }, on_show_data: function(result, after) { var self = this; @@ -384,27 +400,17 @@ instance.web.DataExport = instance.web.Dialog.extend({ var export_format = this.$el.find("#export_format").val(); - this.getParent().get_active_domain().then(function (domain) { - if (domain === undefined) { - var ids_to_export = self.getParent().get_selected_ids(); - var domain = self.dataset.domain; - } - else { - var ids_to_export = false; - var domain = domain; - } - instance.web.blockUI(); - self.session.get_file({ - url: '/web/export/' + export_format, - data: {data: JSON.stringify({ - model: self.dataset.model, - fields: exported_fields, - ids: ids_to_export, - domain: domain, - import_compat: !!self.$el.find("#import_compat").val(), - })}, - complete: instance.web.unblockUI, - }); + instance.web.blockUI(); + this.session.get_file({ + url: '/web/export/' + export_format, + data: {data: JSON.stringify({ + model: this.dataset.model, + fields: exported_fields, + ids: this.ids_to_export, + domain: this.domain, + import_compat: !!this.$el.find("#import_compat").val(), + })}, + complete: instance.web.unblockUI, }); }, close: function() { diff --git a/addons/web/static/src/js/openerpframework.js b/addons/web/static/src/js/openerpframework.js index 027452b42fb..ab10d304b30 100644 --- a/addons/web/static/src/js/openerpframework.js +++ b/addons/web/static/src/js/openerpframework.js @@ -138,6 +138,7 @@ var openerp = {}; throw new Error("You can only instanciate objects with the 'new' operator"); } // All construction is actually done in the init method + this._super = null; if (!initializing && this.init) { var ret = this.init.apply(this, arguments); if (ret) { return ret; } @@ -385,6 +386,14 @@ var Events = openerp.Class.extend({ } }); +/** + Mixin containing an event system. Events are also registered by specifying the target object + (the object which will receive the event when it is raised). Both the event-emitting object + and the target object store or reference to each other. This is used to correctly remove all + reference to the event handler when any of the object is destroyed (when the destroy() method + from ParentedMixin is called). Removing those references is necessary to avoid memory leak + and phantom events (events which are raised and sent to a previously destroyed object). +*/ openerp.EventDispatcherMixin = _.extend({}, openerp.ParentedMixin, { __eventDispatcherMixin: true, init: function() { @@ -808,7 +817,7 @@ var genericJsonRpc = function(fct_name, params, fct) { return result.result; } }, function() { - console.error("JsonRPC communication error", _.toArray(arguments)); + //console.error("JsonRPC communication error", _.toArray(arguments)); var def = $.Deferred(); return def.reject.apply(def, ["communication"].concat(_.toArray(arguments))); }); @@ -890,11 +899,29 @@ openerp.jsonpRpc = function(url, fct_name, params, settings) { }); // append the iframe to the DOM (will trigger the first load) $form.after($iframe); + if (settings.timeout) { + realSetTimeout(function() { + deferred.reject({}); + }, settings.timeout); + } return deferred; } }); }; +var realSetTimeout = function(fct, millis) { + var finished = new Date().getTime() + millis; + var wait = function() { + var current = new Date().getTime(); + if (current < finished) { + setTimeout(wait, finished - current); + } else { + fct(); + } + }; + setTimeout(wait, millis); +}; + openerp.Session = openerp.Class.extend(openerp.PropertiesMixin, { triggers: { 'request': 'Request sent', diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 6241fe6da28..e086678c937 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -1547,7 +1547,7 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ context: context }).then(function (results) { if (_.isEmpty(results)) { return null; } - return [{label: _.escape(self.attrs.string)}].concat( + return [{label: self.attrs.string}].concat( _(results).map(function (result) { return { label: _.escape(result[1]), diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 324896112cc..7a0890bdcfa 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3664,7 +3664,7 @@ instance.web.form.FieldOne2Many = instance.web.form.AbstractField.extend({ _.extend(view.options, { addable: null, selectable: self.multi_selection, - sortable: false, + sortable: true, import_enabled: false, deletable: true }); @@ -5475,7 +5475,7 @@ instance.web.form.FieldStatus = instance.web.form.AbstractField.extend({ }); this.get_selection(); if (this.options.clickable) { - this.$el.on('click','li:not(.oe_folded)',this.on_click_stage); + this.$el.on('click','li[data-id]',this.on_click_stage); } if (this.$el.parent().is('header')) { this.$el.after('<div class="oe_clear"/>'); @@ -5629,6 +5629,131 @@ instance.web.form.FieldMonetary = instance.web.form.FieldFloat.extend({ }, }); +/* + This type of field display a list of checkboxes. It works only with m2ms. This field will display one checkbox for each + record existing in the model targeted by the relation, according to the given domain if one is specified. Checked records + will be added to the relation. +*/ +instance.web.form.FieldMany2ManyCheckBoxes = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { + className: "oe_form_many2many_checkboxes", + init: function() { + this._super.apply(this, arguments); + this.set("value", {}); + this.set("records", []); + this.field_manager.on("view_content_has_changed", this, function() { + var domain = new openerp.web.CompoundDomain(this.build_domain()).eval(); + if (! _.isEqual(domain, this.get("domain"))) { + this.set("domain", domain); + } + }); + this.records_orderer = new instance.web.DropMisordered(); + }, + initialize_field: function() { + instance.web.form.ReinitializeFieldMixin.initialize_field.call(this) + this.on("change:domain", this, this.query_records); + this.set("domain", new openerp.web.CompoundDomain(this.build_domain()).eval()); + this.on("change:records", this, this.render_value); + }, + query_records: function() { + var self = this; + var model = new openerp.Model(openerp.session, this.field.relation); + this.records_orderer.add(model.call("search", [this.get("domain")], {"context": this.build_context()}).then(function(record_ids) { + return model.call("name_get", [record_ids] , {"context": self.build_context()}); + })).then(function(res) { + self.set("records", res); + }); + }, + render_value: function() { + this.$().html(QWeb.render("FieldMany2ManyCheckBoxes", {widget: this, selected: this.get("value")})); + var inputs = this.$("input"); + inputs.change(_.bind(this.from_dom, this)); + if (this.get("effective_readonly")) + inputs.attr("disabled", "true"); + }, + from_dom: function() { + var new_value = {}; + this.$("input").each(function() { + var elem = $(this); + new_value[elem.data("record-id")] = elem.attr("checked") ? true : undefined; + }); + if (! _.isEqual(new_value, this.get("value"))) + this.internal_set_value(new_value); + }, + set_value: function(value) { + value = value || []; + if (value.length >= 1 && value[0] instanceof Array) { + value = value[0][2]; + } + var formatted = {}; + _.each(value, function(el) { + formatted[JSON.stringify(el)] = true; + }); + this._super(formatted); + }, + get_value: function() { + var value = _.filter(_.keys(this.get("value")), function(el) { + return this.get("value")[el]; + }, this); + value = _.map(value, function(el) { + return JSON.parse(el); + }); + return [commands.replace_with(value)]; + }, +}); + +/** + This field can be applied on many2many and one2many. It is a read-only field that will display a single link whose name is + "<number of linked records> <label of the field>". When the link is clicked, it will redirect to another act_window + action on the model of the relation and show only the linked records. + + Widget options: + + * views: The views to display in the act_window action. Must be a list of tuples whose first element is the id of the view + to display (or False to take the default one) and the second element is the type of the view. Defaults to + [[false, "tree"], [false, "form"]] . +*/ +instance.web.form.X2ManyCounter = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeFieldMixin, { + className: "oe_form_x2many_counter", + init: function() { + this._super.apply(this, arguments); + this.set("value", []); + _.defaults(this.options, { + "views": [[false, "tree"], [false, "form"]], + }); + }, + render_value: function() { + var text = _.str.sprintf("%d %s", this.val().length, this.string); + this.$().html(QWeb.render("X2ManyCounter", {text: text})); + this.$("a").click(_.bind(this.go_to, this)); + }, + go_to: function() { + return this.view.recursive_save().then(_.bind(function() { + var val = this.val(); + var context = {}; + if (this.field.type === "one2many") { + context["default_" + this.field.relation_field] = this.view.datarecord.id; + } + var domain = [["id", "in", val]]; + return this.do_action({ + type: 'ir.actions.act_window', + name: this.string, + res_model: this.field.relation, + views: this.options.views, + target: 'current', + context: context, + domain: domain, + }); + }, this)); + }, + val: function() { + var value = this.get("value") || []; + if (value.length >= 1 && value[0] instanceof Array) { + value = value[0][2]; + } + return value; + } +}); + /** * Registry of form fields, called by :js:`instance.web.FormView`. * @@ -5664,6 +5789,8 @@ instance.web.form.widgets = new instance.web.Registry({ 'many2many_binary': 'instance.web.form.FieldMany2ManyBinaryMultiFiles', 'statusbar': 'instance.web.form.FieldStatus', 'monetary': 'instance.web.form.FieldMonetary', + 'many2many_checkboxes': 'instance.web.form.FieldMany2ManyCheckBoxes', + 'x2many_counter': 'instance.web.form.X2ManyCounter', }); /** diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index f9ec22bf712..ba7c58b3820 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -508,10 +508,15 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi var reloaded = $.Deferred(); this.$el.find('.oe_list_content').append( this.groups.render(function () { - if ((self.dataset.index === null || self.dataset.index === undefined) && self.records.length || - self.dataset.index >= self.records.length) { + // Keep '=='. This is not a mistake, this is a wanted behaviour to match null & undefined + if (self.dataset.index == null) { + if (self.records.length) { self.dataset.index = 0; + } + } else if (self.dataset.index >= self.records.length) { + self.dataset.index = 0; } + self.compute_aggregates(); reloaded.resolve(); })); @@ -802,9 +807,11 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi }, /** * Calculate the active domain of the list view. This should be done only - * if the header checkbox has been checked. + * if the header checkbox has been checked. This is done by evaluating the + * search results, and then adding the dataset domain (i.e. action domain). */ get_active_domain: function () { + var self = this; if (this.$('.oe_list_record_selector').prop('checked')) { var search_view = this.getParent().searchview; var search_data = search_view.build_search_data(); @@ -813,7 +820,8 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi contexts: search_data.contexts, group_by_seq: search_data.groupbys || [] }).then(function (results) { - return results.domain; + var domain = self.dataset.domain.concat(results.domain || []); + return domain }); } else { diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index ee87237d045..46241435fcf 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1228,9 +1228,9 @@ </li> </t> <t t-if="widget.selection.folded.length"> - <li t-att-class="value_folded ? 'oe_active oe_folded' : 'oe_folded'"> - <span class="label"> ... </span> - <ul> + <li> + <span class="label oe_dropdown_toggle oe_dropdown_arrow">More</span> + <ul class="oe_dropdown_menu"> <t t-foreach="widget.selection.folded" t-as="i"> <li t-att-class="i[0] === widget.get('value') ? 'oe_active' : ''" t-att-data-id="i[0]"> <span class="label"><t t-esc="i[1]"/></span> @@ -1775,6 +1775,14 @@ </td> </tr> </table> +<t t-name="ExportTreeView-Domain"> + <t t-if="! record.ids_to_export"> + <p><strong>Please pay attention that all records matching your search filter will be exported. Not only the selected ids.</strong></p> + </t> + <t t-if="record.ids_to_export"> + <p><strong>Please note that only the selected ids will be exported.</strong></p> + </t> +</t> <table t-name="ExportTreeView-Secondary" id="field-tree-structure" class="oe_export_fields_selector_export" cellspacing="0" cellpadding="0"> @@ -1934,4 +1942,20 @@ </t> </t> </t> +<t t-name="FieldMany2ManyCheckBoxes"> + <t t-foreach="widget.get('records')" t-as="record"> + <div> + <t t-if="selected[record[0]]"> + <input type="checkbox" t-att-data-record-id="JSON.stringify(record[0])" checked="true"/> + </t> + <t t-if="! selected[record[0]]"> + <input type="checkbox" t-att-data-record-id="JSON.stringify(record[0])"/> + </t> + <t t-esc="record[1]"/> + </div> + </t> +</t> +<t t-name="X2ManyCounter"> + <a href="javascript:void(0)"><t t-esc="text"/></a> +</t> </templates> |
