From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- .../static/src/js/views/basic/basic_controller.js | 883 +++++++++++++++++++++ 1 file changed, 883 insertions(+) create mode 100644 addons/web/static/src/js/views/basic/basic_controller.js (limited to 'addons/web/static/src/js/views/basic/basic_controller.js') diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js new file mode 100644 index 00000000..4cbe9027 --- /dev/null +++ b/addons/web/static/src/js/views/basic/basic_controller.js @@ -0,0 +1,883 @@ +odoo.define('web.BasicController', function (require) { +"use strict"; + +/** + * The BasicController is mostly here to share code between views that will use + * a BasicModel (or a subclass). Currently, the BasicViews are the form, list + * and kanban views. + */ + +var AbstractController = require('web.AbstractController'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var FieldManagerMixin = require('web.FieldManagerMixin'); +var TranslationDialog = require('web.TranslationDialog'); + +var _t = core._t; + +var BasicController = AbstractController.extend(FieldManagerMixin, { + events: Object.assign({}, AbstractController.prototype.events, { + 'click .o_content': '_onContentClicked', + }), + custom_events: _.extend({}, AbstractController.prototype.custom_events, FieldManagerMixin.custom_events, { + discard_changes: '_onDiscardChanges', + pager_changed: '_onPagerChanged', + reload: '_onReload', + resequence_records: '_onResequenceRecords', + set_dirty: '_onSetDirty', + load_optional_fields: '_onLoadOptionalFields', + save_optional_fields: '_onSaveOptionalFields', + translate: '_onTranslate', + }), + /** + * @override + * @param {Object} params + * @param {boolean} params.archiveEnabled + * @param {boolean} params.confirmOnDelete + * @param {boolean} params.hasButtons + */ + init: function (parent, model, renderer, params) { + this._super.apply(this, arguments); + this.archiveEnabled = params.archiveEnabled; + this.confirmOnDelete = params.confirmOnDelete; + this.hasButtons = params.hasButtons; + FieldManagerMixin.init.call(this, this.model); + this.mode = params.mode || 'readonly'; + // savingDef is used to ensure that we always wait for pending save + // operations to complete before checking if there are changes to + // discard when discardChanges is called + this.savingDef = Promise.resolve(); + // discardingDef is used to ensure that we don't ask twice the user if + // he wants to discard changes, when 'canBeDiscarded' is called several + // times "in parallel" + this.discardingDef = null; + this.viewId = params.viewId; + }, + /** + * @override + * @returns {Promise} + */ + start: async function () { + // add classname to reflect the (absence of) access rights (used to + // correctly display the nocontent helper) + this.$el.toggleClass('o_cannot_create', !this.activeActions.create); + await this._super(...arguments); + }, + /** + * Called each time the controller is dettached into the DOM + */ + on_detach_callback() { + this._super.apply(this, arguments); + this.renderer.resetLocalState(); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Determines if we can discard the current changes. If the model is not + * dirty, that is not a problem. However, if it is dirty, we have to ask + * the user for confirmation. + * + * @override + * @param {string} [recordID] - default to main recordID + * @returns {Promise} + * resolved if can be discarded, a boolean value is given to tells + * if there is something to discard or not + * rejected otherwise + */ + canBeDiscarded: function (recordID) { + var self = this; + if (this.discardingDef) { + // discard dialog is already open + return this.discardingDef; + } + if (!this.isDirty(recordID)) { + return Promise.resolve(false); + } + + var message = _t("The record has been modified, your changes will be discarded. Do you want to proceed?"); + this.discardingDef = new Promise(function (resolve, reject) { + var dialog = Dialog.confirm(self, message, { + title: _t("Warning"), + confirm_callback: () => { + resolve(true); + self.discardingDef = null; + }, + cancel_callback: () => { + reject(); + self.discardingDef = null; + }, + }); + dialog.on('closed', self.discardingDef, reject); + }); + return this.discardingDef; + }, + /** + * Ask the renderer if all associated field widget are in a valid state for + * saving (valid value and non-empty value for required fields). If this is + * not the case, this notifies the user with a warning containing the names + * of the invalid fields. + * + * Note: changing the style of invalid fields is the renderer's job. + * + * @param {string} [recordID] - default to main recordID + * @return {boolean} + */ + canBeSaved: function (recordID) { + var fieldNames = this.renderer.canBeSaved(recordID || this.handle); + if (fieldNames.length) { + this._notifyInvalidFields(fieldNames); + return false; + } + return true; + }, + /** + * Waits for the mutex to be unlocked and for changes to be saved, then + * calls _.discardChanges. + * This ensures that the confirm dialog isn't displayed directly if there is + * a pending 'write' rpc. + * + * @see _.discardChanges + */ + discardChanges: function (recordID, options) { + return Promise.all([this.mutex.getUnlockedDef(), this.savingDef]) + .then(this._discardChanges.bind(this, recordID || this.handle, options)); + }, + /** + * Method that will be overridden by the views with the ability to have selected ids + * + * @returns {Array} + */ + getSelectedIds: function () { + return []; + }, + /** + * Returns true iff the given recordID (or the main recordID) is dirty. + * + * @param {string} [recordID] - default to main recordID + * @returns {boolean} + */ + isDirty: function (recordID) { + return this.model.isDirty(recordID || this.handle); + }, + /** + * Saves the record whose ID is given if necessary (@see _saveRecord). + * + * @param {string} [recordID] - default to main recordID + * @param {Object} [options] + * @returns {Promise} + * Resolved with the list of field names (whose value has been modified) + * Rejected if the record can't be saved + */ + saveRecord: function (recordID, options) { + var self = this; + // Some field widgets can't detect (all) their changes immediately or + // may have to validate them before notifying them, so we ask them to + // commit their current value before saving. This has to be done outside + // of the mutex protection of saving because commitChanges will trigger + // changes and these are also protected. However, we must wait for the + // mutex to be idle to ensure that onchange RPCs returned before asking + // field widgets to commit their value (and validate it, for instance + // for one2many with required fields). So the actual saving has to be + // done after these changes. Also the commitChanges operation might not + // be synchronous for other reason (e.g. the x2m fields will ask the + // user if some discarding has to be made). This operation must also be + // mutex-protected as commitChanges function of x2m has to be aware of + // all final changes made to a row. + var unlockedMutex = this.mutex.getUnlockedDef() + .then(function () { + return self.renderer.commitChanges(recordID || self.handle); + }) + .then(function () { + return self.mutex.exec(self._saveRecord.bind(self, recordID, options)); + }); + this.savingDef = new Promise(function (resolve) { + unlockedMutex.then(resolve).guardedCatch(resolve); + }); + + return unlockedMutex; + }, + /** + * @override + * @returns {Promise} + */ + update: async function (params, options) { + this.mode = params.mode || this.mode; + return this._super(params, options); + }, + /** + * @override + */ + reload: function (params) { + if (params && params.controllerState) { + if (params.controllerState.currentId) { + params.currentId = params.controllerState.currentId; + } + params.ids = params.controllerState.resIds; + } + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Does the necessary action when trying to "abandon" a given record (e.g. + * when trying to make a new record readonly without having saved it). By + * default, if the abandoned record is the main view one, the only possible + * action is to leave the current view. Otherwise, it is a x2m line, ask the + * model to remove it. + * + * @private + * @param {string} [recordID] - default to main recordID + */ + _abandonRecord: function (recordID) { + recordID = recordID || this.handle; + if (recordID === this.handle) { + this.trigger_up('history_back'); + } else { + this.model.removeLine(recordID); + } + }, + /** + * We override applyChanges (from the field manager mixin) to protect it + * with a mutex. + * + * @override + */ + _applyChanges: function (dataPointID, changes, event) { + var _super = FieldManagerMixin._applyChanges.bind(this); + return this.mutex.exec(function () { + return _super(dataPointID, changes, event); + }); + }, + /** + * Archive the current selection + * + * @private + * @param {number[]} ids + * @param {boolean} archive + * @returns {Promise} + */ + _archive: async function (ids, archive) { + if (ids.length === 0) { + return Promise.resolve(); + } + if (archive) { + await this.model.actionArchive(ids, this.handle); + } else { + await this.model.actionUnarchive(ids, this.handle); + } + return this.update({}, {reload: false}); + }, + /** + * When the user clicks on a 'action button', this function determines what + * should happen. + * + * @private + * @param {Object} attrs the attrs of the button clicked + * @param {Object} [record] the current state of the view + * @returns {Promise} + */ + _callButtonAction: function (attrs, record) { + record = record || this.model.get(this.handle); + const actionData = Object.assign({}, attrs, { + context: record.getContext({additionalContext: attrs.context || {}}) + }); + const recordData = { + context: record.getContext(), + currentID: record.data.id, + model: record.model, + resIDs: record.res_ids, + }; + return this._executeButtonAction(actionData, recordData); + }, + /** + * Called by the field manager mixin to confirm that a change just occured + * (after that potential onchanges have been applied). + * + * Basically, this only relays the notification to the renderer with the + * new state. + * + * @param {string} id - the id of one of the view's records + * @param {string[]} fields - the changed fields + * @param {OdooEvent} e - the event that triggered the change + * @returns {Promise} + */ + _confirmChange: function (id, fields, e) { + if (e.name === 'discard_changes' && e.target.reset) { + // the target of the discard event is a field widget. In that + // case, we simply want to reset the specific field widget, + // not the full view + return e.target.reset(this.model.get(e.target.dataPointID), e, true); + } + + var state = this.model.get(this.handle); + return this.renderer.confirmChange(state, id, fields, e); + }, + /** + * Ask the user to confirm he wants to save the record + * @private + */ + _confirmSaveNewRecord: function () { + var self = this; + var def = new Promise(function (resolve, reject) { + var message = _t("You need to save this new record before editing the translation. Do you want to proceed?"); + var dialog = Dialog.confirm(self, message, { + title: _t("Warning"), + confirm_callback: resolve.bind(self, true), + cancel_callback: reject, + }); + dialog.on('closed', self, reject); + }); + return def; + }, + /** + * Delete records (and ask for confirmation if necessary) + * + * @param {string[]} ids list of local record ids + */ + _deleteRecords: function (ids) { + var self = this; + function doIt() { + return self.model + .deleteRecords(ids, self.modelName) + .then(self._onDeletedRecords.bind(self, ids)); + } + if (this.confirmOnDelete) { + const message = ids.length > 1 ? + _t("Are you sure you want to delete these records?") : + _t("Are you sure you want to delete this record?"); + Dialog.confirm(this, message, { confirm_callback: doIt }); + } else { + doIt(); + } + }, + /** + * Disables buttons so that they can't be clicked anymore. + * + * @private + */ + _disableButtons: function () { + if (this.$buttons) { + this.$buttons.find('button').attr('disabled', true); + } + }, + /** + * Discards the changes made to the record whose ID is given, if necessary. + * Automatically leaves to default mode for the given record. + * + * @private + * @param {string} [recordID] - default to main recordID + * @param {Object} [options] + * @param {boolean} [options.readonlyIfRealDiscard=false] + * After discarding record changes, the usual option is to make the + * record readonly. However, the action manager calls this function + * at inappropriate times in the current code and in that case, we + * don't want to go back to readonly if there is nothing to discard + * (e.g. when switching record in edit mode in form view, we expect + * the new record to be in edit mode too, but the view manager calls + * this function as the URL changes...) @todo get rid of this when + * the webclient/action_manager's hashchange mechanism is improved. + * @param {boolean} [options.noAbandon=false] + * @returns {Promise} + */ + _discardChanges: function (recordID, options) { + var self = this; + recordID = recordID || this.handle; + options = options || {}; + return this.canBeDiscarded(recordID) + .then(function (needDiscard) { + if (options.readonlyIfRealDiscard && !needDiscard) { + return; + } + self.model.discardChanges(recordID); + if (options.noAbandon) { + return; + } + if (self.model.canBeAbandoned(recordID)) { + self._abandonRecord(recordID); + return; + } + return self._confirmSave(recordID); + }); + }, + /** + * Enables buttons so they can be clicked again. + * + * @private + */ + _enableButtons: function () { + if (this.$buttons) { + this.$buttons.find('button').removeAttr('disabled'); + } + }, + /** + * Executes the action associated with a button + * + * @private + * @param {Object} actionData: the descriptor of the action + * @param {string} actionData.type: the button's action's type, accepts "object" or "action" + * @param {string} actionData.name: the button's action's name + * either the model method's name for type "object" + * or the action's id in database, or xml_id + * @param {string} actionData.context: the action's execution context + * + * @param {Object} recordData: basic information on the current record(s) + * @param {number[]} recordData.resIDs: record ids: + * - on which an object method applies + * - that will be used as active_ids to load an action + * @param {string} recordData.model: model name + * @param {Object} recordData.context: the records' context, will be used to load + * the action, and merged into actionData.context at execution time + * + * @returns {Promise} + */ + async _executeButtonAction(actionData, recordData) { + const prom = new Promise((resolve, reject) => { + this.trigger_up('execute_action', { + action_data: actionData, + env: recordData, + on_closed: () => this.isDestroyed() ? Promise.resolve() : this.reload(), + on_success: resolve, + on_fail: () => this.update({}, { reload: false }).then(reject).guardedCatch(reject) + }); + }); + return this.alive(prom); + }, + /** + * Override to add the current record ID (currentId) and the list of ids + * (resIds) in the current dataPoint to the exported state. + * + * @override + */ + exportState: function () { + var state = this._super.apply(this, arguments); + var env = this.model.get(this.handle, {env: true}); + return _.extend(state, { + currentId: env.currentId, + resIds: env.ids, + }); + }, + /** + * Compute the optional fields local storage key using the given parts. + * + * @param {Object} keyParts + * @param {string} keyParts.viewType view type + * @param {string} [keyParts.relationalField] name of the field with subview + * @param {integer} [keyParts.subViewId] subview id + * @param {string} [keyParts.subViewType] type of the subview + * @param {Object} keyParts.fields fields + * @param {string} keyParts.fields.name field name + * @param {string} keyParts.fields.type field type + * @returns {string} local storage key for optional fields in this view + * @private + */ + _getOptionalFieldsLocalStorageKey: function (keyParts) { + keyParts.model = this.modelName; + keyParts.viewType = this.viewType; + keyParts.viewId = this.viewId; + + var parts = [ + 'model', + 'viewType', + 'viewId', + 'relationalField', + 'subViewType', + 'subViewId', + ]; + + var viewIdentifier = parts.reduce(function (identifier, partName) { + if (partName in keyParts) { + return identifier + ',' + keyParts[partName]; + } + return identifier; + }, 'optional_fields'); + + viewIdentifier = + keyParts.fields.sort(this._nameSortComparer) + .reduce(function (identifier, field) { + return identifier + ',' + field.name; + }, viewIdentifier); + + return viewIdentifier; + }, + /** + * Return the params (currentMinimum, limit and size) to pass to the pager, + * according to the current state. + * + * @private + * @returns {Object} + */ + _getPagingInfo: function (state) { + const isGrouped = state.groupedBy && state.groupedBy.length; + return { + currentMinimum: (isGrouped ? state.groupsOffset : state.offset) + 1, + limit: isGrouped ? state.groupsLimit : state.limit, + size: isGrouped ? state.groupsCount : state.count, + }; + }, + /** + * Return the new actionMenus props. + * + * @override + * @private + */ + _getActionMenuItems: function (state) { + return { + activeIds: this.getSelectedIds(), + context: state.getContext(), + }; + }, + /** + * Sort function used to sort the fields by names, to compute the optional fields keys + * + * @param {Object} left + * @param {Object} right + * @private + */ + _nameSortComparer: function(left, right) { + return left.name < right.name ? -1 : 1; + }, + /** + * Helper function to display a warning that some fields have an invalid + * value. This is used when a save operation cannot be completed. + * + * @private + * @param {string[]} invalidFields - list of field names + */ + _notifyInvalidFields: function (invalidFields) { + var record = this.model.get(this.handle, {raw: true}); + var fields = record.fields; + var warnings = invalidFields.map(function (fieldName) { + var fieldStr = fields[fieldName].string; + return _.str.sprintf('
  • %s
  • ', _.escape(fieldStr)); + }); + warnings.unshift(''); + this.do_warn(_t("Invalid fields:"), warnings.join('')); + }, + /** + * Hook method, called when record(s) has been deleted. + * + * @see _deleteRecord + * @param {string[]} ids list of deleted ids (basic model local handles) + */ + _onDeletedRecords: function (ids) { + this.update({}); + }, + /** + * Saves the record whose ID is given, if necessary. Automatically leaves + * edit mode for the given record, unless told otherwise. + * + * @param {string} [recordID] - default to main recordID + * @param {Object} [options] + * @param {boolean} [options.stayInEdit=false] + * if true, leave the record in edit mode after save + * @param {boolean} [options.reload=true] + * if true, reload the record after (real) save + * @param {boolean} [options.savePoint=false] + * if true, the record will only be 'locally' saved: its changes + * will move from the _changes key to the data key + * @returns {Promise} + * 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; + options = _.defaults(options || {}, { + stayInEdit: false, + reload: true, + savePoint: false, + }); + + // 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)) { + var self = this; + var saveDef = this.model.save(recordID, { // Save then leave edit mode + reload: options.reload, + savePoint: options.savePoint, + viewType: options.viewType, + }); + if (!options.stayInEdit) { + saveDef = saveDef.then(function (fieldNames) { + var def = fieldNames.length ? self._confirmSave(recordID) : self._setMode('readonly', recordID); + return def.then(function () { + return fieldNames; + }); + }); + } + return saveDef; + } else { + return Promise.reject("SaveRecord: this.canBeSave is false"); // Cannot be saved + } + }, + /** + * Change the mode for the record associated to the given ID. + * If the given recordID is the view's main one, then the whole view mode is + * changed (@see BasicController.update). + * + * @private + * @param {string} mode - 'readonly' or 'edit' + * @param {string} [recordID] + * @returns {Promise} + */ + _setMode: function (mode, recordID) { + if ((recordID || this.handle) === this.handle) { + return this.update({mode: mode}, {reload: false}).then(function () { + // necessary to allow all sub widgets to use their dimensions in + // layout related activities, such as autoresize on fieldtexts + core.bus.trigger('DOM_updated'); + }); + } + return Promise.resolve(); + }, + /** + * To override such that it returns true iff the primary action button must + * bounce when the user clicked on the given element, according to the + * current state of the view. + * + * @private + * @param {HTMLElement} element the node the user clicked on + * @returns {boolean} + */ + _shouldBounceOnClick: function (/* element */) { + return false; + }, + /** + * Helper method, to get the current environment variables from the model + * and notifies the component chain (by bubbling an event up) + * + * @private + * @param {Object} [newProps={}] + */ + _updateControlPanel: function (newProps = {}) { + const state = this.model.get(this.handle); + const props = Object.assign(newProps, { + actionMenus: this._getActionMenuItems(state), + pager: this._getPagingInfo(state), + title: this.getTitle(), + }); + return this.updateControlPanel(props); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Called when the user clicks on the 'content' part of the controller + * (typically the renderer area). Makes the first primary button in the + * control panel bounce, in some situations (see _shouldBounceOnClick). + * + * @private + * @param {MouseEvent} ev + */ + _onContentClicked(ev) { + if (this.$buttons && this._shouldBounceOnClick(ev.target)) { + this.$buttons.find('.btn-primary:visible:first').odooBounce(); + } + }, + /** + * Called when a list element asks to discard the changes made to one of + * its rows. It can happen with a x2many (if we are in a form view) or with + * a list view. + * + * @private + * @param {OdooEvent} ev + */ + _onDiscardChanges: function (ev) { + var self = this; + ev.stopPropagation(); + var recordID = ev.data.recordID; + this._discardChanges(recordID) + .then(function () { + // TODO this will tell the renderer to rerender the widget that + // asked for the discard but will unfortunately lose the click + // made on another row if any + self._confirmChange(recordID, [ev.data.fieldName], ev) + .then(ev.data.onSuccess).guardedCatch(ev.data.onSuccess); + }) + .guardedCatch(ev.data.onFailure); + }, + /** + * Forces to save directly the changes if the controller is in readonly, + * because in that case the changes come from widgets that are editable even + * in readonly (e.g. Priority). + * + * @private + * @param {OdooEvent} ev + */ + _onFieldChanged: function (ev) { + if (this.mode === 'readonly' && !('force_save' in ev.data)) { + ev.data.force_save = true; + } + FieldManagerMixin._onFieldChanged.apply(this, arguments); + }, + /** + * @private + * @param {OdooEvent} ev + */ + _onPagerChanged: async function (ev) { + ev.stopPropagation(); + const { currentMinimum, limit } = ev.data; + const state = this.model.get(this.handle, { raw: true }); + const reloadParams = state.groupedBy && state.groupedBy.length ? { + groupsLimit: limit, + groupsOffset: currentMinimum - 1, + } : { + limit, + offset: currentMinimum - 1, + }; + await this.reload(reloadParams); + // reset the scroll position to the top on page changed only + if (state.limit === limit) { + this.trigger_up('scrollTo', { top: 0 }); + } + }, + /** + * When a reload event triggers up, we need to reload the full view. + * For example, after a form view dialog saved some data. + * + * @todo: rename db_id into handle + * + * @param {OdooEvent} ev + * @param {Object} ev.data + * @param {string} [ev.data.db_id] handle of the data to reload and + * re-render (reload the whole form by default) + * @param {string[]} [ev.data.fieldNames] list of the record's fields to + * reload + * @param {Function} [ev.data.onSuccess] callback executed after reload is resolved + * @param {Function} [ev.data.onFailure] callback executed when reload is rejected + */ + _onReload: function (ev) { + ev.stopPropagation(); // prevent other controllers from handling this request + var data = ev && ev.data || {}; + var handle = data.db_id; + var prom; + if (handle) { + // reload the relational field given its db_id + prom = this.model.reload(handle).then(this._confirmSave.bind(this, handle)); + } else { + // no db_id given, so reload the main record + prom = this.reload({ + fieldNames: data.fieldNames, + keepChanges: data.keepChanges || false, + }); + } + prom.then(ev.data.onSuccess).guardedCatch(ev.data.onFailure); + }, + /** + * Resequence records in the given order. + * + * @private + * @param {OdooEvent} ev + * @param {string[]} ev.data.recordIds + * @param {integer} ev.data.offset + * @param {string} ev.data.handleField + */ + _onResequenceRecords: function (ev) { + ev.stopPropagation(); // prevent other controllers from handling this request + this.trigger_up('mutexify', { + action: async () => { + let state = this.model.get(this.handle); + const resIDs = ev.data.recordIds + .map(recordID => state.data.find(d => d.id === recordID).res_id); + const options = { + offset: ev.data.offset, + field: ev.data.handleField, + }; + await this.model.resequence(this.modelName, resIDs, this.handle, options); + this._updateControlPanel(); + state = this.model.get(this.handle); + return this._updateRendererState(state, { noRender: true }); + }, + }); + }, + /** + * Load the optional columns settings in local storage for this view + * + * @param {OdooEvent} ev + * @param {Object} ev.data.keyParts see _getLocalStorageKey + * @param {function} ev.data.callback function to call with the result + * @private + */ + _onLoadOptionalFields: function (ev) { + var res = this.call( + 'local_storage', + 'getItem', + this._getOptionalFieldsLocalStorageKey(ev.data.keyParts) + ); + ev.data.callback(res); + }, + /** + * Save the optional columns settings in local storage for this view + * + * @param {OdooEvent} ev + * @param {Object} ev.data.keyParts see _getLocalStorageKey + * @param {Array} ev.data.optionalColumnsEnabled list of optional + * field names that have been enabled + * @private + */ + _onSaveOptionalFields: function (ev) { + this.call( + 'local_storage', + 'setItem', + this._getOptionalFieldsLocalStorageKey(ev.data.keyParts), + ev.data.optionalColumnsEnabled + ); + }, + /** + * @private + * @param {OdooEvent} ev + */ + _onSetDirty: function (ev) { + ev.stopPropagation(); // prevent other controllers from handling this request + this.model.setDirty(ev.data.dataPointID); + }, + /** + * open the translation view for the current field + * + * @private + * @param {OdooEvent} ev + */ + _onTranslate: async function (ev) { + ev.stopPropagation(); + + if (this.model.isNew(ev.data.id)) { + await this._confirmSaveNewRecord(); + var updatedFields = await this.saveRecord(ev.data.id, { stayInEdit: true }); + await this._confirmChange(ev.data.id, updatedFields, ev); + } + var record = this.model.get(ev.data.id, { raw: true }); + var res_id = record.res_id || record.res_ids[0]; + var result = await this._rpc({ + route: '/web/dataset/call_button', + params: { + model: 'ir.translation', + method: 'translate_fields', + args: [record.model, res_id, ev.data.fieldName], + kwargs: { context: record.getContext() }, + } + }); + + this.translationDialog = new TranslationDialog(this, { + domain: result.domain, + searchName: result.context.search_default_name, + fieldName: ev.data.fieldName, + userLanguageValue: ev.target.value || '', + dataPointID: record.id, + isComingFromTranslationAlert: ev.data.isComingFromTranslationAlert, + isText: result.context.translation_type === 'text', + showSrc: result.context.translation_show_src, + }); + return this.translationDialog.open(); + }, +}); + +return BasicController; +}); -- cgit v1.2.3