summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/basic
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static/src/js/views/basic')
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js65
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js209
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js26
3 files changed, 179 insertions, 121 deletions
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index b7cd40d9e7c..1e0187b348e 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -19,7 +19,8 @@ var _t = core._t;
var BasicController = AbstractController.extend(FieldManagerMixin, {
custom_events: _.extend({}, AbstractController.prototype.custom_events, FieldManagerMixin.custom_events, {
reload: '_onReload',
- sidebar_data_asked: '_onSidebarDataAsked'
+ sidebar_data_asked: '_onSidebarDataAsked',
+ translate: '_onTranslate',
}),
/**
* @override
@@ -118,13 +119,18 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
var self = this;
recordID = recordID || this.handle;
return this.canBeDiscarded(recordID).then(function (needDiscard) {
+ if (options && options.readonlyIfRealDiscard && !needDiscard) {
+ return;
+ }
+
if (needDiscard) { // Just some optimization
self.model.discardChanges(recordID);
}
- if (options && options.readonlyIfRealDiscard && !needDiscard) {
+ if (self.model.isNew(recordID)) {
+ self._abandonRecord(recordID);
return;
}
- self._setMode('readonly', recordID);
+ return self._confirmSave(recordID);
});
},
/**
@@ -164,6 +170,8 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @param {string} [recordID] - default to main recordID
* @param {Object} [options]
* @returns {Deferred}
+ * Resolved with the list of field names (whose value has been modified)
+ * Rejected if the record can't be saved
*/
saveRecord: function (recordID, options) {
// Some field widgets can't detect (all) their changes immediately or
@@ -286,7 +294,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
var self = this;
function doIt() {
return self.model
- .deleteRecords(ids, self.modelName, self.handle)
+ .deleteRecords(ids, self.modelName)
.then(self._onDeletedRecords.bind(self, ids));
}
if (this.confirmOnDelete) {
@@ -362,6 +370,8 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* if true, the record will only be 'locally' saved: its changes
* will move from the _changes key to the data key
* @returns {Deferred}
+ * Resolved with the list of field names (whose value has been modified)
+ * Rejected if the record can't be saved
*/
_saveRecord: function (recordID, options) {
recordID = recordID || this.handle;
@@ -371,22 +381,24 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
savePoint: false,
});
- var def = $.Deferred();
// Check if the view is in a valid state for saving
// Note: it is the model's job to do nothing if there is nothing to save
if (this.canBeSaved(recordID)) {
- def = this.model.save(recordID, { // Save then leave edit mode
+ var self = this;
+ var def = this.model.save(recordID, { // Save then leave edit mode
reload: options.reload,
savePoint: options.savePoint,
});
- } else {
- def.reject(); // Cannot be saved, do nothing at all
- }
-
- if (options.stayInEdit) {
+ if (!options.stayInEdit) {
+ def = def.then(function (fieldNames) {
+ return self._confirmSave(recordID).then(function () {
+ return fieldNames;
+ });
+ });
+ }
return def;
} else {
- return def.then(this._setMode.bind(this, 'readonly', recordID));
+ return $.Deferred().reject(); // Cannot be saved
}
},
/**
@@ -397,17 +409,13 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
* @private
* @param {string} mode - 'readonly' or 'edit'
* @param {string} [recordID]
+ * @returns {Deferred}
*/
_setMode: function (mode, recordID) {
- recordID = recordID || this.handle;
- // If trying to make a temporary record readonly, discard the record
- if (mode === 'readonly' && this.model.isNew(recordID)) {
- this._abandonRecord(recordID);
- return;
- }
- if (recordID === this.handle) {
- this.update({mode: mode}, {reload: false});
+ if ((recordID || this.handle) === this.handle) {
+ return this.update({mode: mode}, {reload: false});
}
+ return $.when();
},
/**
* Helper method, to get the current environment variables from the model
@@ -497,6 +505,23 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
var sidebarEnv = this._getSidebarEnv();
event.data.callback(sidebarEnv);
},
+ /**
+ * open the translation view for the current field
+ *
+ * @private
+ * @param {OdooEvent} event
+ */
+ _onTranslate: function (event) {
+ var record = this.model.get(event.data.id, {raw: true});
+ this._rpc({
+ route: '/web/dataset/call_button',
+ params: {
+ model: 'ir.translation',
+ method: 'translate_fields',
+ args: [record.model, record.res_id, event.data.fieldName, record.getContext()],
+ }
+ }).then(this.do_action.bind(this));
+ },
});
return BasicController;
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 17cb4d277c2..5e5b36472f5 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -180,15 +180,16 @@ var BasicModel = AbstractModel.extend({
});
},
/**
- * Delete a list of records, then, if a parentID is given, reload the
- * parent.
+ * Delete a list of records, then, if the records have a parent, reload it.
*
* @todo we should remove the deleted records from the localData
- * @todo why can't we infer modelName?
+ * @todo why can't we infer modelName? Because of grouped datapoint
+ * --> res_id doesn't correspond to the model and we don't have the
+ * information about the related model
*
* @param {string[]} recordIds list of local resources ids. They should all
- * be of type 'record', and of the same model
- * @param {string} modelName
+ * be of type 'record', be of the same model and have the same parent.
+ * @param {string} modelName mode name used to unlink the records
* @returns {Deferred}
*/
deleteRecords: function (recordIds, modelName) {
@@ -202,9 +203,15 @@ var BasicModel = AbstractModel.extend({
})
.then(function () {
_.each(records, function (record) {
- record.res_ids.splice(record.offset, 1);
- record.res_id = record.res_ids[record.offset];
- record.count--;
+ var parent = record.parentID && self.localData[record.parentID];
+ if (parent && parent.type === 'list') {
+ parent.data = _.without(parent.data, record.id);
+ delete self.localData[record.id];
+ } else {
+ record.res_ids.splice(record.offset, 1);
+ record.res_id = record.res_ids[record.offset];
+ record.count--;
+ }
});
});
},
@@ -289,35 +296,27 @@ var BasicModel = AbstractModel.extend({
return null;
}
- var record = this.localData[id];
+ var element = this.localData[id];
if (options.env) {
var env = {
- ids: record.res_ids ? record.res_ids.slice(0) : [],
+ ids: element.res_ids ? element.res_ids.slice(0) : [],
};
- if (record.type === 'record') {
- env.currentId = this.isNew(record.id) ? undefined : record.res_id;
+ if (element.type === 'record') {
+ env.currentId = this.isNew(element.id) ? undefined : element.res_id;
}
return env;
}
- // do not copy fields: this has a really big performance cost, for views
- // with many records and lots of fields (for ex, kanban view contacts)
- var fields = record.fields;
- delete record.fields;
- var element = $.extend(true, {}, record);
- record.fields = fields;
- element.fields = fields;
- var field, relDataPoint;
if (element.type === 'record') {
- // apply changes
- _.extend(element.data, element._changes);
- for (var fieldName in element.data) {
- field = element.fields[fieldName];
- if (element.data[fieldName] === null) {
- element.data[fieldName] = false;
+ var data = _.extend({}, element.data, element._changes);
+ var relDataPoint;
+ for (var fieldName in data) {
+ var field = element.fields[fieldName];
+ if (data[fieldName] === null) {
+ data[fieldName] = false;
}
if (!field) {
continue;
@@ -326,72 +325,110 @@ var BasicModel = AbstractModel.extend({
if (field.type === 'float' ||
field.type === 'integer' ||
field.type === 'monetary') {
- element.data[fieldName] = element.data[fieldName] || 0;
+ data[fieldName] = data[fieldName] || 0;
}
}
// get relational datapoint
if (field.type === 'many2one') {
if (options.raw) {
- relDataPoint = this.localData[element.data[fieldName]];
- element.data[fieldName] = relDataPoint ? relDataPoint.res_id : false;
+ relDataPoint = this.localData[data[fieldName]];
+ data[fieldName] = relDataPoint ? relDataPoint.res_id : false;
} else {
- element.data[fieldName] = this.get(element.data[fieldName]) || false;
+ data[fieldName] = this.get(data[fieldName]) || false;
}
}
if (field.type === 'one2many' || field.type === 'many2many') {
if (options.raw) {
- relDataPoint = this.localData[element.data[fieldName]];
- var ids = _.map(relDataPoint.data, function (id) {
+ relDataPoint = this.localData[data[fieldName]];
+ var relData = relDataPoint._changes || relDataPoint.data;
+ var ids = _.map(relData, function (id) {
return self.localData[id].res_id;
});
- element.data[fieldName] = ids;
+ data[fieldName] = ids;
} else {
- element.data[fieldName] = this.get(element.data[fieldName]) || [];
+ data[fieldName] = this.get(data[fieldName]) || [];
}
}
}
+ var record = {
+ context: _.extend({}, element.context),
+ count: element.count,
+ data: data,
+ domain: element.domain.slice(0),
+ fields: element.fields,
+ fieldsInfo: element.fieldsInfo,
+ getContext: element.getContext,
+ getDomain: element.getDomain,
+ getEvalContext: element.getEvalContext,
+ getFieldNames: element.getFieldNames,
+ id: element.id,
+ limit: element.limit,
+ model: element.model,
+ offset: element.offset,
+ res_ids: element.res_ids.slice(0),
+ specialData: _.extend({}, element.specialData),
+ type: 'record',
+ viewType: element.viewType,
+ };
- // this is not strictly necessary, but it hides some implementation
- // details, and can easily be removed if needed.
- delete element.orderedBy;
- delete element.aggregateValues;
- delete element.groupedBy;
- if (this.isNew(element.id)) {
- delete element.res_id;
+ if (!this.isNew(element.id)) {
+ record.res_id = element.res_id;
}
var evalContext;
- Object.defineProperty(element, 'evalContext', {
+ Object.defineProperty(record, 'evalContext', {
get: function () {
- evalContext = evalContext || self._getEvalContext(record);
+ evalContext = evalContext || self._getEvalContext(element);
return evalContext;
},
});
+ return record;
}
- if (element.type === 'list') {
- // apply changes if any
- if (element._changes) {
- element.data = element._changes;
- element.count = element._changes.length;
- element.res_ids = _.map(element._changes, function (elemID) {
- return self.localData[elemID].res_id;
- });
- }
- // get relational datapoint
- element.data = _.map(element.data, function (elemID) {
- return self.get(elemID);
+
+ // here, type === 'list'
+ var listData, listCount, resIDs;
+ if (element._changes) {
+ listData = element._changes;
+ listCount = listData.length;
+ resIDs = _.map(listData, function (elemID) {
+ return self.localData[elemID].res_id;
});
+ } else {
+ listData = element.data;
+ listCount = element.count;
+ resIDs = element.res_ids;
}
-
- delete element._cache;
- delete element._changes;
- delete element._forceM2MLink;
- delete element.static;
- delete element.parentID;
- delete element.relation_field;
- delete element.rawContext;
-
- return element;
+ listData = _.map(listData, function (elemID) {
+ return self.get(elemID, options);
+ });
+ var list = {
+ aggregateValues: _.extend({}, element.aggregateValues),
+ context: _.extend({}, element.context),
+ count: listCount,
+ data: listData,
+ domain: element.domain.slice(0),
+ fields: element.fields,
+ getContext: element.getContext,
+ getDomain: element.getDomain,
+ getEvalContext: element.getEvalContext,
+ getFieldNames: element.getFieldNames,
+ groupedBy: element.groupedBy,
+ id: element.id,
+ isOpen: element.isOpen,
+ limit: element.limit,
+ model: element.model,
+ offset: element.offset,
+ orderedBy: element.orderedBy,
+ res_id: element.res_id,
+ res_ids: resIDs,
+ type: 'list',
+ value: element.value,
+ viewType: element.viewType,
+ };
+ if (element.fieldsInfo) {
+ list.fieldsInfo = element.fieldsInfo;
+ }
+ return list;
},
/**
* Returns true if a record is dirty. A record is considered dirty if it has
@@ -678,6 +715,7 @@ var BasicModel = AbstractModel.extend({
* be 'locally' saved: its changes written in a _savePoint key that can
* be restored later by call discardChanges with option rollback to true
* @returns {Deferred}
+ * Resolved with the list of field names (whose value has been modified)
*/
save: function (record_id, options) {
var self = this;
@@ -693,7 +731,6 @@ var BasicModel = AbstractModel.extend({
rec._savePoint = _.extend({}, newValue);
}
});
- return $.when();
}
var shouldReload = 'reload' in options ? options.reload : true;
var method = self.isNew(record_id) ? 'create' : 'write';
@@ -712,10 +749,17 @@ var BasicModel = AbstractModel.extend({
});
}
+ var def = $.Deferred();
+ var changedFields = Object.keys(changes);
+
+ if (options.savePoint) {
+ return def.resolve(changedFields);
+ }
+
// in the case of a write, only perform the RPC if there are changes to save
- if (method === 'create' || Object.keys(changes).length) {
+ if (method === 'create' || changedFields.length) {
var args = method === 'write' ? [[record.data.id], changes] : [changes];
- return self._rpc({
+ self._rpc({
model: record.model,
method: method,
args: args,
@@ -729,23 +773,26 @@ var BasicModel = AbstractModel.extend({
record.count++;
}
- // Update the data directly or reload them
- var def;
- if (shouldReload) {
- def = self._fetchRecord(record);
- } else {
- _.extend(record.data, record._changes);
- }
+ var _changes = record._changes;
// Erase changes as they have been applied
record._changes = {};
record._isDirty = false;
- return def;
+ // Update the data directly or reload them
+ if (shouldReload) {
+ self._fetchRecord(record).then(function (record) {
+ def.resolve(changedFields);
+ });
+ } else {
+ _.extend(record.data, _changes);
+ def.resolve(changedFields);
+ }
});
} else {
- return $.when(record_id);
+ def.resolve(changedFields);
}
+ return def;
});
},
/**
@@ -2038,7 +2085,7 @@ var BasicModel = AbstractModel.extend({
return true;
case 'one2many':
case 'many2many':
- return value.count > 0;
+ return value.length > 0;
default:
return value !== false;
}
@@ -2267,13 +2314,13 @@ var BasicModel = AbstractModel.extend({
});
x2manyList._changes = x2manyList._changes || [];
x2manyList._changes.push(r.id);
- r._changes = value[2];
// this is necessary so the fields are initialized
- for (var fieldName in value[2]) {
+ _.each(r.getFieldNames(), function (fieldName) {
r.data[fieldName] = null;
- }
+ });
+ r._changes = _.defaults(value[2], r.data);
for (var name in r._changes) {
var isFieldInView = name in r.fields;
if (isFieldInView && r.fields[name].type === 'many2one') {
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index 65f444ad7b3..907ea7ddc07 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -16,12 +16,7 @@ var qweb = core.qweb;
var BasicRenderer = AbstractRenderer.extend({
custom_events: {
- /**
- * All basic renderers should be able to handle widget navigation
- * through the TAB key.
- */
- move_next: '_onMoveNext',
- move_previous: '_onMovePrevious',
+ navigation_move: '_onNavigationMove',
},
/**
* Basic renderers implements the concept of "mode", they can either be in
@@ -169,7 +164,7 @@ var BasicRenderer = AbstractRenderer.extend({
var recordWidgets = this.allFieldWidgets[record.id] || [];
for (var i = 0 ; i < recordWidgets.length ; i++) {
- var activated = recordWidgets[currentIndex].activate();
+ var activated = recordWidgets[currentIndex].activate({event: options.event});
if (activated) {
return currentIndex;
}
@@ -600,24 +595,15 @@ var BasicRenderer = AbstractRenderer.extend({
//--------------------------------------------------------------------------
/**
- * When someone presses TAB in a widget, it is nice to be able to go to the
- * next widget, especially since the TAB key event is preventDefaulted
+ * When someone presses the TAB/UP/DOWN/... key in a widget, it is nice to
+ * be able to navigate in the view (default browser behaviors are disabled
+ * by Odoo).
*
* @abstract
* @private
* @param {OdooEvent} ev
*/
- _onMoveNext: function (ev) {},
- /**
- * When someone presses SHIFT+TAB in a widget, it is nice to be able to go
- * back to the previous widget, especially since the TAB key event is
- * preventDefaulted
- *
- * @abstract
- * @private
- * @param {OdooEvent} ev
- */
- _onMovePrevious: function (ev) {},
+ _onNavigationMove: function (ev) {},
});
return BasicRenderer;