diff --git a/client/src/helpers/site/tabs.js b/client/src/helpers/site/tabs.js
new file mode 100644
index 0000000000..a3ea349b65
--- /dev/null
+++ b/client/src/helpers/site/tabs.js
@@ -0,0 +1,144 @@
+/************************************************************************
+ * This file is part of EspoCRM.
+ *
+ * EspoCRM – Open Source CRM application.
+ * Copyright (C) 2014-2024 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 .
+ *
+ * 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.
+ ************************************************************************/
+
+export default class TabsHelper {
+
+ /**
+ * @param {import('models/settings').default} config
+ * @param {import('models/preferences').default} preferences
+ * @param {import('models/user').default} user
+ * @param {import('acl-manager').default} acl
+ * @param {import('metadata').default} metadata
+ */
+ constructor(config, preferences, user, acl, metadata) {
+ /** @private */
+ this.config = config;
+ /** @private */
+ this.preferences = preferences;
+ /** @private */
+ this.user = user;
+ /** @private */
+ this.acl = acl;
+ /** @private */
+ this.metadata = metadata;
+ }
+
+ /**
+ * Get the tab list.
+ *
+ * @return {(Object|string)[]}
+ */
+ getTabList() {
+ let tabList = this.preferences.get('useCustomTabList') && !this.preferences.get('addCustomTabs') ?
+ this.preferences.get('tabList') :
+ this.config.get('tabList');
+
+ if (this.preferences.get('useCustomTabList') && this.preferences.get('addCustomTabs')) {
+ tabList = [
+ ...tabList,
+ ...(this.preferences.get('tabList') || []),
+ ];
+ }
+
+ return Espo.Utils.cloneDeep(tabList) || [];
+ }
+
+ /**
+ * Is a tab a divider.
+ *
+ * @param {string|{type?: string}} item
+ */
+ isTabDivider(item) {
+ return typeof item === 'object' && item.type === 'divider';
+ }
+
+ /**
+ * Is a tab more-delimiter.
+ *
+ * @param {string|{type?: string}} item
+ */
+ isTabMoreDelimiter(item) {
+ return item === '_delimiter_' || item === '_delimiter-ext_';
+ }
+
+ /**
+ * Is a tab a URL.
+ *
+ * @param {string|{type?: string}} item
+ */
+ isTabUrl(item) {
+ return typeof item === 'object' && item.type === 'url';
+ }
+
+ /**
+ * @param {Record|string} item
+ * @return {boolean}
+ */
+ checkTabAccess(item) {
+ if (this.isTabUrl(item)) {
+ if (item.onlyAdmin && !this.user.isAdmin()) {
+ return false;
+ }
+
+ if (!item.aclScope) {
+ return true;
+ }
+
+ return this.acl.check(item.aclScope);
+ }
+
+ if (item === 'Home' || this.isTabMoreDelimiter(item)) {
+ return true;
+ }
+
+ /** @type {Record} */
+ const scopes = this.metadata.get('scopes') || {};
+
+ if (!scopes[item]) {
+ return false;
+ }
+
+ const defs = scopes[item] || {};
+
+ if (defs.disabled) {
+ return false;
+ }
+
+ if (defs.acl) {
+ return this.acl.check(item);
+ }
+
+ if (defs.tabAclPermission) {
+ const level = this.acl.getPermissionLevel(defs.tabAclPermission);
+
+ return level && level !== 'no';
+ }
+
+ return true;
+ }
+}
diff --git a/client/src/views/site/navbar.js b/client/src/views/site/navbar.js
index 5bdedfd029..84ffe7483b 100644
--- a/client/src/views/site/navbar.js
+++ b/client/src/views/site/navbar.js
@@ -28,11 +28,16 @@
import View from 'view';
import $ from 'jquery';
+import TabsHelper from 'helpers/site/tabs';
class NavbarSiteView extends View {
template = 'site/navbar'
+ /**
+ * @private
+ * @type {string|false|null}
+ */
currentTab = null
events = {
@@ -337,18 +342,7 @@ class NavbarSiteView extends View {
* @return {(Object|string)[]}
*/
getTabList() {
- let tabList = this.getPreferences().get('useCustomTabList') && !this.getPreferences().get('addCustomTabs') ?
- this.getPreferences().get('tabList') :
- this.getConfig().get('tabList');
-
- if (this.getPreferences().get('useCustomTabList') && this.getPreferences().get('addCustomTabs')) {
- tabList = [
- ...tabList,
- ...(this.getPreferences().get('tabList') || []),
- ];
- }
-
- tabList = Espo.Utils.cloneDeep(tabList || []);
+ const tabList = this.tabsHelper.getTabList();
if (this.isSide()) {
tabList.unshift('Home');
@@ -386,6 +380,15 @@ class NavbarSiteView extends View {
this.selectTab(false);
});
+ /** @private */
+ this.tabsHelper = new TabsHelper(
+ this.getConfig(),
+ this.getPreferences(),
+ this.getUser(),
+ this.getAcl(),
+ this.getMetadata()
+ );
+
const itemDefs = this.getMetadata().get('app.clientNavbar.items') || {};
/** @type {string[]} */
@@ -500,52 +503,8 @@ class NavbarSiteView extends View {
}
/**
- * @param {Record|string} item
- * @return {boolean}
+ * @private
*/
- filterTabItem(item) {
- if (typeof item === 'object' && item.type === 'url') {
- if (item.onlyAdmin && !this.getUser().isAdmin()) {
- return false;
- }
-
- if (!item.aclScope) {
- return true;
- }
-
- return this.getAcl().check(item.aclScope);
- }
-
- if (['Home', '_delimiter_', '_delimiter-ext_'].includes(item)) {
- return true;
- }
-
- const scopes = this.getMetadata().get('scopes') || {};
-
- if (!scopes[item]) {
- return false;
- }
-
- const defs = /** @type {{disabled?: boolean, acl?: boolean, tabAclPermission?: string}} */
- scopes[item] || {};
-
- if (defs.disabled) {
- return false;
- }
-
- if (defs.acl) {
- return this.getAcl().check(item);
- }
-
- if (defs.tabAclPermission) {
- const level = this.getAcl().getPermissionLevel(defs.tabAclPermission);
-
- return level && level !== 'no';
- }
-
- return true;
- }
-
setupGlobalSearch() {
let isAvailable = false;
@@ -567,6 +526,9 @@ class NavbarSiteView extends View {
this.itemList = this.itemList.filter(it => it !== 'globalSearch');
}
+ /**
+ * @private
+ */
adjustTop() {
const smallScreenWidth = this.getThemeManager().getParam('screenWidthXs');
const navbarHeight = this.getNavbarHeight();
@@ -852,6 +814,9 @@ class NavbarSiteView extends View {
this.$body.css('minHeight', minHeight + 'px');
}
+ /**
+ * @private
+ */
adjustBodyMinHeightTop() {
let minHeight = this.getNavbarHeight();
@@ -1026,19 +991,10 @@ class NavbarSiteView extends View {
});
}
+ /**
+ * @private
+ */
setupTabDefsList() {
- function isMoreDelimiter(item) {
- return item === '_delimiter_' || item === '_delimiter-ext_';
- }
-
- function isDivider(item) {
- return typeof item === 'object' && item.type === 'divider';
- }
-
- function isUrl(item) {
- return typeof item === 'object' && item.type === 'url';
- }
-
/** @type {{url: string, name: string}[]} */
this.urlList = [];
@@ -1050,10 +1006,10 @@ class NavbarSiteView extends View {
}
if (typeof item !== 'object') {
- return this.filterTabItem(item);
+ return this.tabsHelper.checkTabAccess(item);
}
- if (isDivider(item)) {
+ if (this.tabsHelper.isTabDivider(item)) {
if (!this.isSide()) {
return false;
}
@@ -1065,21 +1021,21 @@ class NavbarSiteView extends View {
return true;
}
- if (isUrl(item)) {
- return this.filterTabItem(item);
+ if (this.tabsHelper.isTabUrl(item)) {
+ return this.tabsHelper.checkTabAccess(item);
}
/** @type {(Record|string)[]} */
- let itemList = (item.itemList || []).filter((item) => {
- if (isDivider(item)) {
+ let itemList = (item.itemList || []).filter(item => {
+ if (this.tabsHelper.isTabDivider(item)) {
return true;
}
- return this.filterTabItem(item);
+ return this.tabsHelper.checkTabAccess(item);
});
itemList = itemList.filter((item, i) => {
- if (!isDivider(item)) {
+ if (!this.tabsHelper.isTabDivider(item)) {
return true;
}
@@ -1089,7 +1045,7 @@ class NavbarSiteView extends View {
return true;
}
- if (isDivider(nextItem)) {
+ if (this.tabsHelper.isTabDivider(nextItem)) {
return false;
}
@@ -1097,7 +1053,7 @@ class NavbarSiteView extends View {
});
itemList = itemList.filter((item, i) => {
- if (!isDivider(item)) {
+ if (!this.tabsHelper.isTabDivider(item)) {
return true;
}
@@ -1119,11 +1075,11 @@ class NavbarSiteView extends View {
const nextItem = this.tabList[i + 1];
const prevItem = this.tabList[i - 1];
- if (isMoreDelimiter(item)) {
+ if (this.tabsHelper.isTabMoreDelimiter(item)) {
moreIsMet = true;
}
- if (!isDivider(item)) {
+ if (!this.tabsHelper.isTabDivider(item)) {
return true;
}
@@ -1131,11 +1087,11 @@ class NavbarSiteView extends View {
return true;
}
- if (isDivider(nextItem)) {
+ if (this.tabsHelper.isTabDivider(nextItem)) {
return false;
}
- if (isDivider(prevItem) && isMoreDelimiter(nextItem) && moreIsMet) {
+ if (this.tabsHelper.isTabDivider(prevItem) && this.tabsHelper.isTabMoreDelimiter(nextItem) && moreIsMet) {
return false;
}
@@ -1161,7 +1117,7 @@ class NavbarSiteView extends View {
};
this.tabList.forEach((tab, i) => {
- if (isMoreDelimiter(tab)) {
+ if (this.tabsHelper.isTabMoreDelimiter(tab)) {
if (!vars.moreIsMet) {
vars.moreIsMet = true;
@@ -1192,6 +1148,29 @@ class NavbarSiteView extends View {
this.tabDefsList = tabDefsList;
}
+ /**
+ * @private
+ * @param {{
+ * colorsDisabled: boolean,
+ * tabIconsDisabled: boolean,
+ * }} params
+ * @param {Object|string} tab
+ * @param {number} i
+ * @param {Object} vars
+ * @return {{
+ * isAfterShowMore: boolean,
+ * isDivider: boolean,
+ * color: null,
+ * link: string,
+ * name: string,
+ * isInMore: boolean,
+ * shortLabel: string,
+ * label: string,
+ * isGroup: boolean,
+ * aClassName: string,
+ * iconClass: null
+ * }}
+ */
prepareTabItemDefs(params, tab, i, vars) {
let label;
let link;
@@ -1215,8 +1194,7 @@ class NavbarSiteView extends View {
if (tab === 'Home') {
label = this.getLanguage().translate(tab);
link = '#';
- }
- else if (typeof tab === 'object' && tab.type === 'divider') {
+ } else if (typeof tab === 'object' && tab.type === 'divider') {
isDivider = true;
label = tab.text;
aClassName = 'nav-divider-text';
@@ -1225,8 +1203,7 @@ class NavbarSiteView extends View {
if (label) {
label = translateLabel(label);
}
- }
- else if (typeof tab === 'object' && tab.type === 'url') {
+ } else if (typeof tab === 'object' && tab.type === 'url') {
isUrl = true;
label = tab.text || '#';
name = 'url-' + i;
@@ -1239,8 +1216,7 @@ class NavbarSiteView extends View {
}
this.urlList.push({name: name, url: link});
- }
- else if (typeof tab === 'object') {
+ } else if (typeof tab === 'object') {
isGroup = true;
label = tab.text || '';
@@ -1256,8 +1232,7 @@ class NavbarSiteView extends View {
if (label) {
label = translateLabel(label);
}
- }
- else {
+ } else {
label = this.getLanguage().translate(tab, 'scopeNamesPlural');
link = '#' + tab;
}
@@ -1315,7 +1290,9 @@ class NavbarSiteView extends View {
* @property {true} [divider]
*/
-
+ /**
+ * @private
+ */
setupMenu() {
let avatarHtml = this.getHelper().getAvatarHtml(this.getUser().id, 'small', 20, 'avatar-link');