This repository has been archived on 2026-07-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
espocrm-base/client/src/views/record/panels-container.js
T

977 lines
26 KiB
JavaScript

/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2023 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://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 General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
/** @module views/record/panels-container */
import View from 'view';
/**
* A panel container view. For bottom and side views.
*/
class PanelsContainerRecordView extends View {
/** @private */
panelSoftLockedTypeList = ['default', 'acl', 'delimiter', 'dynamicLogic']
/**
* A panel.
*
* @typedef {Object} module:views/record/panels-container~panel
*
* @property {string} name A name.
* @property {boolean} [hidden] Hidden.
* @property {string} [label] A label.
* @property {'default'|'success'|'danger'|'warning'} [style] A style.
* @property {string} [titleHtml] A title HTML.
* @property {boolean} [notRefreshable] Not refreshable.
* @property {boolean} [isForm] If for a form.
* @property {module:views/record/panels-container~button[]} [buttonList] Buttons.
* @property {module:views/record/panels-container~action[]} [actionList] Dropdown actions.
* @property {string} [view] A view name.
* @property {Object.<string, *>} [Options] A view options.
* @property {boolean} [sticked] To stick to an upper panel.
* @property {Number} [tabNumber] A tab number.
* @property {string} [aclScope] A scope to check access to.
* @property {Espo.Utils~AccessDefs[]} [accessDataList] Access control defs.
*/
/**
* A button. Handled by an `action{Action}` method or a click handler.
*
* @typedef {Object} module:views/record/panels-container~button
*
* @property {string} action An action.
* @property {boolean} [hidden] Hidden.
* @property {string} [label] A label. Translatable.
* @property {string} [html] A HTML.
* @property {string} [text] A text.
* @property {string} [title] A title (on hover). Translatable.
* @property {Object.<string, (string|number|boolean)>} [data] Data attributes.
* @property {function()} [onClick] A click event.
*/
/**
* An action. Handled by an `action{Action}` method or a click handler.
*
* @typedef {Object} module:views/record/panels-container~action
*
* @property {string} [action] An action.
* @property {string} [link] A link URL.
* @property {boolean} [hidden] Hidden.
* @property {string} [label] A label. Translatable.
* @property {string} [html] A HTML.
* @property {string} [text] A text.
* @property {Object.<string, (string|number|boolean)>} [data] Data attributes.
* @property {function()} [onClick] A click event.
*/
/**
* A panel list.
*
* @protected
* @type {module:views/record/panels-container~panel[]}
*/
panelList = null
/** @private */
hasTabs = false
/**
* @private
* @type {Object.<string,*>[]|null}
*/
tabDataList = null
/**
* @protected
*/
currentTab = 0
/**
* @protected
* @type {string}
*/
scope = ''
/**
* @protected
* @type {string}
*/
entityType = ''
/**
* @protected
* @type {string}
*/
name = ''
/**
* A mode.
*
* @type 'detail'|'edit'
*/
mode = 'detail'
data() {
let tabDataList = this.hasTabs ? this.getTabDataList() : [];
return {
panelList: this.panelList,
scope: this.scope,
entityType: this.entityType,
tabDataList: tabDataList,
};
}
events = {
'click .action': function (e) {
let $target = $(e.currentTarget);
let panel = $target.data('panel');
if (!panel) {
return;
}
let panelView = this.getView(panel);
if (!panelView) {
return;
}
let actionItems;
if (
typeof panelView.getButtonList === 'function' &&
typeof panelView.getActionList === 'function'
) {
actionItems = [...panelView.getButtonList(), ...panelView.getActionList()];
}
Espo.Utils.handleAction(panelView, e.originalEvent, e.currentTarget, {
actionItems: actionItems,
className: 'panel-action',
});
// @todo Check data. Maybe pass cloned data with unset params.
/*
let action = $target.data('action');
let data = $target.data();
if (action && panel) {
let method = 'action' + Espo.Utils.upperCaseFirst(action);
let d = _.clone(data);
delete d['action'];
delete d['panel'];
let view = this.getView(panel);
if (view && typeof view[method] == 'function') {
view[method].call(view, d, e);
}
}*/
},
'click .panels-show-more-delimiter [data-action="showMorePanels"]': 'actionShowMorePanels',
/** @this module:views/record/panels-container */
'click .tabs > button': function (e) {
let tab = parseInt($(e.currentTarget).attr('data-tab'));
this.selectTab(tab);
},
}
afterRender() {
this.adjustPanels();
}
adjustPanels() {
if (!this.isRendered()) {
return;
}
let $panels = this.$el.find('> .panel');
$panels
.removeClass('first')
.removeClass('last')
.removeClass('in-middle');
let $visiblePanels = $panels.filter(`:not(.tab-hidden):not(.hidden)`);
let groups = [];
let currentGroup = [];
let inTab = false;
$visiblePanels.each((i, el) => {
let $el = $(el);
let breakGroup = false;
if (
!breakGroup &&
this.hasTabs &&
!inTab &&
$el.attr('data-tab') !== '-1'
) {
inTab = true;
breakGroup = true;
}
if (!breakGroup && !$el.hasClass('sticked')) {
breakGroup = true;
}
if (breakGroup) {
if (i !== 0) {
groups.push(currentGroup);
}
currentGroup = [];
}
currentGroup.push($el);
if (i === $visiblePanels.length - 1) {
groups.push(currentGroup);
}
});
groups.forEach(group => {
group.forEach(($el, i) => {
if (i === group.length - 1) {
if (i === 0) {
return;
}
$el.addClass('last')
return;
}
if (i === 0 && group.length) {
$el.addClass('first')
return;
}
$el.addClass('in-middle');
});
});
}
/**
* Set read-only.
*/
setReadOnly() {
this.readOnly = true;
}
/**
* Set not read-only.
*/
setNotReadOnly(onlyNotSetAsReadOnly) {
this.readOnly = false;
if (onlyNotSetAsReadOnly) {
this.panelList.forEach(item => {
this.applyAccessToActions(item.buttonList);
this.applyAccessToActions(item.actionList);
if (this.isRendered()) {
let actionsView = this.getView(item.actionsViewKey);
if (actionsView) {
actionsView.reRender();
}
}
});
}
}
/**
* @private
* @param {Object[]} actionList
*/
applyAccessToActions(actionList) {
if (!actionList) {
return;
}
actionList.forEach(item => {
if (!Espo.Utils.checkActionAvailability(this.getHelper(), item)) {
item.hidden = true;
return;
}
if (Espo.Utils.checkActionAccess(this.getAcl(), this.model, item, true)) {
if (item.isHiddenByAcl) {
item.isHiddenByAcl = false;
item.hidden = false;
}
}
else {
if (!item.hidden) {
item.isHiddenByAcl = true;
item.hidden = true;
}
}
});
}
/**
* Set up panel views.
*
* @protected
*/
setupPanelViews() {
this.panelList.forEach(p => {
let name = p.name;
let options = {
model: this.model,
panelName: name,
selector: '.panel[data-name="' + name + '"] > .panel-body',
defs: p,
mode: this.mode,
recordHelper: this.recordHelper,
inlineEditDisabled: this.inlineEditDisabled,
readOnly: this.readOnly,
disabled: p.hidden || false,
recordViewObject: this.recordViewObject,
dataObject: this.options.dataObject,
};
options = _.extend(options, p.options);
this.createView(name, p.view, options, (view) => {
if ('getActionList' in view) {
p.actionList = view.getActionList();
this.applyAccessToActions(p.actionList);
}
if ('getButtonList' in view) {
p.buttonList = view.getButtonList();
this.applyAccessToActions(p.buttonList);
}
if (view.titleHtml) {
p.titleHtml = view.titleHtml;
}
else {
if (p.label) {
p.title = this.translate(p.label, 'labels', this.scope);
}
else {
p.title = view.title;
}
}
this.createView(name + 'Actions', 'views/record/panel-actions', {
selector: '.panel[data-name="' + p.name + '"] > .panel-heading > .panel-actions-container',
model: this.model,
defs: p,
scope: this.scope,
entityType: this.entityType,
});
});
});
}
/**
* Set up panels.
*
* @protected
*/
setupPanels() {}
/**
* Get field views.
*
* @param {boolean} [withHidden] With hidden.
* @return {Object.<string, module:views/fields/base>}
*/
getFieldViews(withHidden) {
let fields = {};
this.panelList.forEach(p => {
let panelView = this.getView(p.name);
if ((!panelView.disabled || withHidden) && 'getFieldViews' in panelView) {
fields = _.extend(fields, panelView.getFieldViews());
}
});
return fields;
}
// noinspection JSUnusedGlobalSymbols
/**
* @deprecated Use `getFieldViews`.
* @todo Remove in v9.0.
*/
getFields() {
return this.getFieldViews();
}
/**
* Fetch.
*
* @return {Object.<string, *>}
*/
fetch() {
let data = {};
this.panelList.forEach(p => {
let panelView = this.getView(p.name);
if (!panelView.disabled && 'fetch' in panelView) {
data = _.extend(data, panelView.fetch());
}
});
return data;
}
/**
* @param {string} name
* @return {boolean}
*/
hasPanel(name) {
return !!this.panelList.find(item => item.name === name);
}
processShowPanel(name, callback, wasShown) {
if (this.recordHelper.getPanelStateParam(name, 'hidden')) {
return;
}
if (!this.hasPanel(name)) {
return;
}
this.panelList.filter(item => item.name === name).forEach(item => {
item.hidden = false;
if (typeof item.tabNumber !== 'undefined') {
this.controlTabVisibilityShow(item.tabNumber);
}
});
this.showPanelFinalize(name, callback, wasShown);
}
processHidePanel(name, callback) {
if (!this.recordHelper.getPanelStateParam(name, 'hidden')) {
return;
}
if (!this.hasPanel(name)) {
return;
}
this.panelList.filter(item => item.name === name).forEach(item => {
item.hidden = true;
if (typeof item.tabNumber !== 'undefined') {
this.controlTabVisibilityHide(item.tabNumber);
}
});
this.hidePanelFinalize(name, callback);
}
showPanelFinalize(name, callback, wasShown) {
let process = (wasRendered) => {
let view = this.getView(name);
if (view) {
view.$el.closest('.panel').removeClass('hidden');
view.disabled = false;
view.trigger('show');
view.trigger('panel-show-propagated');
if (wasRendered && !wasShown && view.getFieldViews) {
let fields = view.getFieldViews();
if (fields) {
for (let i in fields) {
fields[i].reRender();
}
}
}
}
if (typeof callback === 'function') {
callback.call(this);
}
};
if (this.isRendered()) {
process(true);
this.adjustPanels();
return;
}
this.once('after:render', () => {
process();
});
}
hidePanelFinalize(name, callback) {
if (this.isRendered()) {
let view = this.getView(name);
if (view) {
view.$el.closest('.panel').addClass('hidden');
view.disabled = true;
view.trigger('hide');
}
if (typeof callback === 'function') {
callback.call(this);
}
this.adjustPanels();
return;
}
if (typeof callback === 'function') {
this.once('after:render', () => {
callback.call(this);
});
}
}
showPanel(name, softLockedType, callback) {
if (!this.hasPanel(name)) {
return;
}
if (this.recordHelper.getPanelStateParam(name, 'hiddenLocked')) {
return;
}
if (softLockedType) {
let param = 'hidden' + Espo.Utils.upperCaseFirst(softLockedType) + 'Locked';
this.recordHelper.setPanelStateParam(name, param, false);
for (let i = 0; i < this.panelSoftLockedTypeList.length; i++) {
let iType = this.panelSoftLockedTypeList[i];
if (iType === softLockedType) {
continue;
}
let iParam = 'hidden' + Espo.Utils.upperCaseFirst(iType) + 'Locked';
if (this.recordHelper.getPanelStateParam(name, iParam)) {
return;
}
}
}
let wasShown = this.recordHelper.getPanelStateParam(name, 'hidden') === false;
this.recordHelper.setPanelStateParam(name, 'hidden', false);
this.processShowPanel(name, callback, wasShown);
}
hidePanel(name, locked, softLockedType, callback) {
if (!this.hasPanel(name)) {
return;
}
this.recordHelper.setPanelStateParam(name, 'hidden', true);
if (locked) {
this.recordHelper.setPanelStateParam(name, 'hiddenLocked', true);
}
if (softLockedType) {
let param = 'hidden' + Espo.Utils.upperCaseFirst(softLockedType) + 'Locked';
this.recordHelper.setPanelStateParam(name, param, true);
}
this.processHidePanel(name, callback);
}
alterPanels(layoutData) {
layoutData = layoutData || this.layoutData || {};
let tabBreakIndexList = [];
let tabDataList = [];
for (let name in layoutData) {
let item = layoutData[name];
if (name === '_delimiter_') {
this.panelList.push({
name: name,
});
}
if (item.tabBreak) {
tabBreakIndexList.push(item.index);
tabDataList.push({
index: item.index,
label: item.tabLabel,
})
}
}
/**
* @private
* @type {Object.<string,*>[]}
*/
this.tabDataList = tabDataList.sort((v1, v2) => v1.index - v2.index);
this.panelList = this.panelList.filter(item => {
return !this.recordHelper.getPanelStateParam(item.name, 'hiddenLocked');
});
let newList = [];
this.panelList.forEach((item, i) => {
item.index = ('index' in item) ? item.index : i;
let allowedInLayout = false;
if (item.name) {
let itemData = layoutData[item.name] || {};
if (itemData.disabled) {
return;
}
if (layoutData[item.name]) {
allowedInLayout = true;
}
for (let i in itemData) {
item[i] = itemData[i];
}
}
if (item.disabled && !allowedInLayout) {
return;
}
item.tabNumber = tabBreakIndexList.length -
tabBreakIndexList.slice().reverse().findIndex(index => item.index > index) - 1;
if (item.tabNumber === tabBreakIndexList.length) {
item.tabNumber = -1;
}
newList.push(item);
});
newList.sort((v1, v2) => v1.index - v2.index);
let firstTabIndex = newList.findIndex(item => item.tabNumber !== -1);
if (firstTabIndex !== -1) {
newList[firstTabIndex].isTabsBeginning = true;
this.hasTabs = true;
this.currentTab = newList[firstTabIndex].tabNumber;
this.panelList
.filter(item => item.tabNumber !== -1 && item.tabNumber !== this.currentTab)
.forEach(item => {
item.tabHidden = true;
});
this.panelList
.forEach((item, i) => {
if (
item.tabNumber !== -1 &&
(i === 0 || this.panelList[i - 1].tabNumber !== item.tabNumber)
) {
item.sticked = false;
}
});
}
this.panelList = newList;
if (this.recordViewObject && this.recordViewObject.dynamicLogic) {
let dynamicLogic = this.recordViewObject.dynamicLogic;
this.panelList.forEach(item => {
if (item.dynamicLogicVisible) {
dynamicLogic.addPanelVisibleCondition(item.name, item.dynamicLogicVisible);
if (this.recordHelper.getPanelStateParam(item.name, 'hidden')) {
item.hidden = true;
}
}
if (item.style && item.style !== 'default' && item.dynamicLogicStyled) {
dynamicLogic.addPanelStyledCondition(item.name, item.dynamicLogicStyled);
}
});
}
if (
this.hasTabs &&
this.options.isReturn &&
this.isStoredTabForThisRecord()
) {
this.selectStoredTab();
}
}
setupPanelsFinal() {
let afterDelimiter = false;
let rightAfterDelimiter = false;
let index = -1;
this.panelList.forEach((p, i) => {
if (p.name === '_delimiter_') {
afterDelimiter = true;
rightAfterDelimiter = true;
index = i;
return;
}
if (afterDelimiter) {
p.hidden = true;
p.hiddenAfterDelimiter = true;
this.recordHelper.setPanelStateParam(p.name, 'hidden', true);
this.recordHelper.setPanelStateParam(p.name, 'hiddenDelimiterLocked', true);
}
if (rightAfterDelimiter) {
p.isRightAfterDelimiter = true;
rightAfterDelimiter = false;
}
});
if (~index) {
this.panelList.splice(index, 1);
}
this.panelsAreSet = true;
this.trigger('panels-set');
}
actionShowMorePanels() {
this.panelList.forEach(p => {
if (!p.hiddenAfterDelimiter) {
return;
}
delete p.isRightAfterDelimiter;
this.showPanel(p.name, 'delimiter');
});
this.$el.find('.panels-show-more-delimiter').remove();
}
onPanelsReady(callback) {
Promise.race([
new Promise(resolve => {
if (this.panelsAreSet) {
resolve();
}
}),
new Promise(resolve => {
this.once('panels-set', resolve);
})
]).then(() => {
callback.call(this);
});
}
getTabDataList() {
return this.tabDataList.map((item, i) => {
let label = item.label;
if (!label) {
label = (i + 1).toString();
}
else if (label[0] === '$') {
label = this.translate(label.substring(1), 'tabs', this.scope);
}
let hidden = this.panelList
.filter(panel => panel.tabNumber === i)
.findIndex(panel => !this.recordHelper.getPanelStateParam(panel.name, 'hidden')) === -1;
return {
label: label,
isActive: i === this.currentTab,
hidden: hidden,
};
});
}
selectTab(tab) {
this.currentTab = tab;
if (this.isRendered()) {
$('body > .popover').remove();
this.$el.find('.tabs > button').removeClass('active');
this.$el.find(`.tabs > button[data-tab="${tab}"]`).addClass('active');
this.$el.find('.panel[data-tab]:not([data-tab="-1"])').addClass('tab-hidden');
this.$el.find(`.panel[data-tab="${tab}"]`).removeClass('tab-hidden');
}
this.adjustPanels();
this.panelList
.filter(item => item.tabNumber === tab && item.name)
.forEach(item => {
let view = this.getView(item.name);
if (view) {
view.trigger('tab-show');
view.propagateEvent('panel-show-propagated');
}
item.tabHidden = false;
});
this.panelList
.filter(item => item.tabNumber !== tab && item.name)
.forEach(item => {
let view = this.getView(item.name);
if (view) {
view.trigger('tab-hide');
}
if (item.tabNumber > -1) {
item.tabHidden = true;
}
});
this.storeTab();
}
/** @private */
storeTab() {
let key = 'tab_' + this.name;
let keyRecord = 'tab_' + this.name + '_record';
this.getSessionStorage().set(key, this.currentTab);
this.getSessionStorage().set(keyRecord, this.entityType + '_' + this.model.id);
}
/** @private */
isStoredTabForThisRecord() {
let keyRecord = 'tab_' + this.name + '_record';
return this.getSessionStorage().get(keyRecord) === this.entityType + '_' + this.model.id;
}
/** @private */
selectStoredTab() {
let key = 'tab_' + this.name;
let tab = this.getSessionStorage().get(key);
if (tab > 0) {
this.selectTab(tab);
}
}
/** @private */
controlTabVisibilityShow(tab) {
if (!this.hasTabs) {
return;
}
if (this.isBeingRendered()) {
this.once('after:render', () => this.controlTabVisibilityShow(tab));
return;
}
this.$el.find(`.tabs > [data-tab="${tab.toString()}"]`).removeClass('hidden');
}
/** @private */
controlTabVisibilityHide(tab) {
if (!this.hasTabs) {
return;
}
if (this.isBeingRendered()) {
this.once('after:render', () => this.controlTabVisibilityHide(tab));
return;
}
let panelList = this.panelList.filter(panel => panel.tabNumber === tab);
let allIsHidden = panelList
.findIndex(panel => !this.recordHelper.getPanelStateParam(panel.name, 'hidden')) === -1;
if (!allIsHidden) {
return;
}
let $tab = this.$el.find(`.tabs > [data-tab="${tab.toString()}"]`);
$tab.addClass('hidden');
if (this.currentTab === tab) {
let firstVisiblePanel = this.panelList
.find(panel => panel.tabNumber > -1 && !panel.hidden);
let firstVisibleTab = firstVisiblePanel ?
firstVisiblePanel.tabNumber : 0;
this.selectTab(firstVisibleTab);
}
}
}
export default PanelsContainerRecordView;