4261 lines
114 KiB
JavaScript
4261 lines
114 KiB
JavaScript
/************************************************************************
|
||
* This file is part of EspoCRM.
|
||
*
|
||
* EspoCRM – Open Source CRM application.
|
||
* Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||
* Website: https://www.espocrm.com
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU Affero General Public License as published by
|
||
* the Free Software Foundation, either version 3 of the License, or
|
||
* (at your option) any later version.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU Affero General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU Affero General Public License
|
||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
*
|
||
* The interactive user interfaces in modified source and object code versions
|
||
* of this program must display Appropriate Legal Notices, as required under
|
||
* Section 5 of the GNU Affero General Public License version 3.
|
||
*
|
||
* In accordance with Section 7(b) of the GNU Affero General Public License version 3,
|
||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||
************************************************************************/
|
||
|
||
/** @module views/record/detail */
|
||
|
||
import BaseRecordView from 'views/record/base';
|
||
import ViewRecordHelper from 'view-record-helper';
|
||
import ActionItemSetup from 'helpers/action-item-setup';
|
||
import StickyBarHelper from 'helpers/record/misc/sticky-bar';
|
||
import SelectTemplateModalView from 'views/modals/select-template';
|
||
import DebounceHelper from 'helpers/util/debounce';
|
||
|
||
/**
|
||
* A detail record view.
|
||
*/
|
||
class DetailRecordView extends BaseRecordView {
|
||
|
||
/**
|
||
* @typedef {Object} module:views/record/detail~options
|
||
*
|
||
* @property {module:model} model A model.
|
||
* @property {string} [scope] A scope.
|
||
* @property {string} [layoutName] A layout name.
|
||
* @property {module:views/record/detail~panelDefs[]} [detailLayout] A detail layout.
|
||
* @property {boolean} [readOnly] Read-only.
|
||
* @property {string} [rootUrl]
|
||
* @property {string} [returnUrl]
|
||
* @property {boolean} [returnAfterCreate]
|
||
* @property {boolean} [editModeDisabled]
|
||
* @property {boolean} [confirmLeaveDisabled]
|
||
* @property {boolean} [isWide]
|
||
* @property {string|null} [sideView]
|
||
* @property {string|null} [bottomView]
|
||
* @property {string} [inlineEditDisabled] Disable inline edit.
|
||
* @property {boolean} [buttonsDisabled] Disable buttons.
|
||
* @property {string} [navigateButtonsDisabled]
|
||
* @property {Object} [dynamicLogicDefs]
|
||
* @property {module:view-record-helper} [recordHelper] A record helper. For a form state management.
|
||
* @property {Object.<string, *>} [attributes]
|
||
* @property {module:views/record/detail~button[]} [buttonList] Buttons.
|
||
* @property {module:views/record/detail~dropdownItem[]} [dropdownItemList] Dropdown items.
|
||
* @property {Object.<string, *>} [dataObject] Additional data.
|
||
* @property {Record} [rootData] Data from the root view.
|
||
* @property {boolean} [shortcutKeysEnabled] Enable shortcut keys.
|
||
*/
|
||
|
||
/**
|
||
* @param {module:views/record/detail~options | Object.<string, *>} options Options.
|
||
*/
|
||
constructor(options) {
|
||
super(options);
|
||
}
|
||
|
||
/** @inheritDoc */
|
||
template = 'record/detail'
|
||
|
||
/** @inheritDoc */
|
||
type = 'detail'
|
||
|
||
/**
|
||
* A layout name. Can be overridden by an option parameter.
|
||
*
|
||
* @protected
|
||
* @type {string}
|
||
*/
|
||
layoutName = 'detail'
|
||
|
||
/**
|
||
* Panel definitions.
|
||
*
|
||
* @typedef {Object} module:views/record/detail~panelDefs
|
||
* @property {string} [label] A translatable label.
|
||
* @property {string} [customLabel] A custom label.
|
||
* @property {string} [name] A name. Useful to be able to show/hide by a name.
|
||
* @property {'default'|'success'|'danger'|'warning'|'info'} [style] A style.
|
||
* @property {boolean} [tabBreak] Is a tab-break.
|
||
* @property {string} [tabLabel] A tab label. If starts with `$`, a translation
|
||
* of the `tabs` category is used.
|
||
* @property {module:views/record/detail~rowDefs[]} [rows] Rows.
|
||
* @property {module:views/record/detail~rowDefs[]} [columns] Columns.
|
||
* @property {string} [noteText] A note text.
|
||
* @property {'success'|'danger'|'warning'|'info'} [noteStyle] A note style.
|
||
*/
|
||
|
||
/**
|
||
* A row.
|
||
*
|
||
* @typedef {Array<module:views/record/detail~cellDefs|false>} module:views/record/detail~rowDefs
|
||
*/
|
||
|
||
/**
|
||
* Cell definitions.
|
||
*
|
||
* @typedef {Object} module:views/record/detail~cellDefs
|
||
* @property {string} [name] A name (usually a field name).
|
||
* @property {string|module:views/fields/base} [view] An overridden field view name or a view instance.
|
||
* @property {string} [type] An overridden field type.
|
||
* @property {boolean} [readOnly] Read-only.
|
||
* @property {boolean} [inlineEditDisabled] Disable inline edit.
|
||
* @property {Object.<string, *>} [params] Overridden field parameters.
|
||
* @property {Object.<string, *>} [options] Field view options.
|
||
* @property {string} [labelText] A label text (not-translatable).
|
||
* @property {boolean} [noLabel] No label.
|
||
* @property {string} [label] A translatable label (using the `fields` category).
|
||
* @property {1|2|3|4} [span] A width.
|
||
*/
|
||
|
||
/**
|
||
* A layout. If null, then will be loaded from the backend (using the `layoutName` value).
|
||
* Can be overridden by an option parameter.
|
||
*
|
||
* @protected
|
||
* @type {module:views/record/detail~panelDefs[]|null}
|
||
*/
|
||
detailLayout = null
|
||
|
||
/**
|
||
* A fields mode.
|
||
*
|
||
* @protected
|
||
* @type {'detail'|'edit'|'list'}
|
||
*/
|
||
fieldsMode = 'detail'
|
||
|
||
/**
|
||
* A current mode. Only for reading.
|
||
*
|
||
* @protected
|
||
* @type {'detail'|'edit'}
|
||
*/
|
||
mode = 'detail'
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
gridLayout = null
|
||
|
||
/**
|
||
* Disable buttons. Can be overridden by an option parameter.
|
||
*
|
||
* @protected
|
||
* @type {boolean}
|
||
*/
|
||
buttonsDisabled = false
|
||
|
||
/**
|
||
* Is record new. Only for reading.
|
||
*
|
||
* @protected
|
||
*/
|
||
isNew = false
|
||
|
||
/**
|
||
* A button. Handled by an `action{Name}` method, a click handler or a handler class.
|
||
*
|
||
* @typedef module:views/record/detail~button
|
||
*
|
||
* @property {string} name A name.
|
||
* @property {string} [label] A label.
|
||
* @property {string} [labelTranslation] A label translation path.
|
||
* @property {string} [html] An HTML.
|
||
* @property {string} [text] A text.
|
||
* @property {'default'|'danger'|'success'|'warning'|'primary'} [style] A style.
|
||
* @property {boolean} [hidden] Hidden.
|
||
* @property {string} [title] A title (not translatable).
|
||
* @property {boolean} [disabled] Disabled.
|
||
* @property {function()} [onClick] A click handler.
|
||
*/
|
||
|
||
/**
|
||
* A dropdown item. Handled by an `action{Name}` method, a click handler or a handler class.
|
||
*
|
||
* @typedef module:views/record/detail~dropdownItem
|
||
*
|
||
* @property {string} name A name.
|
||
* @property {string} [label] A label.
|
||
* @property {string} [labelTranslation] A label translation path.
|
||
* @property {string} [html] An HTML.
|
||
* @property {string} [text] A text.
|
||
* @property {boolean} [hidden] Hidden.
|
||
* @property {Object.<string, string>} [data] Data attributes.
|
||
* @property {string} [title] A title (not translatable).
|
||
* @property {boolean} [disabled] Disabled.
|
||
* @property {function()} [onClick] A click handler.
|
||
* @property {number} [groupIndex] A group index.
|
||
*/
|
||
|
||
/**
|
||
* A button list.
|
||
*
|
||
* @protected
|
||
* @type {module:views/record/detail~button[]}
|
||
*/
|
||
buttonList = [
|
||
{
|
||
name: 'edit',
|
||
label: 'Edit',
|
||
title: 'Ctrl+Space',
|
||
},
|
||
]
|
||
|
||
/**
|
||
* A dropdown item list.
|
||
*
|
||
* @protected
|
||
* @type {Array<module:views/record/detail~dropdownItem>}
|
||
*/
|
||
dropdownItemList = [
|
||
{
|
||
name: 'delete',
|
||
label: 'Remove',
|
||
groupIndex: 0,
|
||
},
|
||
]
|
||
|
||
/**
|
||
* A button list for edit mode.
|
||
*
|
||
* @protected
|
||
* @type {module:views/record/detail~button[]}
|
||
*/
|
||
buttonEditList = [
|
||
{
|
||
name: 'save',
|
||
label: 'Save',
|
||
style: 'primary',
|
||
edit: true,
|
||
title: 'Ctrl+Enter',
|
||
},
|
||
{
|
||
name: 'cancelEdit',
|
||
label: 'Cancel',
|
||
edit: true,
|
||
title: 'Esc',
|
||
},
|
||
]
|
||
|
||
/**
|
||
* A dropdown item list for edit mode.
|
||
*
|
||
* @protected
|
||
* @type {module:views/record/detail~dropdownItem[]}
|
||
*/
|
||
dropdownEditItemList = []
|
||
|
||
/**
|
||
* All action items disabled;
|
||
*
|
||
* @protected
|
||
*/
|
||
allActionItemsDisabled = false
|
||
|
||
/**
|
||
* A DOM element ID. Only for reading.
|
||
*
|
||
* @private
|
||
* @type {string|null}
|
||
*/
|
||
id = null
|
||
|
||
/**
|
||
* A return-URL. Can be overridden by an option parameter.
|
||
*
|
||
* @protected
|
||
* @type {string|null}
|
||
*/
|
||
returnUrl = null
|
||
|
||
/**
|
||
* A return dispatch params. Can be overridden by an option parameter.
|
||
*
|
||
* @protected
|
||
* @type {Object|null}
|
||
*/
|
||
returnDispatchParams = null
|
||
|
||
/**
|
||
* A middle view name.
|
||
*
|
||
* @protected
|
||
*/
|
||
middleView = 'views/record/detail-middle'
|
||
|
||
/**
|
||
* A side view name.
|
||
*
|
||
* @protected
|
||
*/
|
||
sideView = 'views/record/detail-side'
|
||
|
||
/**
|
||
* A bottom view name.
|
||
*
|
||
* @protected
|
||
*/
|
||
bottomView = 'views/record/detail-bottom'
|
||
|
||
/**
|
||
* Disable a side view. Can be overridden by an option parameter.
|
||
*
|
||
* @protected
|
||
*/
|
||
sideDisabled = false
|
||
|
||
/**
|
||
* Disable a bottom view. Can be overridden by an option parameter.
|
||
*
|
||
* @protected
|
||
*/
|
||
bottomDisabled = false
|
||
|
||
/**
|
||
* @protected
|
||
*/
|
||
gridLayoutType = 'record'
|
||
|
||
/**
|
||
* Disable edit mode. Can be overridden by an option parameter.
|
||
*
|
||
* @protected
|
||
*/
|
||
editModeDisabled = false
|
||
|
||
/**
|
||
* Disable navigate (prev, next) buttons. Can be overridden by an option parameter.
|
||
*
|
||
* @protected
|
||
*/
|
||
navigateButtonsDisabled = false
|
||
|
||
/**
|
||
* Read-only. Can be overridden by an option parameter.
|
||
*/
|
||
readOnly = false
|
||
|
||
/**
|
||
* Middle view expanded to full width (no side view).
|
||
* Can be overridden by an option parameter.
|
||
*
|
||
* @protected
|
||
*/
|
||
isWide = false
|
||
|
||
/**
|
||
* Enable a duplicate action.
|
||
*
|
||
* @protected
|
||
*/
|
||
duplicateAction = true
|
||
|
||
/**
|
||
* Enable a self-assign action.
|
||
*
|
||
* @protected
|
||
*/
|
||
selfAssignAction = false
|
||
|
||
/**
|
||
* Enable a print-pdf action.
|
||
*
|
||
* @protected
|
||
*/
|
||
printPdfAction = true
|
||
|
||
/**
|
||
* Enable a convert-currency action.
|
||
*
|
||
* @protected
|
||
*/
|
||
convertCurrencyAction = true
|
||
|
||
/**
|
||
* Enable a save-and-continue-editing action.
|
||
*
|
||
* @protected
|
||
*/
|
||
saveAndContinueEditingAction = true
|
||
|
||
/**
|
||
* Disable the inline-edit. Can be overridden by an option parameter.
|
||
*
|
||
* @protected
|
||
*/
|
||
inlineEditDisabled = false
|
||
|
||
/**
|
||
* Disable a portal layout usage. Can be overridden by an option parameter.
|
||
*
|
||
* @protected
|
||
*/
|
||
portalLayoutDisabled = false
|
||
|
||
/**
|
||
* A panel soft-locked type.
|
||
*
|
||
* @typedef {'default'|'acl'|'delimiter'|'dynamicLogic'
|
||
* } module:views/record/detail~panelSoftLockedType
|
||
*/
|
||
|
||
/**
|
||
* @private
|
||
* @type {module:views/record/detail~panelSoftLockedType[]}
|
||
*/
|
||
panelSoftLockedTypeList = [
|
||
'default',
|
||
'acl',
|
||
'delimiter',
|
||
'dynamicLogic',
|
||
]
|
||
|
||
/**
|
||
* Dynamic logic. Can be overridden by an option parameter.
|
||
*
|
||
* @protected
|
||
* @type {Object}
|
||
* @todo Add typedef.
|
||
*/
|
||
dynamicLogicDefs = {}
|
||
|
||
/**
|
||
* Disable confirm leave-out processing.
|
||
*
|
||
* @protected
|
||
*/
|
||
confirmLeaveDisabled = false
|
||
|
||
/**
|
||
* @protected
|
||
*/
|
||
setupHandlerType = 'record/detail'
|
||
|
||
/**
|
||
* @protected
|
||
*/
|
||
currentTab = 0
|
||
|
||
/**
|
||
* @protected
|
||
* @type {Object.<string,*>|null}
|
||
*/
|
||
middlePanelDefs = null
|
||
|
||
/**
|
||
* @protected
|
||
* @type {Object.<string,*>[]|null}
|
||
*/
|
||
middlePanelDefsList = null
|
||
|
||
/**
|
||
* @protected
|
||
* @type {JQuery|null}
|
||
*/
|
||
$middle = null
|
||
|
||
/**
|
||
* @protected
|
||
* @type {JQuery|null}
|
||
*/
|
||
$bottom = null
|
||
|
||
/**
|
||
* @private
|
||
* @type {JQuery|null}
|
||
*/
|
||
$detailButtonContainer = null
|
||
|
||
/** @private */
|
||
blockUpdateWebSocketPeriod = 500
|
||
|
||
/**
|
||
* @internal
|
||
* @protected
|
||
*/
|
||
stickButtonsFormBottomSelector
|
||
|
||
/**
|
||
* @protected
|
||
* @type {string}
|
||
*/
|
||
dynamicHandlerClassName
|
||
|
||
/**
|
||
* Disable access control.
|
||
*
|
||
* @protected
|
||
* @type {boolean}
|
||
*/
|
||
accessControlDisabled
|
||
|
||
/**
|
||
* @protected
|
||
* @type {boolean}
|
||
*/
|
||
inlineEditModeIsOn = false
|
||
|
||
/**
|
||
* A Ctrl+Enter shortcut action.
|
||
*
|
||
* @protected
|
||
* @type {?string}
|
||
*/
|
||
shortcutKeyCtrlEnterAction = 'save'
|
||
|
||
/**
|
||
* Additional data. Passed to sub-views and fields.
|
||
*
|
||
* @protected
|
||
* @type {Object.<string, *>}
|
||
* @since 9.0.0
|
||
*/
|
||
dataObject
|
||
|
||
/**
|
||
* Data from the root view.
|
||
*
|
||
* @protected
|
||
* @type {Record}
|
||
* @since 9.0.0
|
||
*/
|
||
rootData
|
||
|
||
/**
|
||
* @private
|
||
* @type {DebounceHelper}
|
||
*/
|
||
_webSocketDebounceHelper
|
||
|
||
/**
|
||
* @private
|
||
* @type {number}
|
||
*/
|
||
_webSocketDebounceInterval = 500
|
||
|
||
/**
|
||
* A shortcut-key => action map.
|
||
*
|
||
* @protected
|
||
* @type {?Object.<string, string|function (KeyboardEvent): void>}
|
||
*/
|
||
shortcutKeys = {
|
||
/** @this DetailRecordView */
|
||
'Control+Enter': function (e) {
|
||
this.handleShortcutKeyCtrlEnter(e);
|
||
},
|
||
/** @this DetailRecordView */
|
||
'Control+Alt+Enter': function (e) {
|
||
this.handleShortcutKeyCtrlAltEnter(e);
|
||
},
|
||
/** @this DetailRecordView */
|
||
'Control+KeyS': function (e) {
|
||
this.handleShortcutKeyCtrlS(e);
|
||
},
|
||
/** @this DetailRecordView */
|
||
'Control+Space': function (e) {
|
||
this.handleShortcutKeyCtrlSpace(e);
|
||
},
|
||
/** @this DetailRecordView */
|
||
'Escape': function (e) {
|
||
this.handleShortcutKeyEscape(e);
|
||
},
|
||
/** @this DetailRecordView */
|
||
'Control+Backslash': function (e) {
|
||
this.handleShortcutKeyControlBackslash(e);
|
||
},
|
||
/** @this DetailRecordView */
|
||
'Control+ArrowLeft': function (e) {
|
||
this.handleShortcutKeyControlArrowLeft(e);
|
||
},
|
||
/** @this DetailRecordView */
|
||
'Control+ArrowRight': function (e) {
|
||
this.handleShortcutKeyControlArrowRight(e);
|
||
},
|
||
}
|
||
|
||
/**
|
||
* @inheritDoc
|
||
*/
|
||
events = {
|
||
/** @this DetailRecordView */
|
||
'click .button-container .action': function (e) {
|
||
const target = /** @type {HTMLElement} */e.currentTarget;
|
||
|
||
let actionItems = undefined;
|
||
|
||
if (target.classList.contains('detail-action-item')) {
|
||
actionItems = [...this.buttonList, ...this.dropdownItemList]
|
||
}
|
||
else if (target.classList.contains('edit-action-item')) {
|
||
actionItems = [...this.buttonEditList, ...this.dropdownEditItemList];
|
||
}
|
||
|
||
Espo.Utils.handleAction(this, e.originalEvent, target, {actionItems: actionItems});
|
||
},
|
||
/** @this DetailRecordView */
|
||
'click [data-action="showMoreDetailPanels"]': function () {
|
||
this.showMoreDetailPanels();
|
||
},
|
||
/** @this DetailRecordView */
|
||
'click .middle-tabs > button': function (e) {
|
||
const tab = parseInt($(e.currentTarget).attr('data-tab'));
|
||
|
||
this.selectTab(tab);
|
||
},
|
||
}
|
||
|
||
/**
|
||
* An `edit` action.
|
||
*/
|
||
actionEdit() {
|
||
if (!this.editModeDisabled) {
|
||
this.setEditMode();
|
||
|
||
this.focusOnFirstDiv();
|
||
$(window).scrollTop(0);
|
||
|
||
return;
|
||
}
|
||
|
||
const options = {
|
||
id: this.model.id,
|
||
model: this.model.clone(),
|
||
};
|
||
|
||
if (this.options.rootUrl) {
|
||
options.rootUrl = this.options.rootUrl;
|
||
}
|
||
|
||
if (this.inlineEditModeIsOn) {
|
||
options.attributes = this.getChangedAttributes();
|
||
|
||
this.resetModelChanges();
|
||
}
|
||
|
||
this.getRouter().navigate(`#${this.scope}/edit/${this.model.id}`, {trigger: false});
|
||
this.getRouter().dispatch(this.scope, 'edit', options);
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
actionDelete() {
|
||
this.delete();
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
/**
|
||
* A `save` action.
|
||
*
|
||
* @param {{options?: module:views/record/base~saveOptions}} [data] Data.
|
||
* @return Promise
|
||
*/
|
||
actionSave(data) {
|
||
data = data || {};
|
||
|
||
const modeBeforeSave = this.mode;
|
||
|
||
const promise = this.save(data.options)
|
||
.catch(reason => {
|
||
if (
|
||
modeBeforeSave === this.MODE_EDIT &&
|
||
['error', 'cancel'].includes(reason)
|
||
) {
|
||
this.setEditMode();
|
||
}
|
||
|
||
return Promise.reject(reason);
|
||
});
|
||
|
||
if (!this.lastSaveCancelReason || this.lastSaveCancelReason === 'notModified') {
|
||
this.setDetailMode();
|
||
|
||
this.focusOnFirstDiv();
|
||
$(window).scrollTop(0);
|
||
}
|
||
|
||
return promise;
|
||
}
|
||
|
||
actionCancelEdit() {
|
||
this.cancelEdit();
|
||
|
||
this.focusOnFirstDiv();
|
||
$(window).scrollTop(0);
|
||
}
|
||
|
||
focusOnFirstDiv() {
|
||
const element = /** @type {HTMLElement} */this.$el.find('> div').get(0);
|
||
|
||
if (element) {
|
||
element.focus({preventScroll: true});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* A `save-and-continue-editing` action.
|
||
*/
|
||
actionSaveAndContinueEditing(data) {
|
||
data = data || {};
|
||
|
||
this.save(data.options)
|
||
.catch(() => {});
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
/**
|
||
* A `self-assign` action.
|
||
*/
|
||
actionSelfAssign() {
|
||
const attributes = {
|
||
assignedUserId: this.getUser().id,
|
||
assignedUserName: this.getUser().get('name'),
|
||
};
|
||
|
||
if ('getSelfAssignAttributes' in this) {
|
||
const attributesAdditional = this.getSelfAssignAttributes();
|
||
|
||
if (attributesAdditional) {
|
||
for (const i in attributesAdditional) {
|
||
attributes[i] = attributesAdditional[i];
|
||
}
|
||
}
|
||
}
|
||
|
||
this.model
|
||
.save(attributes, {patch: true})
|
||
.then(() => {
|
||
Espo.Ui.success(this.translate('Self-Assigned'));
|
||
});
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
/**
|
||
* A `convert-currency` action.
|
||
*/
|
||
actionConvertCurrency() {
|
||
this.createView('modalConvertCurrency', 'views/modals/convert-currency', {
|
||
entityType: this.entityType,
|
||
model: this.model,
|
||
}, view => {
|
||
view.render();
|
||
|
||
this.listenToOnce(view, 'after:update', attributes => {
|
||
let isChanged = false;
|
||
|
||
for (const a in attributes) {
|
||
if (attributes[a] !== this.model.get(a)) {
|
||
isChanged = true;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!isChanged) {
|
||
Espo.Ui.warning(this.translate('notUpdated', 'messages'));
|
||
|
||
return;
|
||
}
|
||
|
||
this.model
|
||
.fetch()
|
||
.then(() => {
|
||
Espo.Ui.success(this.translate('done', 'messages'));
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Compose attribute values for a self-assignment.
|
||
*
|
||
* @protected
|
||
* @return {Object.<string,*>|null}
|
||
*/
|
||
getSelfAssignAttributes() {
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Set up action items.
|
||
*
|
||
* @protected
|
||
*/
|
||
setupActionItems() {
|
||
if (this.model.isNew()) {
|
||
this.isNew = true;
|
||
|
||
this.removeActionItem('delete');
|
||
}
|
||
else if (this.getMetadata().get(['clientDefs', this.scope, 'removeDisabled'])) {
|
||
this.removeActionItem('delete');
|
||
}
|
||
|
||
if (this.duplicateAction) {
|
||
if (
|
||
this.getAcl().check(this.entityType, 'create') &&
|
||
!this.getMetadata().get(['clientDefs', this.scope, 'duplicateDisabled'])
|
||
) {
|
||
this.addDropdownItem({
|
||
label: 'Duplicate',
|
||
name: 'duplicate',
|
||
groupIndex: 0,
|
||
});
|
||
}
|
||
}
|
||
|
||
if (this.selfAssignAction) {
|
||
if (
|
||
this.getAcl().check(this.entityType, 'edit') &&
|
||
!~this.getAcl().getScopeForbiddenFieldList(this.entityType).indexOf('assignedUser') &&
|
||
!this.getUser().isPortal()
|
||
) {
|
||
if (this.model.has('assignedUserId')) {
|
||
this.addDropdownItem({
|
||
label: 'Self-Assign',
|
||
name: 'selfAssign',
|
||
hidden: !!this.model.get('assignedUserId'),
|
||
groupIndex: 0,
|
||
});
|
||
|
||
this.listenTo(this.model, 'change:assignedUserId', () => {
|
||
if (!this.model.get('assignedUserId')) {
|
||
this.showActionItem('selfAssign');
|
||
} else {
|
||
this.hideActionItem('selfAssign');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
if (this.type === this.TYPE_DETAIL && this.printPdfAction) {
|
||
let printPdfAction = true;
|
||
|
||
if (
|
||
!~(this.getHelper().getAppParam('templateEntityTypeList') || [])
|
||
.indexOf(this.entityType)
|
||
) {
|
||
printPdfAction = false;
|
||
}
|
||
|
||
if (printPdfAction) {
|
||
this.addDropdownItem({
|
||
label: 'Print to PDF',
|
||
name: 'printPdf',
|
||
groupIndex: 6,
|
||
});
|
||
}
|
||
}
|
||
|
||
if (this.type === this.TYPE_DETAIL && this.convertCurrencyAction) {
|
||
if (
|
||
this.getAcl().check(this.entityType, 'edit') &&
|
||
!this.getMetadata().get(['clientDefs', this.scope, 'convertCurrencyDisabled'])
|
||
) {
|
||
const currencyFieldList = this.getFieldManager()
|
||
.getEntityTypeFieldList(this.entityType, {
|
||
type: 'currency',
|
||
acl: 'edit',
|
||
});
|
||
|
||
if (currencyFieldList.length) {
|
||
this.addDropdownItem({
|
||
label: 'Convert Currency',
|
||
name: 'convertCurrency',
|
||
groupIndex: 5,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
if (
|
||
this.type === this.TYPE_DETAIL &&
|
||
this.getMetadata().get(['scopes', this.scope, 'hasPersonalData'])
|
||
) {
|
||
if (this.getAcl().getPermissionLevel('dataPrivacyPermission') === 'yes') {
|
||
this.dropdownItemList.push({
|
||
label: 'View Personal Data',
|
||
name: 'viewPersonalData',
|
||
groupIndex: 4,
|
||
});
|
||
}
|
||
}
|
||
|
||
if (this.type === this.TYPE_DETAIL && this.getMetadata().get(['scopes', this.scope, 'stream'])) {
|
||
this.addDropdownItem({
|
||
label: 'View Followers',
|
||
name: 'viewFollowers',
|
||
groupIndex: 4,
|
||
});
|
||
}
|
||
|
||
if (this.type === this.TYPE_DETAIL) {
|
||
const actionItemSetup = new ActionItemSetup();
|
||
|
||
actionItemSetup.setup(
|
||
this,
|
||
this.type,
|
||
promise => this.wait(promise),
|
||
item => this.addDropdownItem(item),
|
||
name => this.showActionItem(name),
|
||
name => this.hideActionItem(name)
|
||
);
|
||
|
||
if (this.saveAndContinueEditingAction) {
|
||
this.dropdownEditItemList.push({
|
||
name: 'saveAndContinueEditing',
|
||
label: 'Save & Continue Editing',
|
||
title: 'Ctrl+S',
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Disable action items.
|
||
*/
|
||
disableActionItems() {
|
||
// noinspection JSDeprecatedSymbols
|
||
this.disableButtons();
|
||
}
|
||
|
||
/**
|
||
* Enable action items.
|
||
*/
|
||
enableActionItems() {
|
||
// noinspection JSDeprecatedSymbols
|
||
this.enableButtons();
|
||
}
|
||
|
||
/**
|
||
* Hide a button or dropdown action item.
|
||
*
|
||
* @param {string} name A name.
|
||
*/
|
||
hideActionItem(name) {
|
||
for (const item of this.buttonList) {
|
||
if (item.name === name) {
|
||
item.hidden = true;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const item of this.dropdownItemList) {
|
||
if (item.name === name) {
|
||
item.hidden = true;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const item of this.dropdownEditItemList) {
|
||
if (item.name === name) {
|
||
item.hidden = true;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const item of this.buttonEditList) {
|
||
if (item.name === name) {
|
||
item.hidden = true;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (this.isRendered()) {
|
||
this.$detailButtonContainer
|
||
.find('li > .action[data-action="'+name+'"]')
|
||
.parent()
|
||
.addClass('hidden');
|
||
|
||
this.$detailButtonContainer
|
||
.find('button.action[data-action="'+name+'"]')
|
||
.addClass('hidden');
|
||
|
||
if (this.isDropdownItemListEmpty()) {
|
||
this.$dropdownItemListButton.addClass('hidden');
|
||
}
|
||
|
||
if (this.isDropdownEditItemListEmpty()) {
|
||
this.$dropdownEditItemListButton.addClass('hidden');
|
||
}
|
||
|
||
this.adjustButtons();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Show a button or dropdown action item.
|
||
*
|
||
* @param {string} name A name.
|
||
*/
|
||
showActionItem(name) {
|
||
for (const item of this.buttonList) {
|
||
if (item.name === name) {
|
||
item.hidden = false;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const item of this.dropdownItemList) {
|
||
if (item.name === name) {
|
||
item.hidden = false;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const item of this.dropdownEditItemList) {
|
||
if (item.name === name) {
|
||
item.hidden = false;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const item of this.buttonEditList) {
|
||
if (item.name === name) {
|
||
item.hidden = false;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (this.isRendered()) {
|
||
this.$detailButtonContainer
|
||
.find('li > .action[data-action="'+name+'"]')
|
||
.parent()
|
||
.removeClass('hidden');
|
||
|
||
this.$detailButtonContainer
|
||
.find('button.action[data-action="'+name+'"]')
|
||
.removeClass('hidden');
|
||
|
||
if (!this.isDropdownItemListEmpty()) {
|
||
this.$dropdownItemListButton.removeClass('hidden');
|
||
}
|
||
|
||
if (!this.isDropdownEditItemListEmpty()) {
|
||
this.$dropdownEditItemListButton.removeClass('hidden');
|
||
}
|
||
|
||
this.adjustButtons();
|
||
}
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
/**
|
||
* Disable a button or dropdown action item.
|
||
*
|
||
* @param {string} name A name.
|
||
*/
|
||
disableActionItem(name) {
|
||
for (const item of this.buttonList) {
|
||
if (item.name === name) {
|
||
item.disabled = true;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const item of this.dropdownItemList) {
|
||
if (item.name === name) {
|
||
item.disabled = true;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const item of this.dropdownEditItemList) {
|
||
if (item.name === name) {
|
||
item.disabled = true;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const item of this.buttonEditList) {
|
||
if (item.name === name) {
|
||
item.disabled = true;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (this.isRendered()) {
|
||
this.$detailButtonContainer
|
||
.find('li > .action[data-action="'+name+'"]')
|
||
.parent()
|
||
.addClass('disabled')
|
||
.attr('disabled', 'disabled');
|
||
|
||
this.$detailButtonContainer
|
||
.find('button.action[data-action="'+name+'"]')
|
||
.addClass('disabled')
|
||
.attr('disabled', 'disabled');
|
||
}
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
/**
|
||
* Enable a button or dropdown action item.
|
||
*
|
||
* @param {string} name A name.
|
||
*/
|
||
enableActionItem(name) {
|
||
for (const item of this.buttonList) {
|
||
if (item.name === name) {
|
||
item.disabled = false;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const item of this.dropdownItemList) {
|
||
if (item.name === name) {
|
||
item.disabled = false;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const item of this.dropdownEditItemList) {
|
||
if (item.name === name) {
|
||
item.disabled = false;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const item of this.buttonEditList) {
|
||
if (item.name === name) {
|
||
item.disabled = false;
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (this.isRendered()) {
|
||
this.$detailButtonContainer
|
||
.find('li > .action[data-action="'+name+'"]')
|
||
.parent()
|
||
.removeClass('disabled')
|
||
.removeAttr('disabled');
|
||
|
||
this.$detailButtonContainer
|
||
.find('button.action[data-action="'+name+'"]')
|
||
.removeClass('disabled')
|
||
.removeAttr('disabled');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Whether an action item is visible and not disabled.
|
||
*
|
||
* @param {string} name An action item name.
|
||
*/
|
||
hasAvailableActionItem(name) {
|
||
if (this.allActionItemsDisabled) {
|
||
return false;
|
||
}
|
||
|
||
if (this.type === this.TYPE_DETAIL && this.mode === this.MODE_EDIT) {
|
||
const hasButton = this.buttonEditList
|
||
.findIndex(item => item.name === name && !item.disabled && !item.hidden) !== -1;
|
||
|
||
if (hasButton) {
|
||
return true;
|
||
}
|
||
|
||
return this.dropdownEditItemList
|
||
.findIndex(item => item.name === name && !item.disabled && !item.hidden) !== -1;
|
||
}
|
||
|
||
const hasButton = this.buttonList
|
||
.findIndex(item => item.name === name && !item.disabled && !item.hidden) !== -1;
|
||
|
||
if (hasButton) {
|
||
return true;
|
||
}
|
||
|
||
return this.dropdownItemList
|
||
.findIndex(item => item.name === name && !item.disabled && !item.hidden) !== -1;
|
||
}
|
||
|
||
/**
|
||
* Show a panel.
|
||
*
|
||
* @param {string} name A panel name.
|
||
* @param {module:views/record/detail~panelSoftLockedType} [softLockedType='default']
|
||
* A soft-locked type.
|
||
*/
|
||
showPanel(name, softLockedType) {
|
||
if (this.recordHelper.getPanelStateParam(name, 'hiddenLocked')) {
|
||
return;
|
||
}
|
||
|
||
softLockedType = softLockedType || 'default';
|
||
|
||
const softLockedParam = 'hidden' + Espo.Utils.upperCaseFirst(softLockedType) + 'Locked'
|
||
|
||
this.recordHelper.setPanelStateParam(name, softLockedParam, false);
|
||
|
||
if (
|
||
softLockedType === 'dynamicLogic' &&
|
||
this.recordHelper.getPanelStateParam(name, 'hidden') === false
|
||
) {
|
||
return;
|
||
}
|
||
|
||
for (let i = 0; i < this.panelSoftLockedTypeList.length; i++) {
|
||
const iType = this.panelSoftLockedTypeList[i];
|
||
|
||
if (iType === softLockedType) {
|
||
continue;
|
||
}
|
||
|
||
const iParam = 'hidden' + Espo.Utils.upperCaseFirst(iType) + 'Locked';
|
||
|
||
if (this.recordHelper.getPanelStateParam(name, iParam)) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
const middleView = this.getMiddleView();
|
||
|
||
if (middleView) {
|
||
middleView.showPanelInternal(name);
|
||
}
|
||
|
||
const bottomView = this.getBottomView();
|
||
|
||
if (bottomView) {
|
||
if ('showPanel' in bottomView) {
|
||
bottomView.showPanel(name);
|
||
}
|
||
} else if (this.bottomView) {
|
||
this.once('ready', () => {
|
||
const view = this.getBottomView();
|
||
|
||
if (view) {
|
||
if ('processShowPanel' in view) {
|
||
view.processShowPanel(name);
|
||
|
||
return;
|
||
}
|
||
|
||
if ('showPanel' in view) {
|
||
view.showPanel(name);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
const sideView = this.getSideView();
|
||
|
||
if (sideView) {
|
||
if ('showPanel' in sideView) {
|
||
sideView.showPanel(name);
|
||
}
|
||
} else if (this.sideView) {
|
||
this.once('ready', () => {
|
||
const view = this.getSideView();
|
||
|
||
if (view) {
|
||
if ('processShowPanel' in view) {
|
||
view.processShowPanel(name);
|
||
|
||
return;
|
||
}
|
||
|
||
if ('showPanel' in view) {
|
||
view.showPanel(name);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
this.recordHelper.setPanelStateParam(name, 'hidden', false);
|
||
|
||
if (this.middlePanelDefs[name]) {
|
||
this.controlTabVisibilityShow(this.middlePanelDefs[name].tabNumber);
|
||
|
||
this.adjustMiddlePanels();
|
||
}
|
||
|
||
this.recordHelper.trigger('panel-show');
|
||
}
|
||
|
||
/**
|
||
* Hide a panel.
|
||
*
|
||
* @param {string} name A panel name.
|
||
* @param {boolean} [locked=false] Won't be able to un-hide.
|
||
* @param {module:views/record/detail~panelSoftLockedType} [softLockedType='default']
|
||
* A soft-locked type.
|
||
*/
|
||
hidePanel(name, locked, softLockedType) {
|
||
softLockedType = softLockedType || 'default';
|
||
|
||
if (locked) {
|
||
this.recordHelper.setPanelStateParam(name, 'hiddenLocked', true);
|
||
}
|
||
|
||
const softLockedParam = 'hidden' + Espo.Utils.upperCaseFirst(softLockedType) + 'Locked'
|
||
|
||
this.recordHelper.setPanelStateParam(name, softLockedParam, true);
|
||
|
||
if (
|
||
softLockedType === 'dynamicLogic' &&
|
||
this.recordHelper.getPanelStateParam(name, 'hidden') === true
|
||
) {
|
||
return;
|
||
}
|
||
|
||
const middleView = this.getMiddleView();
|
||
|
||
if (middleView) {
|
||
middleView.hidePanelInternal(name);
|
||
}
|
||
|
||
const bottomView = this.getBottomView();
|
||
|
||
if (bottomView) {
|
||
if ('hidePanel' in bottomView) {
|
||
bottomView.hidePanel(name);
|
||
}
|
||
} else if (this.bottomView) {
|
||
this.once('ready', () => {
|
||
const view = this.getBottomView();
|
||
|
||
if (view) {
|
||
if ('processHidePanel' in view) {
|
||
view.processHidePanel(name);
|
||
|
||
return;
|
||
}
|
||
|
||
if ('hidePanel' in view) {
|
||
view.hidePanel(name);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
const sideView = this.getSideView();
|
||
|
||
if (sideView) {
|
||
if ('hidePanel' in sideView) {
|
||
sideView.hidePanel(name);
|
||
}
|
||
} else if (this.sideView) {
|
||
this.once('ready', () => {
|
||
const view = this.getSideView();
|
||
|
||
if (view) {
|
||
if ('processHidePanel' in view) {
|
||
view.processHidePanel(name);
|
||
|
||
return;
|
||
}
|
||
|
||
if ('hidePanel' in view) {
|
||
view.hidePanel(name);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
this.recordHelper.setPanelStateParam(name, 'hidden', true);
|
||
|
||
if (this.middlePanelDefs[name]) {
|
||
this.controlTabVisibilityHide(this.middlePanelDefs[name].tabNumber);
|
||
|
||
this.adjustMiddlePanels();
|
||
}
|
||
}
|
||
|
||
afterRender() {
|
||
this.$middle = this.$el.find('.middle');
|
||
|
||
if (this.bottomView) {
|
||
this.$bottom = this.$el.find('.bottom');
|
||
}
|
||
|
||
this.initElementReferences();
|
||
|
||
this.adjustMiddlePanels();
|
||
this.adjustButtons();
|
||
|
||
this.initStickableButtonsContainer();
|
||
this.initFieldsControlBehaviour();
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
initFieldsControlBehaviour() {
|
||
const fields = this.getFieldViews();
|
||
|
||
let fieldInEditMode = null;
|
||
|
||
for (const field in fields) {
|
||
const fieldView = fields[field];
|
||
|
||
this.listenTo(fieldView, 'edit', (view) => {
|
||
if (fieldInEditMode && fieldInEditMode.isEditMode()) {
|
||
fieldInEditMode.inlineEditClose();
|
||
}
|
||
|
||
fieldInEditMode = view;
|
||
});
|
||
|
||
this.listenTo(fieldView, 'inline-edit-on', () => {
|
||
this.inlineEditModeIsOn = true;
|
||
});
|
||
|
||
this.listenTo(fieldView, 'inline-edit-off', (o) => {
|
||
o = o || {};
|
||
|
||
if (o.all) {
|
||
return;
|
||
}
|
||
|
||
this.inlineEditModeIsOn = false;
|
||
|
||
this.setIsNotChanged();
|
||
});
|
||
|
||
this.listenTo(fieldView, 'after:inline-edit-off', o => {
|
||
if (this.updatedAttributes && !o.noReset) {
|
||
this.resetModelChanges();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/** @private */
|
||
initStickableButtonsContainer() {
|
||
const helper = new StickyBarHelper(
|
||
this,
|
||
this.stickButtonsFormBottomSelector,
|
||
this.stickButtonsContainerAllTheWay,
|
||
this.numId
|
||
);
|
||
|
||
helper.init();
|
||
}
|
||
|
||
fetch() {
|
||
let data = super.fetch();
|
||
|
||
if (this.hasView('side')) {
|
||
const view = this.getSideView();
|
||
|
||
if ('fetch' in view) {
|
||
data = _.extend(data, view.fetch());
|
||
}
|
||
}
|
||
|
||
if (this.hasView('bottom')) {
|
||
const view = this.getBottomView();
|
||
|
||
if ('fetch' in view) {
|
||
data = _.extend(data, view.fetch());
|
||
}
|
||
}
|
||
|
||
return data;
|
||
}
|
||
|
||
setEditMode() {
|
||
this.trigger('before:set-edit-mode');
|
||
|
||
this.inlineEditModeIsOn = false;
|
||
|
||
this.$el.find('.record-buttons').addClass('hidden');
|
||
this.$el.find('.edit-buttons').removeClass('hidden');
|
||
|
||
return new Promise(resolve => {
|
||
const fields = this.getFieldViews(true);
|
||
|
||
const promiseList = [];
|
||
|
||
for (const field in fields) {
|
||
const fieldView = fields[field];
|
||
|
||
if (fieldView.readOnly) {
|
||
continue;
|
||
}
|
||
|
||
if (fieldView.isEditMode()) {
|
||
fieldView.fetchToModel();
|
||
fieldView.removeInlineEditLinks();
|
||
fieldView.setIsInlineEditMode(false);
|
||
}
|
||
|
||
promiseList.push(
|
||
fieldView
|
||
.setEditMode()
|
||
.then(() => {
|
||
return fieldView.render();
|
||
})
|
||
);
|
||
}
|
||
|
||
this.mode = this.MODE_EDIT;
|
||
|
||
this.trigger('after:set-edit-mode');
|
||
this.trigger('after:mode-change', this.MODE_EDIT);
|
||
|
||
Promise.all(promiseList).then(() => resolve());
|
||
});
|
||
}
|
||
|
||
setDetailMode() {
|
||
this.trigger('before:set-detail-mode');
|
||
|
||
this.$el.find('.edit-buttons').addClass('hidden');
|
||
this.$el.find('.record-buttons').removeClass('hidden');
|
||
|
||
this.inlineEditModeIsOn = false;
|
||
|
||
return new Promise(resolve => {
|
||
const fields = this.getFieldViews(true);
|
||
|
||
const promiseList = [];
|
||
|
||
for (const field in fields) {
|
||
const fieldView = fields[field];
|
||
|
||
if (!fieldView.isDetailMode()) {
|
||
if (fieldView.isEditMode()) {
|
||
fieldView.trigger('inline-edit-off', {
|
||
all: true,
|
||
});
|
||
}
|
||
|
||
promiseList.push(
|
||
fieldView
|
||
.setDetailMode()
|
||
.then(() => fieldView.render())
|
||
);
|
||
}
|
||
}
|
||
|
||
this.mode = this.MODE_DETAIL;
|
||
|
||
this.trigger('after:set-detail-mode');
|
||
this.trigger('after:mode-change', this.MODE_DETAIL);
|
||
|
||
Promise.all(promiseList).then(() => resolve());
|
||
});
|
||
}
|
||
|
||
cancelEdit() {
|
||
this.resetModelChanges();
|
||
|
||
this.setDetailMode();
|
||
this.setIsNotChanged();
|
||
}
|
||
|
||
/**
|
||
* Whether in edit mode.
|
||
* @return {boolean}
|
||
*/
|
||
isEditMode() {
|
||
return this.mode === 'edit';
|
||
}
|
||
|
||
resetModelChanges() {
|
||
let skipReRender = true;
|
||
|
||
if (this.updatedAttributes) {
|
||
this.attributes = this.updatedAttributes;
|
||
this.updatedAttributes = null;
|
||
|
||
skipReRender = false;
|
||
}
|
||
|
||
const attributes = this.model.attributes;
|
||
|
||
for (const attr in attributes) {
|
||
if (!(attr in this.attributes)) {
|
||
this.model.unset(attr);
|
||
}
|
||
}
|
||
|
||
this.model.set(this.attributes, {
|
||
skipReRenderInEditMode: skipReRender,
|
||
action: 'cancel-edit',
|
||
});
|
||
}
|
||
|
||
delete() {
|
||
this.confirm({
|
||
message: this.translate('removeRecordConfirmation', 'messages', this.scope),
|
||
confirmText: this.translate('Remove'),
|
||
}, () => {
|
||
this.trigger('before:delete');
|
||
this.trigger('delete');
|
||
|
||
Espo.Ui.notifyWait();
|
||
|
||
const collection = this.model.collection;
|
||
|
||
this.model
|
||
.destroy({wait: true})
|
||
.then(() => {
|
||
if (collection) {
|
||
if (collection.total > 0) {
|
||
collection.total--;
|
||
}
|
||
}
|
||
|
||
this.model.set('deleted', true, {silent: true});
|
||
|
||
Espo.Ui.success(this.translate('Removed'), {suppress: true});
|
||
|
||
this.trigger('after:delete');
|
||
this.exit('delete');
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Get field views.
|
||
*
|
||
* @param {boolean} [withHidden] With hidden.
|
||
* @return {Object.<string, module:views/fields/base>}
|
||
*/
|
||
getFieldViews(withHidden) {
|
||
const fields = {};
|
||
|
||
if (this.hasView('middle')) {
|
||
if ('getFieldViews' in this.getMiddleView()) {
|
||
_.extend(fields, Espo.Utils.clone(this.getMiddleView().getFieldViews()));
|
||
}
|
||
}
|
||
|
||
if (this.hasView('side')) {
|
||
if ('getFieldViews' in this.getSideView()) {
|
||
_.extend(fields, this.getSideView().getFieldViews(withHidden));
|
||
}
|
||
}
|
||
|
||
if (this.hasView('bottom')) {
|
||
if ('getFieldViews' in this.getBottomView()) {
|
||
_.extend(fields, this.getBottomView().getFieldViews(withHidden));
|
||
}
|
||
}
|
||
|
||
return fields;
|
||
}
|
||
|
||
/**
|
||
* Get a field view.
|
||
*
|
||
* @param {string} name A field name.
|
||
* @return {module:views/fields/base|null}
|
||
*/
|
||
getFieldView(name) {
|
||
let view;
|
||
|
||
if (this.hasView('middle')) {
|
||
view = (this.getMiddleView().getFieldViews() || {})[name];
|
||
}
|
||
|
||
if (!view && this.hasView('side')) {
|
||
view = (this.getSideView().getFieldViews(true) || {})[name];
|
||
}
|
||
|
||
if (!view && this.hasView('bottom')) {
|
||
view = (this.getBottomView().getFieldViews(true) || {})[name];
|
||
}
|
||
|
||
return view || null;
|
||
}
|
||
|
||
// @todo Remove.
|
||
handleDataBeforeRender(data) {}
|
||
|
||
data() {
|
||
let navigateButtonsEnabled = !this.navigateButtonsDisabled && !!this.model.collection;
|
||
|
||
let previousButtonEnabled = false;
|
||
let nextButtonEnabled = false;
|
||
|
||
if (navigateButtonsEnabled) {
|
||
if (this.indexOfRecord > 0 || this.model.collection.offset) {
|
||
previousButtonEnabled = true;
|
||
}
|
||
|
||
const total = this.model.collection.total !== undefined ?
|
||
this.model.collection.total : this.model.collection.length;
|
||
|
||
if (this.indexOfRecord < total - 1 - this.model.collection.offset) {
|
||
nextButtonEnabled = true;
|
||
} else {
|
||
if (total === -1) {
|
||
nextButtonEnabled = true;
|
||
} else if (total === -2) {
|
||
if (this.indexOfRecord < this.model.collection.length - 1 - this.model.collection.offset) {
|
||
nextButtonEnabled = true;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!previousButtonEnabled && !nextButtonEnabled) {
|
||
navigateButtonsEnabled = false;
|
||
}
|
||
}
|
||
|
||
const hasMiddleTabs = this.hasTabs();
|
||
const middleTabDataList = hasMiddleTabs ? this.getMiddleTabDataList() : [];
|
||
|
||
return {
|
||
scope: this.scope,
|
||
entityType: this.entityType,
|
||
buttonList: this.buttonList,
|
||
buttonEditList: this.buttonEditList,
|
||
dropdownItemList: this.getDropdownItemDataList(),
|
||
dropdownEditItemList: this.dropdownEditItemList,
|
||
dropdownItemListEmpty: this.isDropdownItemListEmpty(),
|
||
dropdownEditItemListEmpty: this.isDropdownEditItemListEmpty(),
|
||
buttonsDisabled: this.buttonsDisabled,
|
||
id: this.id,
|
||
isWide: this.isWide,
|
||
isSmall: this.type === 'editSmall' || this.type === 'detailSmall',
|
||
navigateButtonsEnabled: navigateButtonsEnabled,
|
||
previousButtonEnabled: previousButtonEnabled,
|
||
nextButtonEnabled: nextButtonEnabled,
|
||
hasMiddleTabs: hasMiddleTabs,
|
||
middleTabDataList: middleTabDataList,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @return {Array<module:views/record/detail~dropdownItem|false>}
|
||
*/
|
||
getDropdownItemDataList() {
|
||
/** @type {Array<module:views/record/detail~dropdownItem[]>} */
|
||
const dropdownGroups = [];
|
||
|
||
this.dropdownItemList.forEach(item => {
|
||
// For bc.
|
||
if (item === false) {
|
||
return;
|
||
}
|
||
|
||
const index = (item.groupIndex === undefined ? 9999 : item.groupIndex) + 100;
|
||
|
||
if (dropdownGroups[index] === undefined) {
|
||
dropdownGroups[index] = [];
|
||
}
|
||
|
||
dropdownGroups[index].push(item);
|
||
});
|
||
|
||
const dropdownItemList = [];
|
||
|
||
dropdownGroups.forEach(list => {
|
||
list.forEach(it => dropdownItemList.push(it));
|
||
|
||
dropdownItemList.push(false);
|
||
});
|
||
|
||
return dropdownItemList;
|
||
}
|
||
|
||
init() {
|
||
this.entityType = this.model.entityType || this.model.name || 'Common';
|
||
this.scope = this.options.scope || this.entityType;
|
||
|
||
this.layoutName = this.options.layoutName || this.layoutName;
|
||
this.detailLayout = this.options.detailLayout || this.detailLayout;
|
||
|
||
this.type = this.options.type || this.type;
|
||
|
||
this.buttonList = this.options.buttonList || this.buttonList;
|
||
this.dropdownItemList = this.options.dropdownItemList || this.dropdownItemList;
|
||
|
||
this.buttonList = Espo.Utils.cloneDeep(this.buttonList);
|
||
this.buttonEditList = Espo.Utils.cloneDeep(this.buttonEditList);
|
||
this.dropdownItemList = Espo.Utils.cloneDeep(this.dropdownItemList);
|
||
this.dropdownEditItemList = Espo.Utils.cloneDeep(this.dropdownEditItemList);
|
||
|
||
this.returnAfterCreate = this.options.returnAfterCreate;
|
||
|
||
this.returnUrl = this.options.returnUrl || this.returnUrl;
|
||
this.returnDispatchParams = this.options.returnDispatchParams || this.returnDispatchParams;
|
||
|
||
this.exit = this.options.exit || this.exit;
|
||
|
||
if (this.shortcutKeys) {
|
||
this.shortcutKeys = Espo.Utils.cloneDeep(this.shortcutKeys);
|
||
}
|
||
}
|
||
|
||
isDropdownItemListEmpty() {
|
||
if (this.dropdownItemList.length === 0) {
|
||
return true;
|
||
}
|
||
|
||
let isEmpty = true;
|
||
|
||
this.dropdownItemList.forEach(item => {
|
||
if (!item.hidden) {
|
||
isEmpty = false;
|
||
}
|
||
});
|
||
|
||
return isEmpty;
|
||
}
|
||
|
||
isDropdownEditItemListEmpty() {
|
||
if (this.dropdownEditItemList.length === 0) {
|
||
return true;
|
||
}
|
||
|
||
let isEmpty = true;
|
||
|
||
this.dropdownEditItemList.forEach(item => {
|
||
if (!item.hidden) {
|
||
isEmpty = false;
|
||
}
|
||
});
|
||
|
||
return isEmpty;
|
||
}
|
||
|
||
setup() {
|
||
if (typeof this.model === 'undefined') {
|
||
throw new Error('Model has not been injected into record view.');
|
||
}
|
||
|
||
this.recordHelper = this.options.recordHelper ||
|
||
new ViewRecordHelper(this.defaultFieldStates, this.defaultPanelStates);
|
||
|
||
this._initInlineEditSave();
|
||
|
||
const collection = this.collection = this.model.collection;
|
||
|
||
if (collection) {
|
||
this.listenTo(this.model, 'destroy', () => {
|
||
collection.remove(this.model.id);
|
||
|
||
collection.trigger('sync', collection, {}, {});
|
||
});
|
||
|
||
if ('indexOfRecord' in this.options) {
|
||
this.indexOfRecord = this.options.indexOfRecord;
|
||
} else {
|
||
this.indexOfRecord = collection.indexOf(this.model);
|
||
}
|
||
}
|
||
|
||
/** @type {Object.<string,*>|null} */
|
||
this.middlePanelDefs = {};
|
||
|
||
/** @type {Object.<string,*>[]} */
|
||
this.middlePanelDefsList = [];
|
||
|
||
if (this.getUser().isPortal() && !this.portalLayoutDisabled) {
|
||
if (
|
||
this.getMetadata().get(['clientDefs', this.scope, 'additionalLayouts', this.layoutName + 'Portal'])
|
||
) {
|
||
this.layoutName += 'Portal';
|
||
}
|
||
}
|
||
|
||
this.numId = Math.floor((Math.random() * 10000) + 1);
|
||
|
||
// For testing purpose.
|
||
$(window).on('fetch-record.' + this.cid, () => this._webSocketDebounceHelper.process());
|
||
|
||
this.once('remove', () => {
|
||
if (this.isChanged) {
|
||
this.resetModelChanges();
|
||
}
|
||
this.setIsNotChanged();
|
||
|
||
$(window).off('scroll.detail-' + this.numId);
|
||
$(window).off('fetch-record.' + this.cid);
|
||
});
|
||
|
||
this.id = Espo.Utils.toDom(this.entityType) + '-' + Espo.Utils.toDom(this.type) + '-' + this.numId;
|
||
|
||
this.isNew = this.model.isNew();
|
||
|
||
if (!this.editModeDisabled) {
|
||
if ('editModeDisabled' in this.options) {
|
||
this.editModeDisabled = this.options.editModeDisabled;
|
||
}
|
||
}
|
||
|
||
this.confirmLeaveDisabled = this.options.confirmLeaveDisabled || this.confirmLeaveDisabled;
|
||
|
||
this.buttonsDisabled = this.options.buttonsDisabled || this.buttonsDisabled;
|
||
|
||
// for backward compatibility
|
||
// @todo remove
|
||
if ('buttonsPosition' in this.options && !this.options.buttonsPosition) {
|
||
this.buttonsDisabled = true;
|
||
}
|
||
|
||
if ('isWide' in this.options) {
|
||
this.isWide = this.options.isWide;
|
||
}
|
||
|
||
if ('sideView' in this.options) {
|
||
this.sideView = this.options.sideView;
|
||
}
|
||
|
||
if ('bottomView' in this.options) {
|
||
this.bottomView = this.options.bottomView;
|
||
}
|
||
|
||
this.sideDisabled = this.options.sideDisabled || this.sideDisabled;
|
||
this.bottomDisabled = this.options.bottomDisabled || this.bottomDisabled;
|
||
|
||
this.readOnly = this.options.readOnly || this.readOnly;
|
||
|
||
if (!this.readOnly && !this.isNew) {
|
||
this.readOnly = this.getMetadata().get(['clientDefs', this.scope, 'editDisabled']) || false;
|
||
}
|
||
|
||
if (this.getMetadata().get(['clientDefs', this.scope, 'createDisabled'])) {
|
||
this.duplicateAction = false;
|
||
}
|
||
|
||
if ((this.getConfig().get('currencyList') || []).length <= 1) {
|
||
this.convertCurrencyAction = false;
|
||
}
|
||
|
||
this.readOnlyLocked = this.readOnly;
|
||
|
||
this.inlineEditDisabled = this.inlineEditDisabled ||
|
||
this.getMetadata().get(['clientDefs', this.scope, 'inlineEditDisabled']) ||
|
||
false;
|
||
|
||
this.inlineEditDisabled = this.options.inlineEditDisabled || this.inlineEditDisabled;
|
||
this.navigateButtonsDisabled = this.options.navigateButtonsDisabled ||
|
||
this.navigateButtonsDisabled;
|
||
this.portalLayoutDisabled = this.options.portalLayoutDisabled || this.portalLayoutDisabled;
|
||
this.dynamicLogicDefs = this.options.dynamicLogicDefs || this.dynamicLogicDefs;
|
||
|
||
this.accessControlDisabled = this.options.accessControlDisabled || this.accessControlDisabled;
|
||
|
||
this.dataObject = this.options.dataObject || {};
|
||
this.rootData = this.options.rootData || {};
|
||
|
||
this.setupActionItems();
|
||
this.setupBeforeFinal();
|
||
this.setupDynamicBehavior();
|
||
|
||
this.on('after:render', () => {
|
||
this.initElementReferences();
|
||
});
|
||
|
||
this._webSocketDebounceHelper = new DebounceHelper({
|
||
interval: this._webSocketDebounceInterval,
|
||
handler: () => this.handleRecordUpdate(),
|
||
});
|
||
|
||
if (
|
||
!this.isNew &&
|
||
!!this.getHelper().webSocketManager &&
|
||
this.getMetadata().get(['scopes', this.entityType, 'object'])
|
||
) {
|
||
this.subscribeToWebSocket();
|
||
|
||
this.once('remove', () => {
|
||
if (this.isSubscribedToWebSocket) {
|
||
this.unsubscribeFromWebSocket();
|
||
}
|
||
});
|
||
}
|
||
|
||
this.wait(
|
||
this.getHelper().processSetupHandlers(this, this.setupHandlerType)
|
||
);
|
||
|
||
this.initInlineEditDynamicWithLogicInteroperability();
|
||
|
||
this.forcePatchAttributeDependencyMap =
|
||
this.getMetadata().get(['clientDefs', this.scope, 'forcePatchAttributeDependencyMap']) || {};
|
||
}
|
||
|
||
setupBeforeFinal() {
|
||
if (!this.accessControlDisabled) {
|
||
this.manageAccess();
|
||
}
|
||
|
||
this.attributes = this.model.getClonedAttributes();
|
||
|
||
if (this.options.attributes) {
|
||
this.model.set(this.options.attributes);
|
||
}
|
||
|
||
this.listenTo(this.model, 'sync', () => {
|
||
this.attributes = this.model.getClonedAttributes();
|
||
});
|
||
|
||
this.listenTo(this.model, 'change', (m, o) => {
|
||
if (o.sync) {
|
||
for (const attribute in m.attributes) {
|
||
if (!m.hasChanged(attribute)) {
|
||
continue;
|
||
}
|
||
|
||
this.attributes[attribute] = Espo.Utils.cloneDeep(
|
||
m.get(attribute)
|
||
);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
if (this.mode === this.MODE_EDIT || this.inlineEditModeIsOn) {
|
||
this.setIsChanged();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
*/
|
||
setupDynamicBehavior() {
|
||
const dependencyDefs =
|
||
Espo.Utils.clone(this.getMetadata().get(['clientDefs', this.entityType, 'formDependency']) || {});
|
||
|
||
// noinspection JSDeprecatedSymbols
|
||
this.dependencyDefs = _.extend(dependencyDefs, this.dependencyDefs);
|
||
|
||
this.initDependency();
|
||
|
||
const dynamicLogic = {...this.getMetadata().get(`logicDefs.${this.entityType}`, {})};
|
||
|
||
this.dynamicLogicDefs = _.extend(dynamicLogic, this.dynamicLogicDefs);
|
||
|
||
this.initDynamicLogic();
|
||
this.setupFieldLevelSecurity();
|
||
this.initDynamicHandler();
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
_initInlineEditSave() {
|
||
this.listenTo(this.recordHelper, 'inline-edit-save', (field, o) => {
|
||
this.inlineEditSave(field, o);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @param {string} field
|
||
* @param {module:views/record/base~saveOptions} [options]
|
||
*/
|
||
inlineEditSave(field, options) {
|
||
const view = this.getFieldView(field);
|
||
|
||
if (!view) {
|
||
throw new Error(`No field '${field}'.`);
|
||
}
|
||
|
||
options = _.extend({
|
||
inline: true,
|
||
field: field,
|
||
afterValidate: () => {
|
||
if (options.bypassClose) {
|
||
return;
|
||
}
|
||
|
||
view.inlineEditClose(true)
|
||
},
|
||
}, options || {});
|
||
|
||
this.save(options)
|
||
.then(() => {
|
||
view.trigger('after:inline-save');
|
||
view.trigger('after:save');
|
||
|
||
|
||
if (options.bypassClose) {
|
||
view.initialAttributes = this.model.getClonedAttributes();
|
||
}
|
||
})
|
||
.catch(reason => {
|
||
if (reason === 'notModified') {
|
||
if (options.bypassClose) {
|
||
return;
|
||
}
|
||
|
||
view.inlineEditClose(true);
|
||
|
||
return;
|
||
}
|
||
|
||
if (reason === 'error') {
|
||
if (options.bypassClose) {
|
||
return;
|
||
}
|
||
|
||
const initialAttributes = {...view.initialAttributes};
|
||
|
||
view.inlineEdit()
|
||
.then(() => view.initialAttributes = initialAttributes);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
initInlineEditDynamicWithLogicInteroperability() {
|
||
let blockEdit = false;
|
||
|
||
const process = (type, field) => {
|
||
if (!this.inlineEditModeIsOn || this.editModeDisabled) {
|
||
return;
|
||
}
|
||
|
||
if (blockEdit) {
|
||
return;
|
||
}
|
||
|
||
if (type === 'required') {
|
||
const fieldView = this.getFieldView(field);
|
||
|
||
if (fieldView.validateRequired) {
|
||
fieldView.suspendValidationMessage();
|
||
|
||
try {
|
||
if (!fieldView.validateRequired()) {
|
||
return;
|
||
}
|
||
}
|
||
catch (e) {}
|
||
}
|
||
}
|
||
|
||
blockEdit = true;
|
||
|
||
setTimeout(() => blockEdit = false, 300);
|
||
|
||
setTimeout(() => {
|
||
this.setEditMode();
|
||
|
||
this.getFieldViewList()
|
||
.forEach(view => view.removeInlineEditLinks());
|
||
}, 10);
|
||
};
|
||
|
||
this.on('set-field-required', field => process('required', field));
|
||
this.on('set-field-option-list', field => process('options', field));
|
||
this.on('reset-field-option-list', field => process('options', field));
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
initDynamicHandler() {
|
||
const dynamicHandlerClassName = this.dynamicHandlerClassName ||
|
||
this.getMetadata().get(['clientDefs', this.scope, 'dynamicHandler']);
|
||
|
||
const init = /** import('dynamic-handler').default */dynamicHandler => {
|
||
this.listenTo(this.model, 'change', (model, o) => {
|
||
if ('onChange' in dynamicHandler) {
|
||
dynamicHandler.onChange.call(dynamicHandler, model, o);
|
||
}
|
||
|
||
const changedAttributes = model.changedAttributes();
|
||
|
||
for (const attribute in changedAttributes) {
|
||
const methodName = 'onChange' + Espo.Utils.upperCaseFirst(attribute);
|
||
|
||
if (methodName in dynamicHandler) {
|
||
dynamicHandler[methodName]
|
||
.call(dynamicHandler, model, changedAttributes[attribute], o);
|
||
}
|
||
}
|
||
});
|
||
|
||
if ('init' in dynamicHandler) {
|
||
dynamicHandler.init();
|
||
}
|
||
};
|
||
|
||
if (dynamicHandlerClassName) {
|
||
this.wait(
|
||
new Promise(resolve => {
|
||
Espo.loader.require(dynamicHandlerClassName, DynamicHandler => {
|
||
const dynamicHandler = this.dynamicHandler = new DynamicHandler(this);
|
||
|
||
init(dynamicHandler);
|
||
|
||
resolve();
|
||
});
|
||
})
|
||
);
|
||
}
|
||
|
||
const handlerList = this.getMetadata().get(['clientDefs', this.scope, 'dynamicHandlerList']) || [];
|
||
|
||
if (handlerList.length) {
|
||
const self = this;
|
||
|
||
const promiseList = [];
|
||
|
||
handlerList.forEach((className) => {
|
||
promiseList.push(
|
||
new Promise(resolve => {
|
||
Espo.loader.require(className, DynamicHandler => {
|
||
resolve(new DynamicHandler(self));
|
||
});
|
||
})
|
||
);
|
||
});
|
||
|
||
this.wait(
|
||
Promise.all(promiseList).then(list => {
|
||
list.forEach((dynamicHandler) => {
|
||
init(dynamicHandler);
|
||
});
|
||
})
|
||
);
|
||
}
|
||
}
|
||
|
||
setupFinal() {
|
||
this.build();
|
||
|
||
if (this.shortcutKeys && this.options.shortcutKeysEnabled) {
|
||
this.events['keydown.record-detail'] = e => {
|
||
const key = Espo.Utils.getKeyFromKeyEvent(e);
|
||
|
||
if (typeof this.shortcutKeys[key] === 'function') {
|
||
this.shortcutKeys[key].call(this, e.originalEvent);
|
||
|
||
return;
|
||
}
|
||
|
||
const actionName = this.shortcutKeys[key];
|
||
|
||
if (!actionName) {
|
||
return;
|
||
}
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
const methodName = 'action' + Espo.Utils.upperCaseFirst(actionName);
|
||
|
||
if (typeof this[methodName] === 'function') {
|
||
this[methodName]();
|
||
|
||
return;
|
||
}
|
||
|
||
this[actionName]();
|
||
};
|
||
}
|
||
|
||
if (!this.options.focusForCreate) {
|
||
this.once('after:render', () => this.focusOnFirstDiv());
|
||
}
|
||
}
|
||
|
||
setIsChanged() {
|
||
this.isChanged = true;
|
||
|
||
if (this.confirmLeaveDisabled) {
|
||
return;
|
||
}
|
||
|
||
this.setConfirmLeaveOut(true);
|
||
}
|
||
|
||
setIsNotChanged() {
|
||
this.isChanged = false;
|
||
|
||
if (this.confirmLeaveDisabled) {
|
||
return;
|
||
}
|
||
|
||
this.setConfirmLeaveOut(false);
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
* @param {number} indexOfRecord
|
||
*/
|
||
switchToModelByIndex(indexOfRecord) {
|
||
const collection = this.model.collection || this.collection;
|
||
|
||
if (!collection) {
|
||
return;
|
||
}
|
||
|
||
const model = collection.at(indexOfRecord);
|
||
|
||
if (!model) {
|
||
console.error("Model is not found in collection by index.");
|
||
|
||
return;
|
||
}
|
||
|
||
const id = model.id;
|
||
const scope = this.entityType || this.scope;
|
||
|
||
this.getRouter().navigate(`#${scope}/view/${id}`, {trigger: false});
|
||
|
||
this.getRouter().dispatch(scope, 'view', {
|
||
id: id,
|
||
model: model,
|
||
indexOfRecord: indexOfRecord,
|
||
rootUrl: this.options.rootUrl,
|
||
});
|
||
}
|
||
|
||
actionPrevious() {
|
||
this.model.abortLastFetch();
|
||
|
||
if (!this.model.collection) {
|
||
return;
|
||
}
|
||
|
||
const collection = this.model.collection;
|
||
|
||
if (this.indexOfRecord <= 0 && !collection.offset) {
|
||
return;
|
||
}
|
||
|
||
if (
|
||
this.indexOfRecord === 0 &&
|
||
collection.offset > 0 &&
|
||
collection.maxSize
|
||
) {
|
||
collection.offset = Math.max(0, collection.offset - collection.maxSize);
|
||
|
||
collection.fetch()
|
||
.then(() => {
|
||
const indexOfRecord = collection.length - 1;
|
||
|
||
if (indexOfRecord < 0) {
|
||
return;
|
||
}
|
||
|
||
this.switchToModelByIndex(indexOfRecord);
|
||
});
|
||
|
||
return;
|
||
}
|
||
|
||
const indexOfRecord = this.indexOfRecord - 1;
|
||
|
||
this.switchToModelByIndex(indexOfRecord);
|
||
}
|
||
|
||
actionNext() {
|
||
this.model.abortLastFetch();
|
||
|
||
if (!this.model.collection) {
|
||
return;
|
||
}
|
||
|
||
const collection = this.model.collection;
|
||
|
||
if (!(this.indexOfRecord < collection.total - 1 - collection.offset) && collection.total >= 0) {
|
||
return;
|
||
}
|
||
|
||
if (collection.total === -2 && this.indexOfRecord >= collection.length - 1 - collection.offset) {
|
||
return;
|
||
}
|
||
|
||
const indexOfRecord = this.indexOfRecord + 1;
|
||
|
||
if (indexOfRecord <= collection.length - 1 - collection.offset) {
|
||
this.switchToModelByIndex(indexOfRecord);
|
||
|
||
return;
|
||
}
|
||
|
||
collection
|
||
.fetch({
|
||
more: true,
|
||
remove: false,
|
||
})
|
||
.then(() => {
|
||
this.switchToModelByIndex(indexOfRecord);
|
||
});
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
actionViewPersonalData() {
|
||
this.createView('viewPersonalData', 'views/personal-data/modals/personal-data', {
|
||
model: this.model
|
||
}, view => {
|
||
view.render();
|
||
|
||
this.listenToOnce(view, 'erase', () => {
|
||
this.clearView('viewPersonalData');
|
||
this.model.fetch();
|
||
});
|
||
});
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
actionViewFollowers(data) {
|
||
const viewName = this.getMetadata().get(
|
||
['clientDefs', this.entityType, 'relationshipPanels', 'followers', 'viewModalView']
|
||
) ||
|
||
this.getMetadata().get(['clientDefs', 'User', 'modalViews', 'relatedList']) ||
|
||
'views/modals/followers-list';
|
||
|
||
const selectDisabled =
|
||
!this.getUser().isAdmin() &&
|
||
this.getAcl().getPermissionLevel('followerManagementPermission') === 'no' &&
|
||
this.getAcl().getPermissionLevel('portalPermission') === 'no';
|
||
|
||
const options = {
|
||
model: this.model,
|
||
link: 'followers',
|
||
scope: 'User',
|
||
title: this.translate('Followers'),
|
||
filtersDisabled: true,
|
||
url: this.entityType + '/' + this.model.id + '/followers',
|
||
createDisabled: true,
|
||
selectDisabled: selectDisabled,
|
||
rowActionsView: 'views/user/record/row-actions/relationship-followers',
|
||
};
|
||
|
||
if (data.viewOptions) {
|
||
for (const item in data.viewOptions) {
|
||
options[item] = data.viewOptions[item];
|
||
}
|
||
}
|
||
|
||
Espo.Ui.notifyWait();
|
||
|
||
this.createView('modalRelatedList', viewName, options, view => {
|
||
Espo.Ui.notify(false);
|
||
|
||
view.render();
|
||
|
||
this.listenTo(view, 'action', (event, element) => {
|
||
Espo.Utils.handleAction(this, event, element);
|
||
});
|
||
|
||
this.listenToOnce(view, 'close', () => {
|
||
this.clearView('modalRelatedList');
|
||
});
|
||
|
||
view.listenTo(this.model, 'after:relate:followers', () => {
|
||
this.model.fetch();
|
||
});
|
||
|
||
view.listenTo(this.model, 'after:unrelate:followers', () => {
|
||
this.model.fetch();
|
||
});
|
||
});
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
async actionPrintPdf() {
|
||
const view = new SelectTemplateModalView({
|
||
entityType: this.entityType,
|
||
onSelect: models => {
|
||
const model = models[0];
|
||
|
||
const url = `?entryPoint=pdf&entityType=${this.entityType}&entityId=${this.model.id}` +
|
||
`&templateId=${model.id}`;
|
||
|
||
window.open(url, '_blank');
|
||
},
|
||
});
|
||
|
||
await this.assignView('modal', view);
|
||
|
||
await view.render();
|
||
}
|
||
|
||
afterSave() {
|
||
if (this.isNew) {
|
||
Espo.Ui.success(this.translate('Created'));
|
||
} else {
|
||
Espo.Ui.success(this.translate('Saved'));
|
||
}
|
||
|
||
this.enableActionItems();
|
||
|
||
this.setIsNotChanged();
|
||
|
||
setTimeout(() => {
|
||
this.unblockUpdateWebSocket();
|
||
}, this.blockUpdateWebSocketPeriod);
|
||
}
|
||
|
||
beforeSave() {
|
||
Espo.Ui.notify(this.translate('saving', 'messages'));
|
||
|
||
this.blockUpdateWebSocket();
|
||
}
|
||
|
||
beforeBeforeSave() {
|
||
this.disableActionItems();
|
||
}
|
||
|
||
afterSaveError() {
|
||
this.enableActionItems();
|
||
}
|
||
|
||
afterNotModified() {
|
||
const msg = this.translate('notModified', 'messages');
|
||
|
||
Espo.Ui.warning(msg);
|
||
|
||
this.enableActionItems();
|
||
this.setIsNotChanged();
|
||
}
|
||
|
||
afterNotValid() {
|
||
Espo.Ui.error(this.translate('Not valid'))
|
||
|
||
this.enableActionItems();
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
* @param duplicates
|
||
* @param {Object} options
|
||
* @param {function} resolve
|
||
* @param {function} reject
|
||
* @return {boolean}
|
||
*/
|
||
errorHandlerDuplicate(duplicates, options, resolve, reject) {
|
||
Espo.Ui.notify(false);
|
||
|
||
this.createView('duplicate', 'views/modals/duplicate', {
|
||
scope: this.entityType,
|
||
duplicates: duplicates,
|
||
model: this.model,
|
||
}, view => {
|
||
view.render();
|
||
|
||
this.listenToOnce(view, 'save', () => {
|
||
this.actionSave({
|
||
options: {
|
||
headers: {
|
||
'X-Skip-Duplicate-Check': 'true',
|
||
}
|
||
}
|
||
})
|
||
.then(() => resolve())
|
||
.catch(() => reject('error'))
|
||
});
|
||
|
||
this.listenToOnce(view, 'cancel', () => reject('cancel'));
|
||
});
|
||
|
||
return true;
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
errorHandlerModified(data, options) {
|
||
Espo.Ui.notify(false);
|
||
|
||
const versionNumber = data.versionNumber;
|
||
const values = data.values || {};
|
||
|
||
const attributeList = Object.keys(values);
|
||
|
||
const diffAttributeList = [];
|
||
|
||
attributeList.forEach(attribute => {
|
||
if (this.attributes[attribute] !== values[attribute]) {
|
||
diffAttributeList.push(attribute);
|
||
}
|
||
});
|
||
|
||
if (diffAttributeList.length === 0) {
|
||
setTimeout(() => {
|
||
this.model.set('versionNumber', versionNumber, {silent: true});
|
||
this.attributes.versionNumber = versionNumber;
|
||
|
||
if (options.inline && options.field) {
|
||
this.inlineEditSave(options.field);
|
||
|
||
return;
|
||
}
|
||
|
||
this.actionSave();
|
||
}, 5);
|
||
|
||
return;
|
||
}
|
||
|
||
this.createView(
|
||
'dialog',
|
||
'views/modals/resolve-save-conflict',
|
||
{
|
||
model: this.model,
|
||
attributeList: diffAttributeList,
|
||
currentAttributes: Espo.Utils.cloneDeep(this.model.attributes),
|
||
originalAttributes: Espo.Utils.cloneDeep(this.attributes),
|
||
actualAttributes: Espo.Utils.cloneDeep(values),
|
||
}
|
||
)
|
||
.then(view => {
|
||
view.render();
|
||
|
||
this.listenTo(view, 'resolve', () => {
|
||
this.model.set('versionNumber', versionNumber, {silent: true});
|
||
this.attributes.versionNumber = versionNumber;
|
||
|
||
for (const attribute in values) {
|
||
this.setInitialAttributeValue(attribute, values[attribute]);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Get a middle view.
|
||
*
|
||
* @return {module:views/record/detail-middle}
|
||
*/
|
||
getMiddleView() {
|
||
return this.getView('middle');
|
||
}
|
||
|
||
/**
|
||
* Get a side view.
|
||
*
|
||
* @protected
|
||
* @return {module:views/record/detail-side}
|
||
*/
|
||
getSideView() {
|
||
return this.getView('side');
|
||
}
|
||
|
||
/**
|
||
* Get a bottom view.
|
||
*
|
||
* @protected
|
||
* @return {module:views/record/detail-bottom}
|
||
*/
|
||
getBottomView() {
|
||
return this.getView('bottom');
|
||
}
|
||
|
||
setReadOnly() {
|
||
if (!this.readOnlyLocked) {
|
||
this.readOnly = true;
|
||
}
|
||
|
||
const bottomView = this.getBottomView();
|
||
|
||
if (bottomView && 'setReadOnly' in bottomView) {
|
||
bottomView.setReadOnly();
|
||
}
|
||
|
||
const sideView = this.getSideView();
|
||
|
||
if (sideView && 'setReadOnly' in sideView) {
|
||
sideView.setReadOnly();
|
||
}
|
||
|
||
this.getFieldList().forEach((field) => {
|
||
this.setFieldReadOnly(field);
|
||
});
|
||
}
|
||
|
||
setNotReadOnly(onlyNotSetAsReadOnly) {
|
||
if (!this.readOnlyLocked) {
|
||
this.readOnly = false;
|
||
}
|
||
|
||
const bottomView = this.getBottomView();
|
||
|
||
if (bottomView && 'setNotReadOnly' in bottomView) {
|
||
bottomView.setNotReadOnly(onlyNotSetAsReadOnly);
|
||
}
|
||
|
||
const sideView = this.getSideView();
|
||
|
||
if (sideView && 'setNotReadOnly' in sideView) {
|
||
sideView.setNotReadOnly(onlyNotSetAsReadOnly);
|
||
}
|
||
|
||
this.getFieldList().forEach((field) => {
|
||
if (onlyNotSetAsReadOnly) {
|
||
if (this.recordHelper.getFieldStateParam(field, 'readOnly')) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
this.setFieldNotReadOnly(field);
|
||
});
|
||
}
|
||
|
||
manageAccessEdit(second) {
|
||
if (this.isNew) {
|
||
return;
|
||
}
|
||
|
||
const editAccess = this.getAcl().checkModel(this.model, 'edit', true);
|
||
|
||
if (!editAccess || this.readOnlyLocked) {
|
||
this.readOnly = true;
|
||
|
||
this.hideActionItem('edit');
|
||
|
||
if (this.selfAssignAction) {
|
||
this.hideActionItem('selfAssign');
|
||
}
|
||
} else {
|
||
this.showActionItem('edit');
|
||
|
||
if (this.selfAssignAction) {
|
||
this.hideActionItem('selfAssign');
|
||
|
||
if (this.model.has('assignedUserId')) {
|
||
if (!this.model.get('assignedUserId')) {
|
||
this.showActionItem('selfAssign');
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!this.readOnlyLocked) {
|
||
if (this.readOnly && second) {
|
||
if (this.isReady) {
|
||
this.setNotReadOnly(true);
|
||
} else {
|
||
this.on('ready', () => this.setNotReadOnly(true));
|
||
}
|
||
}
|
||
|
||
this.readOnly = false;
|
||
}
|
||
}
|
||
|
||
if (editAccess === null) {
|
||
this.listenToOnce(this.model, 'sync', () => {
|
||
this.model.trigger('acl-edit-ready');
|
||
|
||
this.manageAccessEdit(true);
|
||
});
|
||
}
|
||
}
|
||
|
||
manageAccessDelete() {
|
||
if (this.isNew) {
|
||
return;
|
||
}
|
||
|
||
const deleteAccess = this.getAcl().checkModel(this.model, 'delete', true);
|
||
|
||
if (!deleteAccess) {
|
||
this.hideActionItem('delete');
|
||
} else {
|
||
this.showActionItem('delete');
|
||
}
|
||
|
||
if (deleteAccess === null) {
|
||
this.listenToOnce(this.model, 'sync', () => {
|
||
this.manageAccessDelete(true);
|
||
});
|
||
}
|
||
}
|
||
|
||
manageAccessStream() {
|
||
if (this.isNew) {
|
||
return;
|
||
}
|
||
|
||
if (
|
||
~['no', 'own'].indexOf(this.getAcl().getLevel('User', 'read'))
|
||
&&
|
||
this.getAcl().getPermissionLevel('portalPermission') === 'no'
|
||
) {
|
||
this.hideActionItem('viewFollowers');
|
||
|
||
return;
|
||
}
|
||
|
||
const streamAccess = this.getAcl().checkModel(this.model, 'stream', true);
|
||
|
||
if (!streamAccess) {
|
||
this.hideActionItem('viewFollowers');
|
||
} else {
|
||
this.showActionItem('viewFollowers');
|
||
}
|
||
|
||
if (streamAccess === null) {
|
||
this.listenToOnce(this.model, 'sync', () => {
|
||
this.manageAccessStream(true);
|
||
});
|
||
}
|
||
}
|
||
|
||
manageAccess() {
|
||
this.manageAccessEdit();
|
||
this.manageAccessDelete();
|
||
this.manageAccessStream();
|
||
}
|
||
|
||
/**
|
||
* Add a button.
|
||
*
|
||
* @param {module:views/record/detail~button} o
|
||
* @param {boolean} [toBeginning]
|
||
*/
|
||
addButton(o, toBeginning) {
|
||
const name = o.name;
|
||
|
||
if (!name) {
|
||
return;
|
||
}
|
||
|
||
for (const item of this.buttonList) {
|
||
if (item.name === name) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
toBeginning ?
|
||
this.buttonList.unshift(o) :
|
||
this.buttonList.push(o);
|
||
}
|
||
|
||
/**
|
||
* Add a dropdown item.
|
||
*
|
||
* @param {module:views/record/detail~dropdownItem} o
|
||
* @param {boolean} [toBeginning]
|
||
*/
|
||
addDropdownItem(o, toBeginning) {
|
||
if (!o) {
|
||
// For bc.
|
||
return;
|
||
}
|
||
|
||
const name = o.name;
|
||
|
||
if (!name) {
|
||
return;
|
||
}
|
||
|
||
for (const item of this.dropdownItemList) {
|
||
if (item.name === name) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
toBeginning ?
|
||
this.dropdownItemList.unshift(o) :
|
||
this.dropdownItemList.push(o);
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
/**
|
||
* Add an 'edit' mode button.
|
||
*
|
||
* @param {module:views/record/detail~button} o
|
||
* @param {boolean} [toBeginning]
|
||
*/
|
||
addButtonEdit(o, toBeginning) {
|
||
const name = o.name;
|
||
|
||
if (!name) {
|
||
return;
|
||
}
|
||
|
||
for (const item of this.buttonEditList) {
|
||
if (item.name === name) {
|
||
return;
|
||
}
|
||
}
|
||
|
||
toBeginning ?
|
||
this.buttonEditList.unshift(o) :
|
||
this.buttonEditList.push(o);
|
||
}
|
||
|
||
/**
|
||
* @deprecated Use `enableActionItems`.
|
||
*/
|
||
enableButtons() {
|
||
this.allActionItemsDisabled = false;
|
||
|
||
this.$el.find(".button-container .actions-btn-group .action")
|
||
.removeAttr('disabled')
|
||
.removeClass('disabled');
|
||
|
||
this.$el.find(".button-container .actions-btn-group .dropdown-toggle")
|
||
.removeAttr('disabled')
|
||
.removeClass('disabled');
|
||
|
||
this.buttonList
|
||
.filter(item => item.disabled)
|
||
.forEach(item => {
|
||
this.$detailButtonContainer
|
||
.find(`button.action[data-action="${item.name}"]`)
|
||
.addClass('disabled')
|
||
.attr('disabled', 'disabled');
|
||
});
|
||
|
||
this.buttonEditList
|
||
.filter(item => item.disabled)
|
||
.forEach(item => {
|
||
this.$detailButtonContainer
|
||
.find(`button.action[data-action="${item.name}"]`)
|
||
.addClass('disabled')
|
||
.attr('disabled', 'disabled');
|
||
});
|
||
|
||
this.dropdownItemList
|
||
.filter(item => item.disabled)
|
||
.forEach(item => {
|
||
this.$detailButtonContainer
|
||
.find(`li > .action[data-action="${item.name}"]`)
|
||
.parent()
|
||
.addClass('disabled')
|
||
.attr('disabled', 'disabled');
|
||
});
|
||
|
||
this.dropdownEditItemList
|
||
.filter(item => item.disabled)
|
||
.forEach(item => {
|
||
this.$detailButtonContainer
|
||
.find(`li > .action[data-action="${item.name}"]`)
|
||
.parent()
|
||
.addClass('disabled')
|
||
.attr('disabled', 'disabled');
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @deprecated Use `disableActionItems`.
|
||
*/
|
||
disableButtons() {
|
||
this.allActionItemsDisabled = true;
|
||
|
||
this.$el.find(".button-container .actions-btn-group .action")
|
||
.attr('disabled', 'disabled')
|
||
.addClass('disabled');
|
||
|
||
this.$el.find(".button-container .actions-btn-group .dropdown-toggle")
|
||
.attr('disabled', 'disabled')
|
||
.addClass('disabled');
|
||
}
|
||
|
||
/**
|
||
* Remove a button or dropdown item.
|
||
*
|
||
* @param {string} name A name.
|
||
*/
|
||
removeActionItem(name) {
|
||
// noinspection JSDeprecatedSymbols
|
||
this.removeButton(name);
|
||
}
|
||
|
||
/**
|
||
* @deprecated Use `removeActionItem`.
|
||
*
|
||
* @param {string} name A name.
|
||
*/
|
||
removeButton(name) {
|
||
for (const [i, item] of this.buttonList.entries()) {
|
||
if (item.name === name) {
|
||
this.buttonList.splice(i, 1);
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
for (const [i, item] of this.dropdownItemList.entries()) {
|
||
if (item.name === name) {
|
||
this.dropdownItemList.splice(i, 1);
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!this.isRendered()) {
|
||
return;
|
||
}
|
||
|
||
const $container = this.$el.find('.detail-button-container');
|
||
|
||
const $action = $container.find(`ul > li > a.action[data-action="${name}"]`);
|
||
|
||
if ($action.length) {
|
||
$action.parent().remove();
|
||
|
||
$container.find(`ul > .divider:last-child`).remove();
|
||
|
||
return;
|
||
}
|
||
|
||
const $button = $container.find(`button.action[data-action="${name}"]`);
|
||
|
||
if ($button.length) {
|
||
$button.remove();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Convert a detail layout to an internal layout.
|
||
*
|
||
* @protected
|
||
* @param {module:views/record/detail~panelDefs[]} simplifiedLayout A detail layout.
|
||
* @return {Object[]}
|
||
*/
|
||
convertDetailLayout(simplifiedLayout) {
|
||
const layout = [];
|
||
const el = this.getSelector() || '#' + (this.id);
|
||
|
||
this.panelFieldListMap = {};
|
||
|
||
let tabNumber = -1;
|
||
|
||
for (let p = 0; p < simplifiedLayout.length; p++) {
|
||
const item = simplifiedLayout[p];
|
||
|
||
const panel = {};
|
||
|
||
const tabBreak = item.tabBreak || p === 0;
|
||
|
||
if (tabBreak) {
|
||
tabNumber++;
|
||
}
|
||
|
||
if ('customLabel' in item) {
|
||
panel.label = item.customLabel;
|
||
|
||
if (panel.label) {
|
||
panel.label = this.translate(panel.label, 'panelCustomLabels', this.entityType);
|
||
}
|
||
} else {
|
||
panel.label = item.label || null;
|
||
|
||
if (panel.label) {
|
||
panel.label = panel.label[0] === '$' ?
|
||
this.translate(panel.label.substring(1), 'panels', this.entityType) :
|
||
this.translate(panel.label, 'labels', this.entityType);
|
||
}
|
||
}
|
||
|
||
panel.name = item.name || 'panel-' + p.toString();
|
||
panel.style = item.style || 'default';
|
||
panel.rows = [];
|
||
panel.tabNumber = tabNumber;
|
||
panel.noteText = item.noteText;
|
||
panel.noteStyle = item.noteStyle || 'info';
|
||
|
||
if (panel.noteText) {
|
||
if (panel.noteText.startsWith('$') && !panel.noteText.includes(' ')) {
|
||
const label = panel.noteText.substring(1);
|
||
|
||
panel.noteText = this.translate(label, 'panelNotes', this.entityType);
|
||
}
|
||
|
||
panel.noteText = this.getHelper().transformMarkdownText(panel.noteText);
|
||
}
|
||
|
||
this.middlePanelDefs[panel.name] = {
|
||
name: panel.name,
|
||
style: panel.style,
|
||
tabNumber: panel.tabNumber,
|
||
tabBreak: tabBreak,
|
||
tabLabel: item.tabLabel,
|
||
};
|
||
|
||
this.middlePanelDefsList.push(this.middlePanelDefs[panel.name]);
|
||
|
||
// noinspection JSUnresolvedReference
|
||
if (item.dynamicLogicVisible && this.dynamicLogic) {
|
||
this.dynamicLogic.addPanelVisibleCondition(panel.name, item.dynamicLogicVisible);
|
||
}
|
||
|
||
// noinspection JSUnresolvedReference
|
||
if (item.dynamicLogicStyled && this.dynamicLogic) {
|
||
this.dynamicLogic.addPanelStyledCondition(panel.name, item.dynamicLogicStyled);
|
||
}
|
||
|
||
// noinspection JSUnresolvedReference
|
||
if (item.hidden && tabNumber === 0) {
|
||
panel.hidden = true;
|
||
|
||
this.hidePanel(panel.name);
|
||
|
||
this.underShowMoreDetailPanelList = this.underShowMoreDetailPanelList || [];
|
||
this.underShowMoreDetailPanelList.push(panel.name);
|
||
}
|
||
|
||
let lType = 'rows';
|
||
|
||
if (item.columns) {
|
||
lType = 'columns';
|
||
|
||
panel.columns = [];
|
||
}
|
||
|
||
if (panel.name) {
|
||
this.panelFieldListMap[panel.name] = [];
|
||
}
|
||
|
||
for (const [i, itemI] of item[lType].entries()) {
|
||
const row = [];
|
||
|
||
for (const cellDefs of itemI) {
|
||
if (cellDefs === false) {
|
||
row.push(false);
|
||
|
||
continue;
|
||
}
|
||
|
||
let view = cellDefs.view;
|
||
let name = cellDefs.name;
|
||
|
||
if (!name && view && typeof view === 'object') {
|
||
name = view.name;
|
||
}
|
||
|
||
if (!name) {
|
||
console.warn(`No 'name' specified in detail layout cell.`);
|
||
|
||
continue;
|
||
}
|
||
|
||
let selector;
|
||
|
||
if (view && typeof view === 'object') {
|
||
view.model = this.model;
|
||
view.mode = this.fieldsMode;
|
||
|
||
if (this.readOnly) {
|
||
view.setReadOnly();
|
||
}
|
||
|
||
selector = `.field[data-name="${name}"]`;
|
||
}
|
||
|
||
if (panel.name) {
|
||
this.panelFieldListMap[panel.name].push(name);
|
||
}
|
||
|
||
const type = cellDefs.type || this.model.getFieldType(name) || 'base';
|
||
|
||
view = view ||
|
||
this.model.getFieldParam(name, 'view') ||
|
||
this.getFieldManager().getViewName(type);
|
||
|
||
const o = {
|
||
fullSelector: el + ' .middle .field[data-name="' + name + '"]',
|
||
defs: {
|
||
name: name,
|
||
params: cellDefs.params || {},
|
||
},
|
||
mode: this.fieldsMode,
|
||
};
|
||
|
||
if (this.readOnly) {
|
||
o.readOnly = true;
|
||
}
|
||
|
||
if (cellDefs.readOnly) {
|
||
o.readOnly = true;
|
||
o.readOnlyLocked = true;
|
||
}
|
||
|
||
if (this.readOnlyLocked) {
|
||
o.readOnlyLocked = true;
|
||
}
|
||
|
||
if (this.inlineEditDisabled || cellDefs.inlineEditDisabled) {
|
||
o.inlineEditDisabled = true;
|
||
}
|
||
|
||
// noinspection JSUnresolvedReference
|
||
let fullWidth = cellDefs.fullWidth || false;
|
||
|
||
if (!fullWidth) {
|
||
if (item[lType][i].length === 1) {
|
||
fullWidth = true;
|
||
}
|
||
}
|
||
|
||
if (this.recordHelper.getFieldStateParam(name, 'hidden')) {
|
||
o.disabled = true;
|
||
}
|
||
|
||
if (this.recordHelper.getFieldStateParam(name, 'hiddenLocked')) {
|
||
o.disabledLocked = true;
|
||
}
|
||
|
||
if (this.recordHelper.getFieldStateParam(name, 'readOnly')) {
|
||
o.readOnly = true;
|
||
}
|
||
|
||
if (!o.readOnlyLocked && this.recordHelper.getFieldStateParam(name, 'readOnlyLocked')) {
|
||
o.readOnlyLocked = true;
|
||
}
|
||
|
||
if (this.recordHelper.getFieldStateParam(name, 'required') !== null) {
|
||
o.defs.params = o.defs.params || {};
|
||
o.defs.params.required = this.recordHelper.getFieldStateParam(name, 'required');
|
||
}
|
||
|
||
if (this.recordHelper.hasFieldOptionList(name)) {
|
||
o.customOptionList = this.recordHelper.getFieldOptionList(name);
|
||
}
|
||
|
||
o.validateCallback = () => this.validateField(name);
|
||
|
||
o.recordHelper = this.recordHelper;
|
||
o.dataObject = this.dataObject;
|
||
|
||
if (cellDefs.options) {
|
||
for (const optionName in cellDefs.options) {
|
||
if (typeof o[optionName] !== 'undefined') {
|
||
continue;
|
||
}
|
||
|
||
o[optionName] = cellDefs.options[optionName];
|
||
}
|
||
}
|
||
|
||
const cell = {
|
||
name: name + 'Field',
|
||
view: view,
|
||
field: name,
|
||
fullSelector: el + ' .middle .field[data-name="' + name + '"]',
|
||
fullWidth: fullWidth,
|
||
options: o,
|
||
};
|
||
|
||
if (selector) {
|
||
cell.selector = selector;
|
||
}
|
||
|
||
if ('labelText' in cellDefs) {
|
||
o.labelText = cellDefs.labelText;
|
||
cell.customLabel = cellDefs.labelText;
|
||
}
|
||
|
||
if ('customLabel' in cellDefs) {
|
||
cell.customLabel = cellDefs.customLabel;
|
||
}
|
||
|
||
if ('label' in cellDefs) {
|
||
cell.label = cellDefs.label;
|
||
}
|
||
|
||
if (
|
||
view &&
|
||
typeof view === 'object' &&
|
||
!cell.customLabel &&
|
||
!cell.label &&
|
||
view.getLabelText()
|
||
) {
|
||
cell.customLabel = view.getLabelText();
|
||
}
|
||
|
||
if ('customCode' in cellDefs) {
|
||
cell.customCode = cellDefs.customCode;
|
||
}
|
||
|
||
if ('noLabel' in cellDefs) {
|
||
cell.noLabel = cellDefs.noLabel;
|
||
}
|
||
|
||
if ('span' in cellDefs) {
|
||
cell.span = cellDefs.span;
|
||
}
|
||
|
||
row.push(cell);
|
||
}
|
||
|
||
panel[lType].push(row);
|
||
}
|
||
|
||
layout.push(panel);
|
||
}
|
||
|
||
return layout;
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {function(Object[]): void}callback
|
||
*/
|
||
getGridLayout(callback) {
|
||
if (this.gridLayout !== null) {
|
||
callback(this.gridLayout);
|
||
|
||
return;
|
||
}
|
||
|
||
if (this.detailLayout) {
|
||
this.gridLayout = {
|
||
type: this.gridLayoutType,
|
||
layout: this.convertDetailLayout(this.detailLayout),
|
||
};
|
||
|
||
callback(this.gridLayout);
|
||
|
||
return;
|
||
}
|
||
|
||
this.getHelper().layoutManager.get(this.entityType, this.layoutName, detailLayout => {
|
||
if (typeof this.modifyDetailLayout === 'function') {
|
||
detailLayout = Espo.Utils.cloneDeep(detailLayout);
|
||
|
||
this.modifyDetailLayout(detailLayout);
|
||
}
|
||
|
||
this.detailLayout = detailLayout;
|
||
|
||
this.gridLayout = {
|
||
type: this.gridLayoutType,
|
||
layout: this.convertDetailLayout(this.detailLayout),
|
||
};
|
||
|
||
callback(this.gridLayout);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Create a side view.
|
||
*
|
||
* @protected
|
||
*/
|
||
createSideView() {
|
||
const el = this.getSelector() || '#' + (this.id);
|
||
|
||
this.createView('side', this.sideView, {
|
||
model: this.model,
|
||
scope: this.scope,
|
||
fullSelector: el + ' .side',
|
||
type: this.type,
|
||
readOnly: this.readOnly,
|
||
inlineEditDisabled: this.inlineEditDisabled,
|
||
recordHelper: this.recordHelper,
|
||
recordViewObject: this,
|
||
isReturn: this.options.isReturn,
|
||
dataObject: this.dataObject,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Create a middle view.
|
||
*
|
||
* @protected
|
||
*/
|
||
createMiddleView(callback) {
|
||
const el = this.getSelector() || '#' + (this.id);
|
||
|
||
this.waitForView('middle');
|
||
|
||
this.getGridLayout(layout => {
|
||
if (
|
||
this.hasTabs() &&
|
||
this.options.isReturn &&
|
||
this.isStoredTabForThisRecord()
|
||
) {
|
||
this.selectStoredTab();
|
||
}
|
||
|
||
this.createView('middle', this.middleView, {
|
||
model: this.model,
|
||
scope: this.scope,
|
||
type: this.type,
|
||
layoutDefs: layout,
|
||
fullSelector: el + ' .middle',
|
||
layoutData: {
|
||
model: this.model,
|
||
},
|
||
recordHelper: this.recordHelper,
|
||
recordViewObject: this,
|
||
panelFieldListMap: this.panelFieldListMap,
|
||
}, callback);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Create a bottom view.
|
||
*
|
||
* @protected
|
||
*/
|
||
createBottomView() {
|
||
const el = this.getSelector() || '#' + (this.id);
|
||
|
||
this.createView('bottom', this.bottomView, {
|
||
model: this.model,
|
||
scope: this.scope,
|
||
fullSelector: el + ' .bottom',
|
||
readOnly: this.readOnly,
|
||
type: this.type,
|
||
inlineEditDisabled: this.inlineEditDisabled,
|
||
recordHelper: this.recordHelper,
|
||
recordViewObject: this,
|
||
portalLayoutDisabled: this.portalLayoutDisabled,
|
||
isReturn: this.options.isReturn,
|
||
dataObject: this.dataObject,
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Create views.
|
||
*
|
||
* @protected
|
||
* @param {function(module:views/record/detail-middle): void} [callback]
|
||
*/
|
||
build(callback) {
|
||
if (!this.sideDisabled && this.sideView) {
|
||
this.createSideView();
|
||
}
|
||
|
||
if (this.middleView) {
|
||
this.createMiddleView(callback);
|
||
}
|
||
|
||
if (!this.bottomDisabled && this.bottomView) {
|
||
this.createBottomView();
|
||
}
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
/**
|
||
* Called after create.
|
||
*
|
||
* @return {boolean} True if redirecting is processed.
|
||
*/
|
||
exitAfterCreate() {
|
||
if (!this.returnAfterCreate && this.model.id) {
|
||
const url = '#' + this.scope + '/view/' + this.model.id;
|
||
|
||
this.getRouter().navigate(url, {trigger: false});
|
||
|
||
this.getRouter().dispatch(this.scope, 'view', {
|
||
id: this.model.id,
|
||
rootUrl: this.options.rootUrl,
|
||
model: this.model,
|
||
isAfterCreate: true,
|
||
});
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Called after save or cancel. By default, redirects a page. Can be overridden in options.
|
||
*
|
||
* @param {string|'create'|'save'|'cancel'|'delete'} [after] Name of an action after which #exit is invoked.
|
||
*/
|
||
exit(after) {
|
||
if (after) {
|
||
const methodName = 'exitAfter' + Espo.Utils.upperCaseFirst(after);
|
||
|
||
if (methodName in this) {
|
||
const result = this[methodName]();
|
||
|
||
if (result) {
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
let url;
|
||
let options;
|
||
|
||
if (this.returnUrl) {
|
||
url = this.returnUrl;
|
||
} else {
|
||
if (after === 'delete') {
|
||
url = this.options.rootUrl || '#' + this.scope;
|
||
|
||
if (this.options.rootUrl) {
|
||
this.getRouter().navigate(url, {trigger: true});
|
||
|
||
return;
|
||
}
|
||
|
||
this.getRouter().navigate(url, {trigger: false});
|
||
this.getRouter().dispatch(this.scope, null, {isReturn: true});
|
||
|
||
return;
|
||
}
|
||
|
||
if (this.model.id) {
|
||
url = `#${this.scope}/view/${this.model.id}`;
|
||
|
||
if (!this.returnDispatchParams) {
|
||
this.getRouter().navigate(url, {trigger: false});
|
||
|
||
options = {
|
||
id: this.model.id,
|
||
model: this.model,
|
||
};
|
||
|
||
if (this.options.rootUrl) {
|
||
options.rootUrl = this.options.rootUrl;
|
||
}
|
||
|
||
this.getRouter().dispatch(this.scope, 'view', options);
|
||
}
|
||
} else {
|
||
url = this.options.rootUrl || '#' + this.scope;
|
||
}
|
||
}
|
||
|
||
if (this.returnDispatchParams) {
|
||
const controller = this.returnDispatchParams.controller;
|
||
const action = this.returnDispatchParams.action;
|
||
options = this.returnDispatchParams.options || {};
|
||
|
||
this.getRouter().navigate(url, {trigger: false});
|
||
this.getRouter().dispatch(controller, action, options);
|
||
|
||
return;
|
||
}
|
||
|
||
this.getRouter().navigate(url, {trigger: true});
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
*/
|
||
subscribeToWebSocket() {
|
||
const topic = `recordUpdate.${this.entityType}.${this.model.id}`;
|
||
|
||
this.recordUpdateWebSocketTopic = topic;
|
||
this.isSubscribedToWebSocket = true;
|
||
|
||
this.getHelper().webSocketManager.subscribe(topic, () => this._webSocketDebounceHelper.process());
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
*/
|
||
unsubscribeFromWebSocket() {
|
||
if (!this.isSubscribedToWebSocket) {
|
||
return;
|
||
}
|
||
|
||
this.getHelper().webSocketManager.unsubscribe(this.recordUpdateWebSocketTopic);
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
async handleRecordUpdate() {
|
||
if (this.updateWebSocketIsBlocked) {
|
||
return;
|
||
}
|
||
|
||
if (this.inlineEditModeIsOn || this.mode === this.MODE_EDIT) {
|
||
const m = this.model.clone();
|
||
|
||
await m.fetch();
|
||
|
||
if (this.inlineEditModeIsOn || this.mode === this.MODE_EDIT) {
|
||
this.updatedAttributes = Espo.Utils.cloneDeep(m.attributes);
|
||
}
|
||
|
||
return;
|
||
}
|
||
|
||
await this.model.fetch({highlight: true});
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @param {boolean} [toUnblock]
|
||
*/
|
||
blockUpdateWebSocket(toUnblock = false) {
|
||
this.updateWebSocketIsBlocked = true;
|
||
|
||
if (toUnblock) {
|
||
setTimeout(() => {
|
||
this.unblockUpdateWebSocket();
|
||
}, this.blockUpdateWebSocketPeriod);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
unblockUpdateWebSocket() {
|
||
this.updateWebSocketIsBlocked = false;
|
||
}
|
||
|
||
/**
|
||
* Show more detail panels.
|
||
*/
|
||
showMoreDetailPanels() {
|
||
this.hidePanel('showMoreDelimiter');
|
||
|
||
this.underShowMoreDetailPanelList.forEach(item => {
|
||
this.showPanel(item);
|
||
});
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
/**
|
||
* @protected
|
||
* @return {Number}
|
||
*/
|
||
getTabCount() {
|
||
if (!this.hasTabs()) {
|
||
return 0;
|
||
}
|
||
|
||
let count = 1;
|
||
|
||
(this.detailLayout || []).forEach(item => {
|
||
if (item.tabBreak) {
|
||
count ++;
|
||
}
|
||
});
|
||
|
||
return count;
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
* @return {boolean}
|
||
*/
|
||
hasTabs() {
|
||
if (typeof this._hasMiddleTabs !== 'undefined') {
|
||
return this._hasMiddleTabs;
|
||
}
|
||
|
||
if (!this.detailLayout) {
|
||
return false;
|
||
}
|
||
|
||
for (const item of this.detailLayout) {
|
||
if (item.tabBreak) {
|
||
this._hasMiddleTabs = true;
|
||
|
||
return true;
|
||
}
|
||
}
|
||
|
||
this._hasMiddleTabs = false;
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
* @return {{label: string}[]}
|
||
*/
|
||
getMiddleTabDataList() {
|
||
const currentTab = this.currentTab;
|
||
|
||
const panelDataList = this.middlePanelDefsList;
|
||
|
||
return panelDataList
|
||
.filter((item, i) => i === 0 || item.tabBreak)
|
||
.map((item, i) => {
|
||
let label = item.tabLabel;
|
||
|
||
let hidden = false;
|
||
|
||
if (i > 0) {
|
||
hidden = panelDataList
|
||
.filter(panel => panel.tabNumber === i)
|
||
.findIndex(panel => !this.recordHelper.getPanelStateParam(panel.name, 'hidden')) === -1;
|
||
}
|
||
|
||
if (!label) {
|
||
label = i === 0 ?
|
||
this.translate('Overview') :
|
||
(i + 1).toString();
|
||
}
|
||
else if (label.substring(0, 7) === '$label:') {
|
||
label = this.translate(label.substring(7), 'labels', this.scope);
|
||
}
|
||
else if (label[0] === '$') {
|
||
label = this.translate(label.substring(1), 'tabs', this.scope);
|
||
}
|
||
|
||
return {
|
||
label: label,
|
||
isActive: currentTab === i,
|
||
hidden: hidden,
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Select a tab.
|
||
*
|
||
* @protected
|
||
* @param {number} tab
|
||
*/
|
||
selectTab(tab) {
|
||
this.currentTab = tab;
|
||
|
||
$('.popover.in').removeClass('in');
|
||
|
||
this.whenRendered().then(() => {
|
||
this.$el.find('.middle-tabs > button').removeClass('active');
|
||
this.$el.find(`.middle-tabs > button[data-tab="${tab}"]`).addClass('active');
|
||
|
||
this.$el.find('.middle > .panel[data-tab]').addClass('tab-hidden');
|
||
this.$el.find(`.middle > .panel[data-tab="${tab}"]`).removeClass('tab-hidden');
|
||
|
||
this.adjustMiddlePanels();
|
||
this.recordHelper.trigger('panel-show');
|
||
});
|
||
|
||
this.storeTab();
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
storeTab() {
|
||
const key = 'tab_middle';
|
||
const keyRecord = 'tab_middle_record';
|
||
|
||
this.getSessionStorage().set(key, this.currentTab);
|
||
this.getSessionStorage().set(keyRecord, this.entityType + '_' + this.model.id);
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
selectStoredTab() {
|
||
const key = 'tab_middle';
|
||
|
||
const tab = this.getSessionStorage().get(key);
|
||
|
||
if (tab > 0) {
|
||
this.selectTab(tab);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
isStoredTabForThisRecord() {
|
||
const keyRecord = 'tab_middle_record';
|
||
|
||
return this.getSessionStorage().get(keyRecord) === this.entityType + '_' + this.model.id;
|
||
}
|
||
|
||
/**
|
||
* @inheritDoc
|
||
*/
|
||
onInvalid(invalidFieldList) {
|
||
if (!this.hasTabs()) {
|
||
return;
|
||
}
|
||
|
||
const tabList = [];
|
||
|
||
for (const field of invalidFieldList) {
|
||
const view = this.getMiddleView().getFieldView(field);
|
||
|
||
if (!view) {
|
||
continue;
|
||
}
|
||
|
||
const tabString = view.$el
|
||
.closest('.panel.tab-hidden')
|
||
.attr('data-tab');
|
||
|
||
const tab = parseInt(tabString);
|
||
|
||
if (tabList.indexOf(tab) !== -1) {
|
||
continue;
|
||
}
|
||
|
||
tabList.push(tab);
|
||
}
|
||
|
||
if (!tabList.length) {
|
||
return;
|
||
}
|
||
|
||
const $tabs = this.$el.find('.middle-tabs');
|
||
|
||
tabList.forEach(tab => {
|
||
const $tab = $tabs.find(`> [data-tab="${tab.toString()}"]`);
|
||
|
||
$tab.addClass('invalid');
|
||
|
||
$tab.one('click', () => {
|
||
$tab.removeClass('invalid');
|
||
});
|
||
})
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
controlTabVisibilityShow(tab) {
|
||
if (!this.hasTabs() || tab === 0) {
|
||
return;
|
||
}
|
||
|
||
if (this.isBeingRendered()) {
|
||
this.once('after:render', () => this.controlTabVisibilityShow(tab));
|
||
|
||
return;
|
||
}
|
||
|
||
this.$el.find(`.middle-tabs > [data-tab="${tab.toString()}"]`).removeClass('hidden');
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
controlTabVisibilityHide(tab) {
|
||
if (!this.hasTabs() || tab === 0) {
|
||
return;
|
||
}
|
||
|
||
if (this.isBeingRendered()) {
|
||
this.once('after:render', () => this.controlTabVisibilityHide(tab));
|
||
|
||
return;
|
||
}
|
||
|
||
const panelList = this.middlePanelDefsList.filter(panel => panel.tabNumber === tab);
|
||
|
||
const allIsHidden = panelList
|
||
.findIndex(panel => !this.recordHelper.getPanelStateParam(panel.name, 'hidden')) === -1;
|
||
|
||
if (!allIsHidden) {
|
||
return;
|
||
}
|
||
|
||
const $tab = this.$el.find(`.middle-tabs > [data-tab="${tab.toString()}"]`);
|
||
|
||
$tab.addClass('hidden');
|
||
|
||
if (this.currentTab === tab) {
|
||
this.selectTab(0);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
adjustMiddlePanels() {
|
||
if (!this.isRendered() || !this.$middle.length) {
|
||
return;
|
||
}
|
||
|
||
const $panels = this.$middle.find('> .panel');
|
||
const $bottomPanels = this.$bottom ? this.$bottom.find('> .panel') : null;
|
||
|
||
$panels
|
||
.removeClass('first')
|
||
.removeClass('last')
|
||
.removeClass('in-middle');
|
||
|
||
const $visiblePanels = $panels.filter(`:not(.tab-hidden):not(.hidden)`);
|
||
|
||
$visiblePanels.each((i, el) => {
|
||
const $el = $(el);
|
||
|
||
if (i === $visiblePanels.length - 1) {
|
||
if ($bottomPanels && $bottomPanels.first().hasClass('sticked')) {
|
||
if (i === 0) {
|
||
$el.addClass('first');
|
||
|
||
return;
|
||
}
|
||
|
||
$el.addClass('in-middle');
|
||
|
||
return;
|
||
}
|
||
|
||
if (i === 0) {
|
||
return;
|
||
}
|
||
|
||
$el.addClass('last');
|
||
|
||
return;
|
||
}
|
||
|
||
if (i > 0 && i < $visiblePanels.length - 1) {
|
||
$el.addClass('in-middle');
|
||
|
||
return;
|
||
}
|
||
|
||
if (i === 0) {
|
||
$el.addClass('first');
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
adjustButtons() {
|
||
const $buttons = this.$detailButtonContainer.filter('.record-buttons').find('button.btn');
|
||
|
||
$buttons
|
||
.removeClass('radius-left')
|
||
.removeClass('radius-right');
|
||
|
||
const $buttonsVisible = $buttons.filter('button:not(.hidden)');
|
||
|
||
$buttonsVisible.first().addClass('radius-left');
|
||
$buttonsVisible.last().addClass('radius-right');
|
||
|
||
this.adjustEditButtons();
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
adjustEditButtons() {
|
||
const $buttons = this.$detailButtonContainer.filter('.edit-buttons').find('button.btn');
|
||
|
||
$buttons
|
||
.removeClass('radius-left')
|
||
.removeClass('radius-right');
|
||
|
||
const $buttonsVisible = $buttons.filter('button:not(.hidden)');
|
||
|
||
$buttonsVisible.first().addClass('radius-left');
|
||
$buttonsVisible.last().addClass('radius-right');
|
||
}
|
||
|
||
/**
|
||
* @private
|
||
*/
|
||
initElementReferences() {
|
||
if (this.$detailButtonContainer && this.$detailButtonContainer.length) {
|
||
return;
|
||
}
|
||
|
||
this.$detailButtonContainer = this.$el.find('.detail-button-container');
|
||
|
||
this.$dropdownItemListButton = this.$detailButtonContainer
|
||
.find('.dropdown-item-list-button');
|
||
|
||
this.$dropdownEditItemListButton = this.$detailButtonContainer
|
||
.find('.dropdown-edit-item-list-button');
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
*/
|
||
focusForEdit() {
|
||
this.$el
|
||
.find('.field:not(.hidden) .form-control:not([disabled])')
|
||
.first()
|
||
.focus();
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
*/
|
||
focusForCreate() {
|
||
this.$el
|
||
.find('.form-control:not([disabled])')
|
||
.first()
|
||
.focus();
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
* @param {KeyboardEvent} e
|
||
*/
|
||
handleShortcutKeyCtrlEnter(e) {
|
||
const action = this.shortcutKeyCtrlEnterAction;
|
||
|
||
if (this.inlineEditModeIsOn || this.buttonsDisabled || !action) {
|
||
return;
|
||
}
|
||
|
||
if (this.mode !== this.MODE_EDIT) {
|
||
return;
|
||
}
|
||
|
||
if (!this.hasAvailableActionItem(action)) {
|
||
return;
|
||
}
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
if (document.activeElement instanceof HTMLInputElement) {
|
||
// Fields may need to fetch data first.
|
||
document.activeElement.dispatchEvent(new Event('change', {bubbles: true}));
|
||
}
|
||
|
||
const methodName = 'action' + Espo.Utils.upperCaseFirst(action);
|
||
|
||
this[methodName]();
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
* @param {KeyboardEvent} e
|
||
*/
|
||
handleShortcutKeyCtrlS(e) {
|
||
if (this.inlineEditModeIsOn || this.buttonsDisabled) {
|
||
return;
|
||
}
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
if (this.mode !== this.MODE_EDIT) {
|
||
return;
|
||
}
|
||
|
||
if (!this.saveAndContinueEditingAction) {
|
||
return;
|
||
}
|
||
|
||
if (!this.hasAvailableActionItem('saveAndContinueEditing')) {
|
||
return;
|
||
}
|
||
|
||
this.actionSaveAndContinueEditing();
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
* @param {KeyboardEvent} e
|
||
*/
|
||
handleShortcutKeyCtrlSpace(e) {
|
||
if (this.inlineEditModeIsOn || this.buttonsDisabled) {
|
||
return;
|
||
}
|
||
|
||
if (this.type !== this.TYPE_DETAIL || this.mode !== this.MODE_DETAIL) {
|
||
return;
|
||
}
|
||
|
||
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') {
|
||
return;
|
||
}
|
||
|
||
if (!this.hasAvailableActionItem('edit')) {
|
||
return;
|
||
}
|
||
|
||
$(e.currentTarget)
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
this.actionEdit();
|
||
|
||
if (!this.editModeDisabled) {
|
||
setTimeout(() => this.focusForEdit(), 200);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
* @param {KeyboardEvent} e
|
||
*/
|
||
handleShortcutKeyEscape(e) {
|
||
if (this.inlineEditModeIsOn || this.buttonsDisabled) {
|
||
return;
|
||
}
|
||
|
||
if (this.type !== this.TYPE_DETAIL || this.mode !== this.MODE_EDIT) {
|
||
return;
|
||
}
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
// Fetching a currently edited form element.
|
||
this.model.set(this.fetch());
|
||
|
||
if (this.isChanged) {
|
||
this.confirm(this.translate('confirmLeaveOutMessage', 'messages'))
|
||
.then(() => this.actionCancelEdit());
|
||
|
||
return;
|
||
}
|
||
|
||
this.actionCancelEdit();
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
* @param {KeyboardEvent} e
|
||
*/
|
||
handleShortcutKeyCtrlAltEnter(e) {}
|
||
|
||
/**
|
||
* @public
|
||
* @param {KeyboardEvent} e
|
||
*/
|
||
handleShortcutKeyControlBackslash(e) {
|
||
if (!this.hasTabs()) {
|
||
return;
|
||
}
|
||
|
||
const $buttons = this.$el.find('.middle-tabs > button:not(.hidden)');
|
||
|
||
if ($buttons.length === 1) {
|
||
return;
|
||
}
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
let index = $buttons.toArray().findIndex(el => $(el).hasClass('active'));
|
||
|
||
index++;
|
||
|
||
if (index >= $buttons.length) {
|
||
index = 0;
|
||
}
|
||
|
||
const $tab = $($buttons.get(index));
|
||
|
||
const tab = parseInt($tab.attr('data-tab'));
|
||
|
||
this.selectTab(tab);
|
||
|
||
if (this.mode === this.MODE_EDIT) {
|
||
setTimeout(() => {
|
||
this.$middle
|
||
.find(`.panel[data-tab="${tab}"] .cell:not(.hidden)`)
|
||
.first()
|
||
.focus();
|
||
}, 50);
|
||
|
||
return;
|
||
}
|
||
|
||
this.$el
|
||
.find(`.middle-tabs button[data-tab="${tab}"]`)
|
||
.focus();
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
* @param {KeyboardEvent} e
|
||
*/
|
||
handleShortcutKeyControlArrowLeft(e) {
|
||
if (this.inlineEditModeIsOn || this.buttonsDisabled) {
|
||
return;
|
||
}
|
||
|
||
if (this.navigateButtonsDisabled) {
|
||
return;
|
||
}
|
||
|
||
if (this.type !== this.TYPE_DETAIL || this.mode !== this.MODE_DETAIL) {
|
||
return;
|
||
}
|
||
|
||
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') {
|
||
return;
|
||
}
|
||
|
||
const $button = this.$el.find('button[data-action="previous"]');
|
||
|
||
if (!$button.length || $button.hasClass('disabled')) {
|
||
return;
|
||
}
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
this.actionPrevious();
|
||
}
|
||
|
||
/**
|
||
* @protected
|
||
* @param {KeyboardEvent} e
|
||
*/
|
||
handleShortcutKeyControlArrowRight(e) {
|
||
if (this.inlineEditModeIsOn || this.buttonsDisabled) {
|
||
return;
|
||
}
|
||
|
||
if (this.navigateButtonsDisabled) {
|
||
return;
|
||
}
|
||
|
||
if (this.type !== this.TYPE_DETAIL || this.mode !== this.MODE_DETAIL) {
|
||
return;
|
||
}
|
||
|
||
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') {
|
||
return;
|
||
}
|
||
|
||
const $button = this.$el.find('button[data-action="next"]');
|
||
|
||
if (!$button.length || $button.hasClass('disabled')) {
|
||
return;
|
||
}
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
this.actionNext();
|
||
}
|
||
|
||
// noinspection JSUnusedGlobalSymbols
|
||
/**
|
||
* Get a current mode.
|
||
*
|
||
* @since 8.0.0
|
||
* @return {'detail'|'edit'}
|
||
*/
|
||
getMode() {
|
||
return this.mode;
|
||
}
|
||
}
|
||
|
||
export default DetailRecordView;
|