diff options
Diffstat (limited to 'addons/web/static/src/js/views/basic')
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; |
