diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index bf9013a43c8..934fc5101d6 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -741,9 +741,6 @@ display: block; color: #4c4c4c; text-decoration: none; - width: 200px; - text-overflow: ellipsis; - overflow: hidden; } .openerp .oe_dropdown_menu > li > a:hover { text-decoration: none; @@ -1455,7 +1452,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 { diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index 42cfe615d21..84f0f401e83 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -632,9 +632,6 @@ $sheet-padding: 16px display: block color: #4c4c4c text-decoration: none - width: 200px - text-overflow: ellipsis - overflow: hidden &:hover text-decoration: none .oe_dropdown_arrow:after @@ -1171,7 +1168,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 diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 679afe77bcd..d497cf25343 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -432,7 +432,7 @@ instance.web.DataSet = instance.web.Class.extend(instance.web.PropertiesMixin, // TODO: reorder results to match ids list return this._model.call('read', [ids, fields || false], - {context: this._model.context(options.context)}); + {context: this.get_context(options.context)}); }, /** * Read a slice of the records represented by this DataSet, based on its @@ -724,7 +724,7 @@ instance.web.DataSetSearch = instance.web.DataSet.extend({ }); }, get_domain: function (other_domain) { - this._model.domain(other_domain); + return this._model.domain(other_domain); }, alter_ids: function (ids) { this._super(ids); @@ -761,6 +761,7 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ this._super.apply(this, arguments); this.reset_ids([]); this.last_default_get = {}; + this.running_reads = []; }, default_get: function(fields, options) { var self = this; @@ -827,6 +828,9 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ this.to_write = []; this.cache = []; this.delete_all = false; + _.each(_.clone(this.running_reads), function(el) { + el.reject(); + }); }, read_ids: function (ids, fields, options) { var self = this; @@ -842,7 +846,6 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ to_get.push(id); } }); - var completion = $.Deferred(); var return_records = function() { var records = _.map(ids, function(id) { return _.extend({}, _.detect(self.cache, function(c) {return c.id === id;}).values, {"id": id}); @@ -877,10 +880,20 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ }, 0); }); } - completion.resolve(records); + return $.when(records); }; if(to_get.length > 0) { - var rpc_promise = this._super(to_get, fields, options).done(function(records) { + var def = $.Deferred(); + self.running_reads.push(def); + def.always(function() { + self.running_reads = _.without(self.running_reads, def); + }); + this._super(to_get, fields, options).then(function() { + def.resolve.apply(def, arguments); + }, function() { + def.reject.apply(def, arguments); + }); + return def.then(function(records) { _.each(records, function(record, index) { var id = to_get[index]; var cached = _.detect(self.cache, function(x) {return x.id === id;}); @@ -891,13 +904,11 @@ instance.web.BufferedDataSet = instance.web.DataSetStatic.extend({ cached.values = _.defaults(_.clone(cached.values), record); } }); - return_records(); + return return_records(); }); - $.when(rpc_promise).fail(function() {completion.reject();}); } else { - return_records(); + return return_records(); } - return completion.promise(); }, /** * Invalidates caching of a record in the dataset to ensure the next read diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 5d06d75573f..0d66a978311 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -1540,7 +1540,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]), @@ -1726,7 +1726,10 @@ instance.web.search.CustomFilters = instance.web.search.Input.extend({ var $name = this.$('input:first'); var private_filter = !this.$('#oe_searchview_custom_public').prop('checked'); var set_as_default = this.$('#oe_searchview_custom_default').prop('checked'); - + if (_.isEmpty($name.val())){ + this.do_warn(_t("Error"), _t("Filter name is required.")); + return false; + } var search = this.view.build_search_data(); instance.web.pyeval.eval_domains_and_contexts({ domains: search.domains, diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 33bca889dc0..12b2e241f76 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -3027,7 +3027,7 @@ instance.web.form.CompletionFieldMixin = { values.push({ label: _t("Search More..."), action: function() { - dataset.name_search(search_val, self.build_domain(), 'ilike', false).done(function(data) { + dataset.name_search(search_val, self.build_domain(), 'ilike', 160).done(function(data) { self._search_create_popup("search", data); }); }, @@ -3649,7 +3649,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 }); @@ -4044,7 +4044,13 @@ instance.web.form.One2ManyListView = instance.web.ListView.extend({ else return $.when(); }).done(function () { - self.handle_button(name, id, callback); + if (!self.o2m.options.reload_on_button) { + self.handle_button(name, id, callback); + }else { + self.handle_button(name, id, function(){ + self.o2m.view.reload(); + }); + } }); }, diff --git a/addons/web/static/src/js/view_list.js b/addons/web/static/src/js/view_list.js index 74c2fc47aee..b9c789cb6a9 100644 --- a/addons/web/static/src/js/view_list.js +++ b/addons/web/static/src/js/view_list.js @@ -496,17 +496,21 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi * * @returns {$.Deferred} promise to content reloading */ - reload_content: function () { + reload_content: synchronized(function () { var self = this; self.$el.find('.oe_list_record_selector').prop('checked', false); this.records.reset(); var reloaded = $.Deferred(); this.$el.find('.oe_list_content').append( this.groups.render(function () { - if (self.dataset.index == null && self.records.length || - self.dataset.index >= self.records.length) { + 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(); })); @@ -515,7 +519,7 @@ instance.web.ListView = instance.web.View.extend( /** @lends instance.web.ListVi limit: this._limit }); return reloaded.promise(); - }, + }), reload: function () { return this.reload_content(); }, @@ -1328,6 +1332,9 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we .removeClass('ui-icon-triangle-1-s') .addClass('ui-icon-triangle-1-e'); child.close(); + // force recompute the selection as closing group reset properties + var selection = self.get_selection(); + $(self).trigger('selected', [selection.ids, this.records]); } }); } @@ -1338,22 +1345,29 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we if (group.grouped_on) { var row_data = {}; row_data[group.grouped_on] = group; + var group_label = _t("Undefined"); var group_column = _(self.columns).detect(function (column) { return column.id === group.grouped_on; }); - if (! group_column) { - throw new Error(_.str.sprintf( - _t("Grouping on field '%s' is not possible because that field does not appear in the list view."), - group.grouped_on)); - } - var group_label; - try { - group_label = group_column.format(row_data, { - value_if_empty: _t("Undefined"), - process_modifiers: false - }); - } catch (e) { - group_label = _.str.escapeHTML(row_data[group_column.id].value); + if (group_column) { + try { + group_label = group_column.format(row_data, { + value_if_empty: _t("Undefined"), + process_modifiers: false + }); + } catch (e) { + group_label = _.str.escapeHTML(row_data[group_column.id].value); + } + } else { + group_label = group.value; + if (group_label instanceof Array) { + group_label = group_label[1]; + } + if (group_label === false) { + group_label = _t('Undefined'); + } + group_label = _.str.escapeHTML(group_label); } + // group_label is html-clean (through format or explicit // escaping if format failed), can inject straight into HTML $group_column.html(_.str.sprintf(_t("%s (%d)"), @@ -1425,14 +1439,13 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we var view = this.view, limit = view.limit(), - d = new $.Deferred(), page = this.datagroup.openable ? this.page : view.page; var fields = _.pluck(_.select(this.columns, function(x) {return x.tag == "field"}), 'name'); var options = { offset: page * limit, limit: limit, context: {bin_size: true} }; //TODO xmo: investigate why we need to put the setTimeout - $.async_when().done(function() { - dataset.read_slice(fields, options).done(function (records) { + return $.async_when().then(function() { + return dataset.read_slice(fields, options).then(function (records) { // FIXME: ignominious hacks, parents (aka form view) should not send two ListView#reload_content concurrently if (self.records.length) { self.records.reset(null, {silent: true}); @@ -1464,13 +1477,12 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we self.records.add(records, {silent: true}); list.render(); - d.resolve(list); if (_.isEmpty(records)) { view.no_result(); } + return list; }); }); - return d.promise(); }, setup_resequence_rows: function (list, dataset) { // drag and drop enabled if list is not sorted and there is a @@ -1550,11 +1562,12 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we self.render_groups(groups)); if (post_render) { post_render(); } }, function (dataset) { - self.render_dataset(dataset).done(function (list) { + self.render_dataset(dataset).then(function (list) { self.children[null] = list; self.elements = [list.$current.replaceAll($el)[0]]; self.setup_resequence_rows(list, dataset); + }).always(function() { if (post_render) { post_render(); } }); }); @@ -1597,6 +1610,22 @@ instance.web.ListView.Groups = instance.web.Class.extend( /** @lends instance.we } }); +/** + * Serializes concurrent calls to this asynchronous method. The method must + * return a deferred or promise. + * + * Current-implementation is class-serialized (the mutex is common to all + * instances of the list view). Can be switched to instance-serialized if + * having concurrent list views becomes possible and common. + */ +function synchronized(fn) { + var fn_mutex = new $.Mutex(); + return function () { + var args = _.toArray(arguments); + args.unshift(this); + return fn_mutex.exec(fn.bind.apply(fn, args)); + }; +} var DataGroup = instance.web.Class.extend({ init: function(parent, model, domain, context, group_by, level) { this.model = new instance.web.Model(model, context, domain); @@ -1608,6 +1637,10 @@ var DataGroup = instance.web.Class.extend({ }, list: function (fields, ifGroups, ifRecords) { var self = this; + if (!_.isEmpty(this.group_by)) { + // ensure group_by fields are read. + fields = _.unique((fields || []).concat(this.group_by)); + } var query = this.model.query(fields).order_by(this.sort).group_by(this.group_by); $.when(query).done(function (querygroups) { // leaf node diff --git a/addons/web/static/src/js/view_tree.js b/addons/web/static/src/js/view_tree.js index 2f9efad9171..70873f48d6f 100644 --- a/addons/web/static/src/js/view_tree.js +++ b/addons/web/static/src/js/view_tree.js @@ -163,12 +163,13 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie var is_loaded = 0, $this = $(this), record_id = $this.data('id'), + row_parent_id = $this.data('row-parent-id'), record = self.records[record_id], children_ids = record[self.children_field]; _(children_ids).each(function(childid) { - if (self.$el.find('#treerow_' + childid).length) { - if (self.$el.find('#treerow_' + childid).is(':hidden')) { + if (self.$el.find('[id=treerow_' + childid + '][data-row-parent-id='+ record_id +']').length ) { + if (self.$el.find('[id=treerow_' + childid + '][data-row-parent-id='+ record_id +']').is(':hidden')) { is_loaded = -1; } else { is_loaded++; @@ -180,7 +181,7 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie self.getdata(record_id, children_ids); } } else { - self.showcontent(record_id, is_loaded < 0); + self.showcontent($this, record_id, is_loaded < 0); } }); }, @@ -192,7 +193,6 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie _(records).each(function (record) { self.records[record.id] = record; }); - var $curr_node = self.$el.find('#treerow_' + id); var children_rows = QWeb.render('TreeView.rows', { 'records': records, @@ -201,9 +201,9 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie 'fields': self.fields, 'level': $curr_node.data('level') || 0, 'render': instance.web.format_value, - 'color_for': self.color_for + 'color_for': self.color_for, + 'row_parent_id': id }); - if ($curr_node.length) { $curr_node.addClass('oe_open'); $curr_node.after(children_rows); @@ -243,14 +243,13 @@ instance.web.TreeView = instance.web.View.extend(/** @lends instance.web.TreeVie }, // show & hide the contents - showcontent: function (record_id, show) { - this.$el.find('#treerow_' + record_id) - .toggleClass('oe_open', show); - + showcontent: function (curnode,record_id, show) { + curnode.parent('tr').toggleClass('oe_open', show); _(this.records[record_id][this.children_field]).each(function (child_id) { - var $child_row = this.$el.find('#treerow_' + child_id); + var $child_row = this.$el.find('[id=treerow_' + child_id + '][data-row-parent-id='+ curnode.data('id') +']'); if ($child_row.hasClass('oe_open')) { - this.showcontent(child_id, false); + $child_row.toggleClass('oe_open',show); + this.showcontent($child_row, child_id, false); } $child_row.toggle(show); }, this); diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 1d44165dcb2..715ed094092 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -684,7 +684,8 @@