diff --git a/application/Espo/Resources/i18n/en_US/Global.json b/application/Espo/Resources/i18n/en_US/Global.json index a7915be899..b797401c3d 100644 --- a/application/Espo/Resources/i18n/en_US/Global.json +++ b/application/Espo/Resources/i18n/en_US/Global.json @@ -390,6 +390,7 @@ "massRemoveResult": "{count} records have been removed", "massRemoveResultSingle": "{count} record has been removed", "noRecordsRemoved": "No records were removed", + "changesLossConfirmation": "Unsaved changes will be lost. Are you sure?", "clickToRefresh": "Click to refresh", "writeYourCommentHere": "Write your comment here", "writeMessageToUser": "Write a message to {user}", diff --git a/client/src/view-record-helper.js b/client/src/view-record-helper.js index e2860ca80a..6aa4ccfb37 100644 --- a/client/src/view-record-helper.js +++ b/client/src/view-record-helper.js @@ -35,6 +35,14 @@ import {Events} from 'bullbone'; */ class ViewRecordHelper { + /** + * @private + * @type {{ + * isChanged: boolean, + * }} + */ + state + /** * @param {Object.} [defaultFieldStates] Default field states. * @param {Object.} [defaultPanelStates] Default panel states. @@ -61,6 +69,10 @@ class ViewRecordHelper { this.hiddenPanels = {}; /** @private */ this.fieldOptionListMap = {}; + + this.state = { + isChanged: false, + }; } /** @@ -209,6 +221,26 @@ class ViewRecordHelper { hasFieldOptionList(field) { return (field in this.fieldOptionListMap); } + + /** + * Is changed. + * + * @return {boolean} + * @since 9.2.0 + */ + isChanged() { + return this.state.isChanged; + } + + /** + * Set is changed. + * + * @param {boolean} isChanged + * @since 9.2.0 + */ + setIsChanged(isChanged) { + this.state.isChanged = isChanged; + } } Object.assign(ViewRecordHelper.prototype, Events); diff --git a/client/src/views/fields/base.js b/client/src/views/fields/base.js index cef2181373..3c02d8492b 100644 --- a/client/src/views/fields/base.js +++ b/client/src/views/fields/base.js @@ -1411,64 +1411,65 @@ class BaseFieldView extends View { * * @return {Promise} */ - inlineEdit() { + async inlineEdit() { + if (this.recordHelper && this.recordHelper.isChanged()) { + await this.confirm(this.translate('changesLossConfirmation', 'messages')); + } + this.trigger('edit', this); this.initialAttributes = this.model.getClonedAttributes(); this._isInlineEditMode = true; - const promise = this.setEditMode() - .then(() => this.reRender(true)) - .then(() => this.addInlineEditLinks()) - .then(() => { - this.$el.on('keydown.inline-edit', e => { - const key = Espo.Utils.getKeyFromKeyEvent(e); - - if (key === 'Control+Enter') { - e.stopPropagation(); - - if (document.activeElement instanceof HTMLInputElement) { - // Fields may need to fetch data first. - document.activeElement.dispatchEvent(new Event('change', {bubbles: true})); - } - - this.fetchToModel(); - this.inlineEditSave(); - - setTimeout(() => { - this.get$cell().focus(); - }, 100); - - return; - } - - if (key === 'Escape') { - e.stopPropagation(); - - this.inlineEditClose() - .then(() => { - this.get$cell().focus(); - }); - - return; - } - - if (key === 'Control+KeyS') { - e.preventDefault(); - e.stopPropagation(); - - this.fetchToModel(); - this.inlineEditSave({bypassClose: true}); - } - }); - - setTimeout(() => this.focusOnInlineEdit(), 10); - }); - this.trigger('inline-edit-on'); - return promise; + await this.setEditMode(); + await this.reRender(true); + await this.addInlineEditLinks(); + + this.$el.on('keydown.inline-edit', e => { + const key = Espo.Utils.getKeyFromKeyEvent(e); + + if (key === 'Control+Enter') { + e.stopPropagation(); + + if (document.activeElement instanceof HTMLInputElement) { + // Fields may need to fetch data first. + document.activeElement.dispatchEvent(new Event('change', {bubbles: true})); + } + + this.fetchToModel(); + this.inlineEditSave(); + + setTimeout(() => { + this.get$cell().focus(); + }, 100); + + return; + } + + if (key === 'Escape') { + e.stopPropagation(); + + this.inlineEditClose() + .then(() => { + this.get$cell().focus(); + }); + + return; + } + + if (key === 'Control+KeyS') { + e.preventDefault(); + e.stopPropagation(); + + this.fetchToModel(); + this.inlineEditSave({bypassClose: true}); + } + }); + + setTimeout(() => this.focusOnInlineEdit(), 10); } /** diff --git a/client/src/views/record/detail.js b/client/src/views/record/detail.js index 3e77c2356c..d485f885bd 100644 --- a/client/src/views/record/detail.js +++ b/client/src/views/record/detail.js @@ -2304,6 +2304,7 @@ class DetailRecordView extends BaseRecordView { setIsChanged() { this.isChanged = true; + this.recordHelper.setIsChanged(true); if (this.confirmLeaveDisabled) { return; @@ -2314,6 +2315,7 @@ class DetailRecordView extends BaseRecordView { setIsNotChanged() { this.isChanged = false; + this.recordHelper.setIsChanged(false); if (this.confirmLeaveDisabled) { return;