summaryrefslogtreecommitdiff
path: root/addons/web/static/src
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-01-10 11:41:35 +0100
committerChristophe Simonis <chs@odoo.com>2018-01-10 11:41:35 +0100
commit228ecdbe28246b0031a9cf7e63b90f4ae11131df (patch)
tree33a3c3807c98ee79352abce486decd365e33b1b9 /addons/web/static/src
parent8270b212909de5cb26e958c023a4f0a27947eeef (diff)
parentff0abd214c89dce19984ed0f3c72ea556e23da01 (diff)
[MERGE] forward port branch 11.0 up to ff0abd214c8
Diffstat (limited to 'addons/web/static/src')
-rw-r--r--addons/web/static/src/js/core/utils.js17
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js21
-rw-r--r--addons/web/static/src/js/fields/field_registry.js1
-rw-r--r--addons/web/static/src/js/fields/field_utils.js22
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js8
-rw-r--r--addons/web/static/src/js/services/data_manager.js6
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js402
-rw-r--r--addons/web/static/src/js/views/field_manager_mixin.js2
-rw-r--r--addons/web/static/src/js/views/form/form_controller.js35
-rw-r--r--addons/web/static/src/js/views/list/list_controller.js6
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js2
-rw-r--r--addons/web/static/src/js/widgets/debug_manager.js2
-rw-r--r--addons/web/static/src/xml/base.xml20
13 files changed, 385 insertions, 159 deletions
diff --git a/addons/web/static/src/js/core/utils.js b/addons/web/static/src/js/core/utils.js
index 3fc88ac5b20..99534708a94 100644
--- a/addons/web/static/src/js/core/utils.js
+++ b/addons/web/static/src/js/core/utils.js
@@ -349,6 +349,23 @@ var utils = {
].join(';');
},
/**
+ * Sort an array in place, keeping the initial order for identical values.
+ *
+ * @param {Array} array
+ * @param {function} iteratee
+ */
+ stableSort: function (array, iteratee) {
+ var stable = array.slice();
+ return array.sort(function stableCompare (a, b) {
+ var order = iteratee(a, b);
+ if (order !== 0) {
+ return order;
+ } else {
+ return stable.indexOf(a) - stable.indexOf(b);
+ }
+ });
+ },
+ /**
* @param {any} array
* @param {any} elem1
* @param {any} elem2
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index eb56844de14..d0662fe7cef 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -582,6 +582,7 @@ var FieldMonetary = InputField.extend({
this.formatOptions.currency = this.currency;
this.formatOptions.digits = [16, 2];
+ this.formatOptions.field_digits = this.nodeOptions.field_digits;
},
//--------------------------------------------------------------------------
@@ -1311,6 +1312,25 @@ var FieldBinaryFile = AbstractFieldBinary.extend({
},
});
+var FieldBinaryDownloadLink = AbstractFieldBinary.extend({
+ template: 'FieldBinaryDownloadLink',
+ events: _.extend({}, AbstractFieldBinary.prototype.events, {
+ 'click': function (ev) {
+ ev.stopPropagation();
+ },
+ }),
+ supportedFieldTypes: ['binary'],
+ init: function () {
+ this._super.apply(this, arguments);
+ this.filename = _t("Binary File");
+ if (this.nodeOptions.filename && this.recordData[this.nodeOptions.filename]) {
+ this.filename = this.recordData[this.nodeOptions.filename];
+ }
+ this.url = "data:application/octet-stream;base64," + this.value;
+ this.text = _t("Download");
+ },
+});
+
var PriorityWidget = AbstractField.extend({
// the current implementation of this widget makes it
// only usable for fields of type selection
@@ -2541,6 +2561,7 @@ return {
TranslatableFieldMixin: TranslatableFieldMixin,
DebouncedField: DebouncedField,
FieldEmail: FieldEmail,
+ FieldBinaryDownloadLink: FieldBinaryDownloadLink,
FieldBinaryFile: FieldBinaryFile,
FieldBinaryImage: FieldBinaryImage,
FieldBoolean: FieldBoolean,
diff --git a/addons/web/static/src/js/fields/field_registry.js b/addons/web/static/src/js/fields/field_registry.js
index 6692663d5f9..85b4fc1187f 100644
--- a/addons/web/static/src/js/fields/field_registry.js
+++ b/addons/web/static/src/js/fields/field_registry.js
@@ -33,6 +33,7 @@ registry
.add('email', basic_fields.FieldEmail)
.add('phone', basic_fields.FieldPhone)
.add('url', basic_fields.UrlWidget)
+ .add('download_link', basic_fields.FieldBinaryDownloadLink)
.add('image', basic_fields.FieldBinaryImage)
.add('binary', basic_fields.FieldBinaryFile)
.add('monetary', basic_fields.FieldMonetary)
diff --git a/addons/web/static/src/js/fields/field_utils.js b/addons/web/static/src/js/fields/field_utils.js
index 932ea997e6a..812a7f380f7 100644
--- a/addons/web/static/src/js/fields/field_utils.js
+++ b/addons/web/static/src/js/fields/field_utils.js
@@ -28,6 +28,20 @@ var _t = core._t;
//------------------------------------------------------------------------------
/**
+ *
+ * @param {string} value base64 representation of the binary
+ * @param {Object} [field]
+ * a description of the field (note: this parameter is ignored)
+ * @param {Object} [options] additional options (note: this parameter is ignored)
+ */
+function formatBinary(value, field, options) {
+ if (!value) {
+ return '';
+ }
+ return utils.human_size(value.length / 1.37); // base64 estimated size: http://en.wikipedia.org/wiki/Base64#MIME
+}
+
+/**
* @todo Really? it returns a jQuery element... We should try to avoid this and
* let DOM utility functions handle this directly. And replace this with a
* function that returns a string so we can get rid of the forceString.
@@ -286,8 +300,12 @@ function formatMonetary(value, field, options) {
currency = session.get_currency(currency_id);
}
+ var digits = (currency && currency.digits) || options.digits;
+ if (options.field_digits === true) {
+ digits = field.digits || digits;
+ }
var formatted_value = formatFloat(value, field, {
- digits: (currency && currency.digits) || options.digits,
+ digits: digits,
});
if (!currency || options.noSymbol) {
@@ -557,7 +575,7 @@ function parseMany2one(value) {
return {
format: {
- binary: _.identity, // todo
+ binary: formatBinary,
boolean: formatBoolean,
char: formatChar,
date: formatDate,
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index c4697cdafe1..bdafcee04da 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -1018,6 +1018,7 @@ var FieldX2Many = AbstractField.extend({
*/
_onEditLine: function (ev) {
ev.stopPropagation();
+ this.trigger_up('freeze_order', {id: this.value.id});
var editedRecord = this.value.data[ev.data.index];
this.renderer.setRowMode(editedRecord.id, 'edit')
.done(ev.data.onSuccess);
@@ -1104,6 +1105,7 @@ var FieldX2Many = AbstractField.extend({
*/
_onResequence: function (event) {
var self = this;
+ this.trigger_up('freeze_order', {id: this.value.id});
var rowIDs = event.data.rowIDs.slice();
var rowID = rowIDs.pop();
var defs = _.map(rowIDs, function (rowID, index) {
@@ -1123,6 +1125,11 @@ var FieldX2Many = AbstractField.extend({
operation: 'UPDATE',
id: rowID,
data: _.object([event.data.handleField], [event.data.offset + rowIDs.length]),
+ }).always(function () {
+ self.trigger_up('toggle_column_order', {
+ id: self.value.id,
+ name: event.data.handleField,
+ });
});
});
},
@@ -1229,6 +1236,7 @@ var FieldOne2Many = FieldX2Many.extend({
}
} else if (!this.creatingRecord) {
this.creatingRecord = true;
+ this.trigger_up('freeze_order', {id: this.value.id});
this._setValue({
operation: 'CREATE',
position: this.editable,
diff --git a/addons/web/static/src/js/services/data_manager.js b/addons/web/static/src/js/services/data_manager.js
index 47cd29399b5..72ec816efdd 100644
--- a/addons/web/static/src/js/services/data_manager.js
+++ b/addons/web/static/src/js/services/data_manager.js
@@ -365,12 +365,6 @@ return core.Class.extend({
child.attrs.modifiers.column_invisible || false;
}
});
-
- // detect editables list has they behave differently with respect
- // to the sorting (changes are not sorted directly)
- if (mode === 'list' && view.arch.attrs.editable) {
- attrs.keepChangesUnsorted = true;
- }
}
}
if (attrs.Widget.prototype.fieldsToFetch) {
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 39f1be0d107..3621b7c1227 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -37,6 +37,7 @@ odoo.define('web.BasicModel', function (require) {
* offset: {integer},
* openGroupByDefault: {boolean},
* orderedBy: {Object[]},
+ * orderedResIDs: {integer[]},
* parentID: {string},
* rawContext: {Object},
* relationField: {string},
@@ -88,6 +89,7 @@ var Context = require('web.Context');
var core = require('web.core');
var Domain = require('web.Domain');
var session = require('web.session');
+var utils = require('web.utils');
var _t = core._t;
@@ -95,11 +97,13 @@ var x2ManyCommands = {
// (0, virtualID, {values})
CREATE: 0,
create: function (virtualID, values) {
+ delete values.id;
return [x2ManyCommands.CREATE, virtualID || false, values];
},
// (1, id, {values})
UPDATE: 1,
update: function (id, values) {
+ delete values.id;
return [x2ManyCommands.UPDATE, id, values];
},
// (2, id[, _])
@@ -410,29 +414,11 @@ var BasicModel = AbstractModel.extend({
return record;
}
- // here, type === 'list'
- // apply potential changes (only for x2many lists):
- // for list having the flag _keepChangesUnsorted set to true (typically,
- // editable lists), we sort before apply changes by default (to keep the
- // changes unsorted, i.e. the edited line at the same place). However, if
- // the user forces a sort, all changes done so far are sorted (this is the
- // purpose of the _keepChangesUnsortedCount key), and changes done later
- // on won't be sorted.
- var count;
- if (element._keepChangesUnsorted) {
- // only apply a subset (by default 0) of changes before sorting
- count = element._keepChangesUnsortedCount || 0;
- element = this._applyX2ManyOperations(element, {to: count, position: 'bottom'});
- } else {
- // apply all changes before sorting
- element = this._applyX2ManyOperations(element, {position: 'bottom'});
- }
+ // apply potential changes (only for x2many lists)
+ element = this._applyX2ManyOperations(element);
this._sortList(element);
- if (element._keepChangesUnsorted) {
- // apply the remaining changes after the sort
- element = this._applyX2ManyOperations(element, {from: count, position: 'bottom'});
- }
- if (element._changes) {
+
+ if (!element.orderedResIDs && element._changes) {
_.each(element._changes, function (change) {
if (change.operation === 'ADD' && change.isNew) {
element.data = _.without(element.data, change.id);
@@ -444,6 +430,7 @@ var BasicModel = AbstractModel.extend({
}
});
}
+
var list = {
aggregateValues: _.extend({}, element.aggregateValues),
context: _.extend({}, element.context),
@@ -921,6 +908,8 @@ var BasicModel = AbstractModel.extend({
// Erase changes as they have been applied
record._changes = {};
+ self.unfreezeOrder(record.id);
+
// Update the data directly or reload them
if (shouldReload) {
self._fetchRecord(record).then(function (record) {
@@ -953,6 +942,20 @@ var BasicModel = AbstractModel.extend({
record.fieldsInfo = _.extend({}, record.fieldsInfo, viewInfo.fieldsInfo);
},
/**
+ * For list resources, this freezes the current records order.
+ *
+ * @param {string} listID a valid element ID of type list
+ */
+ freezeOrder: function (listID) {
+ var list = this.localData[listID];
+ if (list.type === 'record') {
+ return;
+ }
+ list = this._applyX2ManyOperations(list);
+ this._sortList(list);
+ this.localData[listID].orderedResIDs = list.res_ids;
+ },
+ /**
* Manually sets a resource as dirty. This is used to notify that a field
* has been modified, but with an invalid value. In that case, the value is
* not sent to the basic model, but the record should still be flagged as
@@ -968,7 +971,7 @@ var BasicModel = AbstractModel.extend({
*
* @param {string} list_id id for the list resource
* @param {string} fieldName valid field name
- * @returns {this} so we can chain call this method
+ * @returns {Deferred}
*/
setSort: function (list_id, fieldName) {
var list = this.localData[list_id];
@@ -979,21 +982,26 @@ var BasicModel = AbstractModel.extend({
delete change.isNew;
});
}
- // the user manually selected a sort order, so we sort the list with all
- // the changes he made so far, even if it is editable, however, changes
- // he'll made after won't be sorted if the list is editable
- list._keepChangesUnsortedCount = list._changes ? list._changes.length : 0;
if (list.orderedBy.length === 0) {
list.orderedBy.push({name: fieldName, asc: true});
} else if (list.orderedBy[0].name === fieldName){
- list.orderedBy[0].asc = !list.orderedBy[0].asc;
+ if (!list.orderedResIDs) {
+ list.orderedBy[0].asc = !list.orderedBy[0].asc;
+ }
} else {
var orderedBy = _.reject(list.orderedBy, function (o) {
return o.name === fieldName;
});
list.orderedBy = [{name: fieldName, asc: true}].concat(orderedBy);
}
- return this;
+
+ list.orderedResIDs = null;
+ if (list.static) {
+ // sorting might require to fetch the field for records where the
+ // sort field is still unknown (i.e. on other pages for example)
+ return this._fetchUngroupedList(list);
+ }
+ return $.when();
},
/**
* Toggle the active value of given records (to archive/unarchive them)
@@ -1048,6 +1056,34 @@ var BasicModel = AbstractModel.extend({
});
}
},
+ /**
+ * For a list datapoint, unfreezes the current records order and sorts it.
+ * For a record datapoint, unfreezes the x2many list datapoints.
+ *
+ * @param {string} elementID a valid element ID
+ */
+ unfreezeOrder: function (elementID) {
+ var list = this.localData[elementID];
+ if (list.type === 'record') {
+ var data = _.extend({}, list.data, list._changes);
+ for (var fieldName in data) {
+ var field = list.fields[fieldName];
+ if (!field || !data[fieldName]) {
+ continue;
+ }
+ if (field.type === 'one2many' || field.type === 'many2many') {
+ var recordlist = this.localData[data[fieldName]];
+ recordlist.orderedResIDs = null;
+ for (var index in recordlist.data) {
+ this.unfreezeOrder(recordlist.data[index]);
+ }
+ }
+ }
+ return;
+ }
+ list.orderedResIDs = null;
+ this._sortList(list);
+ },
//--------------------------------------------------------------------------
// Private
@@ -1060,7 +1096,7 @@ var BasicModel = AbstractModel.extend({
* not by list or kanban controllers.
*
* @private
- * @param {string} listID a valid handle for a list object
+ * @param {Object} list a valid list object
* @param {Object} [options]
* @param {string} [options.position=top] if the new record should be added
* on top or on bottom of the list
@@ -1080,6 +1116,12 @@ var BasicModel = AbstractModel.extend({
list._changes.push({operation: 'ADD', id: id, position: position, isNew: true});
var record = self.localData[id];
list._cache[record.res_id] = id;
+ if (list.orderedResIDs) {
+ var index = list.offset + (position !== 'top' ? list.limit : 0);
+ list.orderedResIDs.splice(index, 0, record.res_id);
+ // list could be a copy of the original one
+ self.localData[list.id].orderedResIDs = list.orderedResIDs;
+ }
return id;
});
},
@@ -1341,7 +1383,6 @@ var BasicModel = AbstractModel.extend({
// TODO: before registering the changes, verify that the x2many
// value has changed
record._changes[name] = list.id;
- var shouldLoad = false;
list._changes = list._changes || [];
// save it in case of a [5] which will remove the _changes
@@ -1361,11 +1402,7 @@ var BasicModel = AbstractModel.extend({
}
if (command[0] === 1 && command[1]) {
// updating an existing record
- recID = _.find(list.data, function (childID) {
- var child = self.localData[childID];
- return child.res_id === command[1];
- });
- rec = self.localData[recID];
+ rec = self.localData[list._cache[command[1]]];
}
if (!rec) {
var params = {
@@ -1391,18 +1428,25 @@ var BasicModel = AbstractModel.extend({
defs.push(self._applyOnChange(command[2], rec));
} else if (command[0] === 4) {
// LINK TO
- // the dataPoint id will be set when the record will be fetched (for now,
- // this dataPoint doesn't exist yet)
- list._changes.push({operation: 'ADD', id: null, resID: command[1]});
- shouldLoad = true;
+ rec = self.localData[list._cache[command[1]]];
+ if (rec) {
+ // modifications done on a record are discarded if
+ // the onchange uses a LINK TO
+ self.discardChanges(rec.id);
+ }
+ // the dataPoint id will be set when the record will be
+ // fetched (for now, this dataPoint may not exist yet)
+ list._changes.push({
+ operation: 'ADD',
+ id: rec ? rec.id : null,
+ resID: command[1],
+ });
} else if (command[0] === 5) {
// DELETE ALL
list._changes = [{operation: 'REMOVE_ALL'}];
}
});
- if (shouldLoad) {
- defs.push(self._readUngroupedList(list));
- }
+ defs.push(self._readUngroupedList(list));
} else {
var newValue = self._parseServerValue(field, val);
if (newValue !== oldValue) {
@@ -1540,12 +1584,6 @@ var BasicModel = AbstractModel.extend({
if (idInCommands && change.operation === 'ADD') {
idsToRemove = _.without(idsToRemove, change.id);
}
- // decrement _keepChangesUnsortedCount if we filter out an
- // operation whose index is smaller than the count (as there
- // will be one operation less to apply before sorting)
- if (idInCommands && index < list._keepChangesUnsortedCount) {
- list._keepChangesUnsortedCount--;
- }
return idInCommands;
});
_.each(idsToRemove, function (id) {
@@ -1653,18 +1691,7 @@ var BasicModel = AbstractModel.extend({
break;
}
});
- var idsInRange;
- if (list.limit) {
- idsInRange = list.res_ids.slice(list.offset, list.offset + list.limit);
- } else {
- idsInRange = list.res_ids;
- }
- list.data = [];
- _.each(idsInRange, function (id) {
- if (list._cache[id]) {
- list.data.push(list._cache[id]);
- }
- });
+ this._setDataInRange(list);
return list;
},
/**
@@ -2344,7 +2371,6 @@ var BasicModel = AbstractModel.extend({
rawContext: rawContext,
relationField: field.relation_field,
viewType: view ? view.type : fieldInfo.viewType,
- _keepChangesUnsorted: fieldInfo.keepChangesUnsorted,
});
record.data[fieldName] = list.id;
if (!fieldInfo.__no_fetch) {
@@ -2624,6 +2650,7 @@ var BasicModel = AbstractModel.extend({
}
});
list = this._applyX2ManyOperations(list);
+ this._sortList(list);
if (type === 'many2many' || list._forceM2MLink) {
var relRecordCreated = _.filter(relRecordAdded, function (rec) {
return typeof rec.res_id === 'string';
@@ -2644,7 +2671,6 @@ var BasicModel = AbstractModel.extend({
_.each(relRecordUpdated, function (relRecord) {
var changes = self._generateChanges(relRecord, options);
if (!_.isEmpty(changes)) {
- delete changes.id;
var command = x2ManyCommands.update(relRecord.res_id, changes);
commands[fieldName].push(command);
}
@@ -2664,7 +2690,6 @@ var BasicModel = AbstractModel.extend({
relRecord = _.findWhere(relRecordUpdated, {res_id: list.res_ids[i]});
changes = relRecord ? this._generateChanges(relRecord, options) : {};
if (!_.isEmpty(changes)) {
- delete changes.id;
command = x2ManyCommands.update(relRecord.res_id, changes);
didChange = true;
} else {
@@ -2675,7 +2700,7 @@ var BasicModel = AbstractModel.extend({
// this is a new id (maybe existing in DB, but new in JS)
relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]});
changes = this._generateChanges(relRecord, options);
- if ('id' in changes) {
+ if (changes.id) {
// the subrecord already exists in db
delete changes.id;
if (this.isNew(record.id)) {
@@ -3065,7 +3090,7 @@ var BasicModel = AbstractModel.extend({
offset: params.offset || (type === 'record' ? _.indexOf(res_ids, res_id) : 0),
openGroupByDefault: params.openGroupByDefault,
orderedBy: params.orderedBy || [],
- _keepChangesUnsorted: params._keepChangesUnsorted,
+ orderedResIDs: params.orderedResIDs,
parentID: params.parentID,
rawContext: params.rawContext,
ref: params.ref || res_id,
@@ -3445,6 +3470,81 @@ var BasicModel = AbstractModel.extend({
});
},
/**
+ * Reads data from server for all missing fields.
+ *
+ * @private
+ * @param {Object} list a valid resource object
+ * @param {interger[]} resIDs
+ * @param {string[]} fieldNames to check and read if missing
+ * @returns {Deferred<Object>}
+ */
+ _readMissingFields: function (list, resIDs, fieldNames) {
+ var self = this;
+
+ var missingIDs = [];
+ for (var i = 0, len = resIDs.length; i < len; i++) {
+ var resId = resIDs[i];
+ var dataPointID = list._cache[resId];
+ if (!dataPointID) {
+ missingIDs.push(resId);
+ continue;
+ }
+ var record = self.localData[dataPointID];
+ var data = _.extend({}, record.data, record._changes);
+ if (_.difference(fieldNames, _.keys(data)).length) {
+ missingIDs.push(resId);
+ }
+ }
+
+ var def;
+ if (missingIDs.length && fieldNames.length) {
+ def = self._rpc({
+ model: list.model,
+ method: 'read',
+ args: [missingIDs, fieldNames],
+ context: list.getContext(),
+ });
+ } else {
+ def = $.when(_.map(missingIDs, function (id) {
+ return {id:id};
+ }));
+ }
+ return def.then(function (records) {
+ _.each(resIDs, function (id) {
+ var dataPoint;
+ var data = _.findWhere(records, {id: id});
+ if (id in list._cache) {
+ dataPoint = self.localData[list._cache[id]];
+ if (data) {
+ self._parseServerData(fieldNames, dataPoint, data);
+ _.extend(dataPoint.data, data);
+ }
+ } else {
+ dataPoint = self._makeDataPoint({
+ context: list.context,
+ data: data,
+ fieldsInfo: list.fieldsInfo,
+ fields: list.fields,
+ modelName: list.model,
+ parentID: list.id,
+ viewType: list.viewType,
+ });
+ self._parseServerData(fieldNames, dataPoint, dataPoint.data);
+
+ // add many2one records
+ list._cache[id] = dataPoint.id;
+ }
+ // set the dataPoint id in potential 'ADD' operation adding the current record
+ _.each(list._changes, function (change) {
+ if (change.operation === 'ADD' && !change.id && change.resID === id) {
+ change.id = dataPoint.id;
+ }
+ });
+ });
+ return list;
+ });
+ },
+ /**
* For a grouped list resource, this method fetches all group data by
* performing a /read_group. It also tries to read open subgroups if they
* were open before.
@@ -3503,6 +3603,7 @@ var BasicModel = AbstractModel.extend({
aggregateValues: aggregateValues,
groupedBy: list.groupedBy.slice(1),
orderedBy: list.orderedBy,
+ orderedResIDs: list.orderedResIDs,
limit: list.limit,
openGroupByDefault: list.openGroupByDefault,
parentID: list.id,
@@ -3555,70 +3656,44 @@ var BasicModel = AbstractModel.extend({
*/
_readUngroupedList: function (list) {
var self = this;
- var def;
- var ids = [];
- var missingIds = [];
+ var def = $.when();
+
// generate the current count and res_ids list by applying the changes
- var listWithChanges = this._applyX2ManyOperations(list);
- var currentCount = listWithChanges.count;
- var currentResIDs = listWithChanges.res_ids;
- var upper_bound = list.limit ? Math.min(list.offset + list.limit, currentCount) : currentCount;
- var fieldNames = list.getFieldNames();
- for (var i = list.offset; i < upper_bound; i++) {
- var id = currentResIDs[i];
- ids.push(id);
- if (!list._cache[id]) {
- missingIds.push(id);
- }
- }
- if (missingIds.length) {
- if (fieldNames.length) {
- def = this._rpc({
- model: list.model,
- method: 'read',
- args: [missingIds, fieldNames],
- context: list.getContext(),
+ list = this._applyX2ManyOperations(list);
+
+ // for multi-pages list datapoints, we might need to read the
+ // order field first to apply the order on all pages
+ if (list.res_ids.length > list.limit) {
+ if (!list.orderedResIDs && list.orderedBy.length) {
+ var fieldNames = _.pluck(list.orderedBy, 'name');
+ def = this._readMissingFields(list, _.filter(list.res_ids, _.isNumber), fieldNames);
+ def.then(function () {
+ self._sortList(list);
});
- } else {
- def = $.when(_.map(missingIds, function (id) {
- return {id:id};
- }));
}
- } else {
- def = $.when();
}
- return def.then(function (records) {
- list.data = [];
- _.each(ids, function (id) {
- var dataPoint;
- if (id in list._cache) {
- dataPoint = self.localData[list._cache[id]];
- } else {
- dataPoint = self._makeDataPoint({
- context: list.context,
- data: _.findWhere(records, {id: id}),
- fieldsInfo: list.fieldsInfo,
- fields: list.fields,
- modelName: list.model,
- parentID: list.id,
- viewType: list.viewType,
- });
-
- // add many2one records
- self._parseServerData(fieldNames, dataPoint, dataPoint.data);
- list._cache[id] = dataPoint.id;
+ return def.then(function () {
+ var resIDs = [];
+ // generate the current count and res_ids list by applying the changes
+ var currentCount = list.count;
+ var currentResIDs = list.res_ids;
+ var upperBound = list.limit ? Math.min(list.offset + list.limit, currentCount) : currentCount;
+ var fieldNames = list.getFieldNames();
+ for (var i = list.offset; i < upperBound; i++) {
+ var resId = currentResIDs[i];
+ if (_.isNumber(resId)) {
+ resIDs.push(resId);
}
- // set the dataPoint id in potential 'ADD' operation adding the current record
- _.each(list._changes, function (change) {
- if (change.operation === 'ADD' && !change.id && change.resID === id) {
- change.id = dataPoint.id;
- }
- });
- if (_.contains(list.res_ids, id)) {
- list.data.push(dataPoint.id);
+ }
+ return self._readMissingFields(list, resIDs, fieldNames).then(function () {
+ if (list.res_ids.length <= list.limit) {
+ self._sortList(list);
+ } else {
+ // sortList has already been applied after first the read
+ self._setDataInRange(list);
}
+ return list;
});
- return list;
});
},
/**
@@ -3690,6 +3765,38 @@ var BasicModel = AbstractModel.extend({
});
},
/**
+ * Set data in range, i.e. according to the list offset and limit.
+ *
+ * @param {Object} list
+ */
+ _setDataInRange: function (list) {
+ var idsInRange;
+ if (list.limit) {
+ idsInRange = list.res_ids.slice(list.offset, list.offset + list.limit);
+ } else {
+ idsInRange = list.res_ids;
+ }
+ list.data = [];
+ _.each(idsInRange, function (id) {
+ if (list._cache[id]) {
+ list.data.push(list._cache[id]);
+ }
+ });
+
+ // display newly created record in addition to the displayed records
+ if (list.limit) {
+ for (var i = list.offset + list.limit; i < list.res_ids.length; i++) {
+ var id = list.res_ids[i];
+ var dataPointID = list._cache[id];
+ if (_.findWhere(list._changes, {isNew: true, id: dataPointID})) {
+ list.data.push(dataPointID);
+ } else {
+ break;
+ }
+ }
+ }
+ },
+ /**
* Change the offset of a record. Note that this does not reload the data.
* The offset is used to load a different record in a list of record (for
* example, a form view with a pager. Clicking on next/previous actually
@@ -3720,14 +3827,28 @@ var BasicModel = AbstractModel.extend({
// only sort x2many lists
return;
}
+ var self = this;
- if (list.orderedBy.length) {
- var self = this;
-
+ if (list.orderedResIDs) {
+ var orderedResIDs = {};
+ for (var k = 0; k < list.orderedResIDs.length; k++) {
+ orderedResIDs[list.orderedResIDs[k]] = k;
+ }
+ utils.stableSort(list.res_ids, function compareResIdIndexes (resId1, resId2) {
+ if (!(resId1 in orderedResIDs) && !(resId2 in orderedResIDs)) {
+ return 0;
+ }
+ if (!(resId1 in orderedResIDs)) {
+ return Infinity;
+ }
+ if (!(resId2 in orderedResIDs)) {
+ return -Infinity;
+ }
+ return orderedResIDs[resId1] - orderedResIDs[resId2];
+ });
+ } else if (list.orderedBy.length) {
// sort records according to ordered_by[0]
- var data = list.data;
- var res_ids = list.res_ids;
- var compareRecords = function (record1ID, record2ID, level) {
+ var compareRecords = function (resId1, resId2, level) {
if(!level) {
level = 0;
}
@@ -3735,6 +3856,17 @@ var BasicModel = AbstractModel.extend({
return 0;
}
var order = list.orderedBy[level];
+ var record1ID = list._cache[resId1];
+ var record2ID = list._cache[resId2];
+ if (!record1ID && !record2ID) {
+ return 0;
+ }
+ if (!record1ID) {
+ return Infinity;
+ }
+ if (!record2ID) {
+ return -Infinity;
+ }
var r1 = self.localData[record1ID];
var r2 = self.localData[record2ID];
var data1 = _.extend({}, r1.data, r1._changes);
@@ -3747,17 +3879,9 @@ var BasicModel = AbstractModel.extend({
}
return compareRecords(record1ID, record2ID, level + 1);
};
- data.sort(compareRecords);
-
- // sort res_ids accordingly (only the current range of ids, the one
- // mapping the data, needs to be sorted)
- var preRangeIDs = res_ids.slice(0, list.offset); // resIDs before the range
- var postRangeIDs = res_ids.slice(list.offset + list.limit); // resIDs after the range
- var rangeIDs = _.map(data, function (dataPointID) {
- return self.localData[dataPointID].res_id;
- });
- list.res_ids = preRangeIDs.concat(rangeIDs).concat(postRangeIDs);
+ utils.stableSort(list.res_ids, compareRecords);
}
+ this._setDataInRange(list);
},
/**
* Updates the res_ids of the parent of a given element of type list.
diff --git a/addons/web/static/src/js/views/field_manager_mixin.js b/addons/web/static/src/js/views/field_manager_mixin.js
index 2909308cf29..0bc1b5acfce 100644
--- a/addons/web/static/src/js/views/field_manager_mixin.js
+++ b/addons/web/static/src/js/views/field_manager_mixin.js
@@ -53,7 +53,7 @@ var FieldManagerMixin = {
return self.model.save(dataPointID).then(function () {
return self._confirmSave(dataPointID);
});
- } else {
+ } else if (options.notifyChange !== false) {
return self._confirmChange(dataPointID, result, event);
}
});
diff --git a/addons/web/static/src/js/views/form/form_controller.js b/addons/web/static/src/js/views/form/form_controller.js
index 05c57c435fc..2820dad448f 100644
--- a/addons/web/static/src/js/views/form/form_controller.js
+++ b/addons/web/static/src/js/views/form/form_controller.js
@@ -12,9 +12,10 @@ var qweb = core.qweb;
var FormController = BasicController.extend({
custom_events: _.extend({}, BasicController.prototype.custom_events, {
- open_one2many_record: '_onOpenOne2ManyRecord',
bounce_edit: '_onBounceEdit',
button_clicked: '_onButtonClicked',
+ freeze_order: '_onFreezeOrder',
+ open_one2many_record: '_onOpenOne2ManyRecord',
open_record: '_onOpenRecord',
toggle_column_order: '_onToggleColumnOrder',
}),
@@ -299,6 +300,17 @@ var FormController = BasicController.extend({
this._super(state);
},
/**
+ * Calls unfreezeOrder when changing the mode.
+ *
+ * @override
+ */
+ _setMode: function (mode, recordID) {
+ if ((recordID || this.handle) === this.handle) {
+ this.model.unfreezeOrder(this.handle);
+ }
+ return this._super.apply(this, arguments);
+ },
+ /**
* Updates the controller's title according to the new state
*
* @override
@@ -445,6 +457,17 @@ var FormController = BasicController.extend({
this._setMode('edit');
},
/**
+ * This method is called when someone tries to freeze the order, most likely
+ * in a x2many list view
+ *
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onFreezeOrder: function (event) {
+ event.stopPropagation();
+ this.model.freezeOrder(event.data.id);
+ },
+ /**
* Opens a one2many record (potentially new) in a dialog. This handler is
* o2m specific as in this case, the changes done on the related record
* shouldn't be saved in DB when the user clicks on 'Save' in the dialog,
@@ -518,10 +541,12 @@ var FormController = BasicController.extend({
*/
_onToggleColumnOrder: function (event) {
event.stopPropagation();
- this.model.setSort(event.data.id, event.data.name);
- var field = event.data.field;
- var state = this.model.get(this.handle);
- this.renderer.confirmChange(state, state.id, [field]);
+ var self = this;
+ this.model.setSort(event.data.id, event.data.name).then(function () {
+ var field = event.data.field;
+ var state = self.model.get(self.handle);
+ self.renderer.confirmChange(state, state.id, [field]);
+ });
},
});
diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js
index 5c6b7ea414d..982d5456bca 100644
--- a/addons/web/static/src/js/views/list/list_controller.js
+++ b/addons/web/static/src/js/views/list/list_controller.js
@@ -485,8 +485,10 @@ var ListController = BasicController.extend({
if (!data.groupedBy) {
this.pager.updateState({current_min: 1});
}
- this.model.setSort(data.id, event.data.name);
- this.update({});
+ var self = this;
+ this.model.setSort(data.id, event.data.name).then(function () {
+ self.update({});
+ });
},
/**
* In a grouped list view, each group can be clicked on to open/close them.
diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 6b9e4aebe56..f4c5219259a 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -276,6 +276,7 @@ var ListRenderer = BasicRenderer.extend({
}
if (node.attrs.widget || (options && options.renderWidgets)) {
var $el = this._renderFieldWidget(node, record, _.pick(options, 'mode'));
+ this._handleAttributes($el, node);
return $td.append($el);
}
var name = node.attrs.name;
@@ -286,6 +287,7 @@ var ListRenderer = BasicRenderer.extend({
escape: true,
isPassword: 'password' in node.attrs,
});
+ this._handleAttributes($td, node);
return $td.html(formattedValue);
},
/**
diff --git a/addons/web/static/src/js/widgets/debug_manager.js b/addons/web/static/src/js/widgets/debug_manager.js
index 4160d6f554b..2c9cf3d4457 100644
--- a/addons/web/static/src/js/widgets/debug_manager.js
+++ b/addons/web/static/src/js/widgets/debug_manager.js
@@ -806,7 +806,7 @@ if (config.debug) {
// Instantiate the DebugManager and insert it into the DOM once dialog is opened
this.opened(function() {
self.debug_manager = new DebugManager(self);
- var $header = self.$modal.find('.modal-header');
+ var $header = self.$modal.find('.modal-header:first');
return self.debug_manager.prependTo($header).then(function() {
self.debug_manager.update('action', parent.dialog_widget.action, parent.dialog_widget);
});
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 6ecc29652a7..8ee6f643c51 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -265,8 +265,19 @@
</t>
<li><a href="#" data-action="fvg">Fields View Get</a></li>
<t t-if="can_edit">
- <li><a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="view.fields_view.view_id">Edit <t t-esc="_.str.capitalize(view.type)"/>View</a></li>
- <li t-if="searchview and searchview.$el.is(':visible')"><a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="searchview.fields_view.view_id">Edit SearchView</a></li>
+ <li>
+ <a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="view.fields_view.view_id">
+ <t t-if="view.type == 'form'">Edit Form view</t>
+ <t t-elif="view.type == 'list'">Edit List view</t>
+ <t t-elif="view.type == 'kanban'">Edit Kanban view</t>
+ <t t-elif="view.type == 'graph'">Edit Graph view</t>
+ <t t-elif="view.type == 'gantt'">Edit Gantt view</t>
+ <t t-elif="view.type == 'pivot'">Edit Pivot view</t>
+ <t t-elif="view.type == 'calendar'">Edit Calendar view</t>
+ <t t-else="1">Edit view, type: <t t-esc="_.str.capitalize(view.type)"/></t>
+ </a>
+ </li>
+ <li t-if="searchview and searchview.$el.is(':visible')"><a href="#" data-action="edit" data-model="ir.ui.view" t-att-data-id="searchview.fields_view.view_id">Edit Search view</a></li>
</t>
</t>
</t>
@@ -786,7 +797,7 @@
<t t-foreach="elements" t-as="el">
<t t-set="color" t-value="el[colorField] || 0"/>
<span t-attf-class="badge dropdown o_tag_color_#{color}" t-att-data-color="color" t-att-data-index="el_index" t-att-data-id="el.id">
- <span class="o_badge_text"><t t-raw="el.display_name"/></span>
+ <span class="o_badge_text"><t t-esc="el.display_name"/></span>
<span t-if="!readonly" class="fa fa-times o_delete"/>
</span>
</t>
@@ -895,6 +906,9 @@
</t>
</div>
</t>
+<t t-name="FieldBinaryDownloadLink">
+ <a t-att-download="widget.filename" t-att-href="widget.url"><t t-esc="widget.text"/></a>
+</t>
<t t-name="HiddenInputFile">
<div t-attf-class="o_hidden_input_file #{fileupload_class or ''}" t-att-style="fileupload_style">
<form class="o_form_binary_form" t-att-target="fileupload_id"