From 0592305126f04c171ad38ebd2393526e01bf2cb9 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Wed, 19 Aug 2015 17:35:44 +0200 Subject: [PATCH] [REF] web: data.js: refactoring of BufferedDataSet (less code and speed up) --- addons/web/static/src/js/framework/data.js | 212 ++++++------------ .../src/js/views/form_relational_widgets.js | 97 ++++---- addons/web/static/src/js/views/form_view.js | 1 - 3 files changed, 118 insertions(+), 192 deletions(-) diff --git a/addons/web/static/src/js/framework/data.js b/addons/web/static/src/js/framework/data.js index 9530486066d..a1fdd394937 100644 --- a/addons/web/static/src/js/framework/data.js +++ b/addons/web/static/src/js/framework/data.js @@ -739,155 +739,93 @@ var BufferedDataSet = DataSetStatic.extend({ self.last_default_get = res; }); }, + get_cache: function (id) { + if (!this.cache[id]) { + this.cache[id] = { + 'id': id, + 'values': {}, + 'from_read': {}, + 'changes': {}, + 'readonly_fields': {}, + 'to_create': false, + 'to_delete': false}; + } + return this.cache[id]; + }, _update_cache: function (id, options) { // One should call this method after modifying this.from_read, - // this.to_create or this.to_write. It updates this.cache and - // this.from_readonly. - var cached = _.detect(this.cache, function(x) {return x.id === id;}); - if (!cached) { - this.cache.push(cached = {id: id, values: {}}); - } - var record = _.detect(this.from_read, function(x) {return x.id === id;}); - if (record) { - cached.values = _.clone(record.values); - } else if (record = _.detect(this.to_create, function(x) {return x.id === id;})) { - cached.values = _.clone(record.values); - cached.values = _.extend(cached.values, record.defaults); - } else { - record = {}; - } - - var record_to_write = _.detect(this.to_write, function(x) {return x.id === id;}); - if (record_to_write) { - _.extend(cached.values, record_to_write.values); - } - - var record_from_readonly = _.detect(this.from_readonly, function(x) {return x.id === id;}); - if (options && options.readonly_fields) { - if (!record_from_readonly) { - this.from_readonly.push(record_from_readonly = {id: id, values: {}}); - } - _.extend(record_from_readonly.values, options.readonly_fields); - } - if (record_from_readonly) { - _.extend(cached.values, record_from_readonly.values); + // this.to_create or this.change. It updates this.cache and + // this.readonly_fields. + var cached = this.get_cache(id); + if (options) { + _.extend(cached.from_read, options.from_read); + _.extend(cached.changes, options.changes); + _.extend(cached.readonly_fields, options.readonly_fields); + if (options.to_create !== undefined) cached.to_create = options.to_create; + if (options.to_delete !== undefined) cached.to_delete = options.to_delete; } + cached.values = _.extend({'id': id}, cached.from_read, cached.changes, cached.readonly_fields); return cached; }, - create: function(data, options) { - var cached = { - id:_.uniqueId(this.virtual_id_prefix), - values: _.extend({}, data, (options || {}).readonly_fields || {}), - defaults: this.last_default_get - }; - this.to_create.push(_.clone(cached)); - this._update_cache(cached.id, options); - return $.Deferred().resolve(cached.id).promise(); + create: function(data, options) { + var changes = _.extend({}, this.last_default_get, data); + var cached = this._update_cache(_.uniqueId(this.virtual_id_prefix), _.extend({'changes': changes, 'to_create': true}, options)); + this.trigger("dataset_changed", data, options); + return $.Deferred().resolve(cached.id); }, write: function (id, data, options) { var self = this; - var cached = this._update_cache(id, options); + var cached = this.get_cache(id); - // read missing field to create the to_write diff - var def_read = $.Deferred(); - this.mutex.def.then(function () { - if (typeof id !== "number" && id.indexOf(self.virtual_id_prefix) === 0) { - return def_read.resolve(); - } - var data_fields = _.keys(data); - var record_fields = _.keys((_.detect(self.from_read, function(x) {return x.id === id;}) || {}).values); - var fields = _.union(data_fields, record_fields); - if (fields.length !== data_fields.length || fields.length !== record_fields.length) { - self.read_ids(self.ids, fields).always(function () { - def_read.resolve(); - }); - } else { - def_read.resolve(); - } - }); + // if update after a remove, it's like an add before updating + cached.to_delete = false; // apply change var def = $.Deferred(); - def_read.then(function () { - self.mutex.exec(function () { - var dirty = false; - - var record_to_create = _.detect(self.to_create, function(x) {return x.id === id;}); - if (record_to_create) { - // update record in self.to_create - _.each(data, function (v, k) { - if (!_.isEqual(v, cached.values[k])) { - record_to_create.values[k] = v; - dirty = true; - } else { - delete data[k]; - } - }); + this.mutex.exec(function () { + var dirty = false; + _.each(data, function (v, k) { + if (!_.isEqual(v, cached.values[k])) { + dirty = true; + if (_.isEqual(v, cached.from_read[k])) { // clean changes + delete cached.changes[k]; + } else { + cached.changes[k] = v; + } } else { - // check if the record has changed, and update self.to_write accordingly - var record_to_write = _.detect(self.to_write, function(x) {return x.id === id;}); - if (!record_to_write) { - self.to_write.push(record_to_write = {id: id, values: {}}); - } - - var record_from_read = _.detect(self.from_read, function(x) {return x.id === id;}); - if (!record_from_read) { - record_from_read = {values: {}}; - } - _.each(data, function (v, k) { - if (!_.isEqual(v, cached.values[k])) { - dirty = true; - if (_.isEqual(v, record_from_read.values[k])) { // clean changes - delete record_to_write.values[k]; - } else { - record_to_write.values[k] = v; - } - } else { - delete data[k]; - } - }); - if (_.isEmpty(record_to_write.values)) { - self.to_write.splice(self.to_write.indexOf(record_to_write), 1); - } + delete data[k]; } - - self._update_cache(id, options); - - if (dirty) { - self.trigger("dataset_changed", id, data, options); - } - - return def.resolve(data).promise(); }); + self._update_cache(id, options); + + if (dirty) { + self.trigger("dataset_changed", id, data, options); + } + + return def.resolve(data).promise(); }); return def; }, unlink: function(ids, callback, error_callback) { var self = this; - _.each(ids, function(id) { - if (! _.detect(self.to_create, function(x) { return x.id === id; })) { - self.to_delete.push({id: id}); - } + _.each(ids, function (id) { + self.get_cache(id).to_delete = true; }); - this.from_read = _.reject(this.from_read, function(x) { return _.include(ids, x.id);}); - this.to_create = _.reject(this.to_create, function(x) { return _.include(ids, x.id);}); - this.to_write = _.reject(this.to_write, function(x) { return _.include(ids, x.id);}); - this.cache = _.reject(this.cache, function(x) { return _.include(ids, x.id);}); - this.set_ids(_.without.apply(_, [this.ids].concat(ids))); + this.set_ids(_.without(this.ids, _.pluck(_.filter(this.cache, function (c) {return c.to_delete;}), 'id'))); this.trigger("dataset_changed", ids, callback, error_callback); return $.async_when({result: true}).done(callback); }, reset_ids: function(ids, options) { this.set_ids(ids); - this.to_delete = []; - this.to_create = []; - this.to_write = []; if (!options || !options.keep_read_data) { - this.from_read = []; - this.from_readonly = []; + this.cache = {}; + } else { + _.each(this.cache, function (cache) { + cache.changes = {}; + cache.to_delete = false; + }); } - this.cache = []; this.delete_all = false; _.each(_.clone(this.running_reads), function(el) { el.reject(); @@ -897,21 +835,15 @@ var BufferedDataSet = DataSetStatic.extend({ // read what is necessary from the server to have ids and the given // fields in this.from_read var self = this; - var to_get = []; - options = options || {}; - _.each(ids, function(id) { - if (typeof id === 'number' || id.indexOf(self.virtual_id_prefix) === -1) { - var record = _.detect(self.from_read, function(x) {return x.id === id;}); - if (!record || !_.all(fields, function(x) {return record.values[x] !== undefined;})) { - to_get.push(id); - } - } + var to_get = _.filter(ids, function(id) { + var cache = self.get_cache(id); + return !cache.to_create && _.any(fields, function(x) {return cache.from_read[x] === undefined;}); }); + options = options || {}; var return_records = function() { var records = _.map(ids, function(id) { - var cache = _.find(self.cache, function(cache) {return cache.id === id;}); - return _.extend({}, cache && cache.values, {"id": id}, options.readonly_fields || {}); + return self.get_cache(id).values; }); if (self.debug_mode) { if (_.include(records, undefined)) { @@ -970,13 +902,7 @@ var BufferedDataSet = DataSetStatic.extend({ _.each(records, function(record, index) { // add information into from_read var id = to_get[index]; - var record_from_read = _.detect(self.from_read, function(x) {return x.id === id;}); - if (record_from_read) { - _.extend(record_from_read.values, record); - } else { - self.from_read.push({id: id, values: record}); - } - self._update_cache(id, options); + self._update_cache(id, _.extend(options, {'from_read': record})); }); return return_records(); }); @@ -997,10 +923,9 @@ var BufferedDataSet = DataSetStatic.extend({ // Don't evict records which haven't yet been saved: there is no more // recent data on the server (and there potentially isn't any data), // and this breaks the assumptions of other methods (that the data - // for new and altered records is both in the cache and in the to_write + // for new and altered records is both in the cache and in the change // or to_create collection) - this.from_read = _.reject(this.from_read, function(x) { return x.id === id;}); - this._update_cache(id); + this._update_cache(id, {'from_read': {}}); }, call_button: function (method, args) { this.evict_record(args[0][0]); @@ -1011,8 +936,11 @@ var BufferedDataSet = DataSetStatic.extend({ return this._super(id, signal); }, alter_ids: function(n_ids) { + var dirty = !_.isEqual(this.ids, n_ids); this._super(n_ids); - this.trigger("dataset_changed", n_ids); + if (dirty) { + this.trigger("dataset_changed", n_ids); + } }, }); diff --git a/addons/web/static/src/js/views/form_relational_widgets.js b/addons/web/static/src/js/views/form_relational_widgets.js index a3e0286fb28..b5aaf38b5fa 100644 --- a/addons/web/static/src/js/views/form_relational_widgets.js +++ b/addons/web/static/src/js/views/form_relational_widgets.js @@ -442,19 +442,20 @@ var AbstractManyField = common.AbstractField.extend({ this.set('value', []); this.starting_ids = []; this.mutex = new utils.Mutex(); - this.has_not_committed_changes = false; this.view.on("load_record", this, this._on_load_record); this.dataset.on('dataset_changed', this, function() { - self.has_not_committed_changes = true; + var options = _.clone(_.last(arguments)); + if (!_.isObject(options) || _.isArray(options)) { + options = {}; + } // don't trigger changes if all commands are not resolved // the editable lists change the dataset without call AbstractManyField methods - if (self.mutex.def.state() === "resolved" && !self.internal_dataset_changed) { - self.trigger("change:commands"); + if (!self.internal_dataset_changed && !options.internal_dataset_changed) { + self.trigger("change:commands", options); } }); this.on("change:commands", this, function (options) { - self.has_not_committed_changes = false; - self._inhibit_on_change_flag = !!(options||{})._inhibit_on_change_flag; + self._inhibit_on_change_flag = !!options._inhibit_on_change_flag; self.set({'value': self.dataset.ids.slice()}); self._inhibit_on_change_flag = false; }); @@ -463,9 +464,10 @@ var AbstractManyField = common.AbstractField.extend({ _on_load_record: function (record) { this.starting_ids = []; // don't set starting_ids for the new record - if (record.id && (!isNaN(record.id) || record.id.indexOf(this.dataset.virtual_id_prefix) === -1)) { + if (record.id && record[this.name] && (!isNaN(record.id) || record.id.indexOf(this.dataset.virtual_id_prefix) === -1)) { this.starting_ids = record[this.name].slice(); } + this.trigger("load_record", record); }, set_value: function(ids) { @@ -574,6 +576,7 @@ var AbstractManyField = common.AbstractField.extend({ var dataset = this.dataset; var res = true; options = options || {}; + var internal_options = _.extend({}, options, {'internal_dataset_changed': true}); _.each(command_list, function(command) { self.mutex.exec(function() { @@ -582,12 +585,12 @@ var AbstractManyField = common.AbstractField.extend({ case COMMANDS.CREATE: var data = _.clone(command[2]); delete data.id; - return dataset.create(data, options).then(function (id) { + return dataset.create(data, internal_options).then(function (id) { dataset.ids.push(id); res = id; }); case COMMANDS.UPDATE: - return dataset.write(id, command[2], options).then(function () { + return dataset.write(id, command[2], internal_options).then(function () { if (dataset.ids.indexOf(id) === -1) { dataset.ids.push(id); res = id; @@ -599,14 +602,14 @@ var AbstractManyField = common.AbstractField.extend({ return dataset.unlink([id]); case COMMANDS.LINK_TO: if (dataset.ids.indexOf(id) === -1) { - return dataset.add_ids([id], options); + return dataset.add_ids([id], internal_options); } return; case COMMANDS.DELETE_ALL: return dataset.reset_ids([], {keep_read_data: true}); case COMMANDS.REPLACE_WITH: dataset.ids = []; - return dataset.alter_ids(command[2], options); + return dataset.alter_ids(command[2], internal_options); default: throw new Error("send_commands to '"+self.name+"' receive a non command value." + "\n" + JSON.stringify(command_list)); @@ -631,29 +634,27 @@ var AbstractManyField = common.AbstractField.extend({ replace_with_ids = [], add_ids = [], command_list = [], - id, index, alter_order; + id, index, record; - function format_many2one (values) { - values = _.clone(values); - for (var k in values) { - if ((values[k] instanceof Array) && values[k].length === 2 && typeof values[k][0] === "number" && typeof values[k][1] === "string") { - values[k] = values[k][0]; - } - } - return values; - } - _.each(this.get('value'), function (id) { index = starting_ids.indexOf(id); if (index !== -1) { starting_ids.splice(index, 1); } - if (alter_order = _.detect(self.dataset.to_create, function(x) {return x.id === id;})) { - command_list.push(COMMANDS.create(format_many2one(alter_order.values))); - return; - } - if (alter_order = _.detect(self.dataset.to_write, function(x) {return x.id === id;})) { - command_list.push(COMMANDS.update(alter_order.id, format_many2one(alter_order.values))); + var record = self.dataset.get_cache(id); + if (!_.isEmpty(record.changes)) { + var values = _.clone(record.changes); + // format many2one values + for (var k in values) { + if ((values[k] instanceof Array) && values[k].length === 2 && typeof values[k][0] === "number" && typeof values[k][1] === "string") { + values[k] = values[k][0]; + } + } + if (record.to_create) { + command_list.push(COMMANDS.create(values)); + } else { + command_list.push(COMMANDS.update(record.id, values)); + } return; } if (!is_one2many || self.dataset.delete_all) { @@ -683,7 +684,7 @@ var AbstractManyField = common.AbstractField.extend({ }, is_valid: function () { - return this.mutex.def.state() === "resolved" && !this.has_not_committed_changes && this._super(); + return this.mutex.def.state() === "resolved" && this._super(); }, is_false: function() { @@ -853,9 +854,7 @@ var FieldX2Many = AbstractManyField.extend({ if (view && view.type === "list" && view.controller.__focus) { var def = $.Deferred(); view.controller._on_blur_one2many(true).always(function () { - setTimeout(function () {def.resolve();},0); - }, function () { - setTimeout(function () {def.reject();},0); + def.resolve(); }); return def; } @@ -887,17 +886,7 @@ var X2ManyDataSet = data.BufferedDataSet.extend({ self.context.add(context); }); return this.context; - }, - create: function(data, options) { - var self = this; - var def = this._super(data, options); - def.then(function (id) { - setTimeout(function () { - self.trigger("dataset_changed", id, data, options); - },0); - }); - return def; - }, + } }); var X2ManyViewManager = ViewManager.extend({ @@ -1046,6 +1035,7 @@ var X2ManyList = ListView.List.extend({ var One2ManyListView = X2ManyListView.extend({ _template: 'One2Many.listview', init: function (parent, dataset, view_id, options) { + var self = this; this._super(parent, dataset, view_id, _.extend(options || {}, { GroupsType: One2ManyGroups, ListType: X2ManyList @@ -1065,6 +1055,13 @@ var One2ManyListView = X2ManyListView.extend({ } }; $(document).on('mousedown', this._mousedown_blur_line); + + this.dataset.on('dataset_changed', this, function () { + self._dataset_changed = true; + }); + this.dataset.x2m.on('load_record', this, function () { + self._dataset_changed = false; + }); }, do_add_record: function () { if (this.editable()) { @@ -1130,10 +1127,10 @@ var One2ManyListView = X2ManyListView.extend({ return $.when(); }).done(function () { var ds = self.x2m.dataset; - var cached_records = _.any([ds.to_create, ds.to_delete, ds.to_write], function(value) { - return value.length; + var changed_records = _.find(ds.cache, function(record) { + return record.to_create || record.to_delete || !_.isEmpty(record.changes); }); - if (!self.x2m.options.reload_on_button && !cached_records) { + if (!self.x2m.options.reload_on_button && !changed_records) { self.handle_button(name, id, callback); }else { self.handle_button(name, id, function(){ @@ -1160,6 +1157,7 @@ var One2ManyListView = X2ManyListView.extend({ }, _on_focus_one2many: function () { this.dataset.x2m.internal_dataset_changed = true; + this._dataset_changed = false; this.__focus = true; }, _on_blur_one2many: function (force) { @@ -1180,11 +1178,12 @@ var One2ManyListView = X2ManyListView.extend({ def = this.cancel_edition(); } - def.then(function () { - if (self.dataset.x2m.has_not_committed_changes) { + def.then(function () { + if (self._dataset_changed) { self.dataset.trigger('dataset_changed'); } - }); + }); + return def; }, _after_edit: function () { diff --git a/addons/web/static/src/js/views/form_view.js b/addons/web/static/src/js/views/form_view.js index 9beb6a8c9a0..fcd33ccacbc 100644 --- a/addons/web/static/src/js/views/form_view.js +++ b/addons/web/static/src/js/views/form_view.js @@ -617,7 +617,6 @@ var FormView = View.extend(common.FieldManagerMixin, { }); }); - var args = _.toArray(arguments); return mutex.def.then(function () { return self.onchanges_mutex.def; }).then(function() { var save_obj = self.save_list.pop(); if (save_obj) {