From b1de15339aade7b5e151380da460f01d2f32b675 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Fri, 21 Jun 2024 15:42:48 +0300 Subject: [PATCH] metadata navbar menu --- .../Resources/metadata/app/clientNavbar.json | 41 +++++ client/res/templates/site/navbar.tpl | 10 +- client/src/handlers/navbar-menu.js | 48 ++++++ client/src/views/site/navbar.js | 148 +++++++++++------- schema/metadata/app/clientNavbar.json | 47 +++++- 5 files changed, 229 insertions(+), 65 deletions(-) create mode 100644 client/src/handlers/navbar-menu.js diff --git a/application/Espo/Resources/metadata/app/clientNavbar.json b/application/Espo/Resources/metadata/app/clientNavbar.json index 905e54a2a5..e6726b18be 100644 --- a/application/Espo/Resources/metadata/app/clientNavbar.json +++ b/application/Espo/Resources/metadata/app/clientNavbar.json @@ -18,5 +18,46 @@ "order": 15, "disabled": false } + }, + "menuItems": { + "admin": { + "order": 0, + "groupIndex": 1, + "link": "#Admin", + "labelTranslation": "Global.labels.Administration", + "accessDataList": [ + { + "isAdminOnly": true + } + ] + }, + "preferences": { + "order": 1, + "groupIndex": 1, + "link": "#Preferences", + "labelTranslation": "Global.labels.Preferences" + }, + "lastViewed": { + "order": 0, + "groupIndex": 5, + "link": "#LastViewed", + "labelTranslation": "Global.scopeNamesPlural.LastViewed", + "configCheck": "!actionHistoryDisabled", + "handler": "handlers/navbar-menu", + "actionFunction": "lastViewed" + }, + "about": { + "order": 0, + "groupIndex": 10, + "link": "#LastViewed", + "labelTranslation": "Global.labels.About" + }, + "logout": { + "order": 1, + "groupIndex": 10, + "labelTranslation": "Global.labels.Log Out", + "handler": "handlers/navbar-menu", + "actionFunction": "logout" + } } } diff --git a/client/res/templates/site/navbar.tpl b/client/res/templates/site/navbar.tpl index bf9dc65e66..54ca460edc 100644 --- a/client/res/templates/site/navbar.tpl +++ b/client/res/templates/site/navbar.tpl @@ -195,11 +195,11 @@ {{#each menuDataList}} {{#unless divider}}
  • {{#if html}}{{{html}}}{{else}}{{label}}{{/if}}
  • + {{#if name}}data-name="{{name}}"{{/if}} + {{#if link}}href="{{link}}"{{else}}role="button"{{/if}} + tabindex="0" + class="nav-link{{#if handler}} action{{/if}}" + >{{#if html}}{{{html}}}{{else}}{{label}}{{/if}} {{else}}
  • {{/unless}} diff --git a/client/src/handlers/navbar-menu.js b/client/src/handlers/navbar-menu.js new file mode 100644 index 0000000000..2d905d9dd3 --- /dev/null +++ b/client/src/handlers/navbar-menu.js @@ -0,0 +1,48 @@ +/************************************************************************ + * 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. + ************************************************************************/ + +import ActionHandler from 'action-handler'; + +class NavbarMenuHandler extends ActionHandler { + + logout() { + this.view.getRouter().logout(); + } + + // noinspection JSUnusedGlobalSymbols + lastViewed() { + Espo.Ui.notify(' ... '); + + this.view.createView('dialog', 'views/modals/last-viewed', {}, view => { + view.render(); + Espo.Ui.notify(false); + }); + } +} + +export default NavbarMenuHandler; diff --git a/client/src/views/site/navbar.js b/client/src/views/site/navbar.js index 9367517354..5bdedfd029 100644 --- a/client/src/views/site/navbar.js +++ b/client/src/views/site/navbar.js @@ -64,10 +64,6 @@ class NavbarSiteView extends View { this.switchSideMenu(); }, /** @this NavbarSiteView */ - 'click a.action': function (e) { - Espo.Utils.handleAction(this, e.originalEvent, e.currentTarget); - }, - /** @this NavbarSiteView */ 'click [data-action="toggleCollapsable"]': function () { this.toggleCollapsable(); }, @@ -91,7 +87,7 @@ class NavbarSiteView extends View { tabDefsList1: this.tabDefsList.filter(item => !item.isInMore), tabDefsList2: this.tabDefsList.filter(item => item.isInMore), title: this.options.title, - menuDataList: this.getMenuDataList(), + menuDataList: this.menuDataList, userId: this.getUser().id, logoSrc: this.getLogoSrc(), itemDataList: this.getItemDataList(), @@ -362,6 +358,24 @@ class NavbarSiteView extends View { } setup() { + this.addHandler('click', 'a.action', (/** MouseEvent */event, target) => { + let actionData; + const name = target.dataset.name; + + if (name) { + const item = this.menuDataList.find(it => it.name === name); + + if (item.handler && item.actionFunction) { + actionData = { + handler: item.handler, + actionFunction: item.actionFunction, + }; + } + } + + Espo.Utils.handleAction(this, event, target, actionData); + }); + this.getRouter().on('routed', (e) => { if (e.controller) { this.selectTab(e.controller); @@ -424,6 +438,8 @@ class NavbarSiteView extends View { this.$body.removeClass('has-navbar'); }); + + this.setupMenu(); } getItemDataList() { @@ -1292,82 +1308,96 @@ class NavbarSiteView extends View { /** * @typedef {Object} MenuDataItem * @property {string} [link] + * @property {string} [name] * @property {string} [html] + * @property {string} [handler] + * @property {string} [actionFunction] * @property {true} [divider] */ - /** - * @return {MenuDataItem[]} - */ - getMenuDataList() { + + setupMenu() { let avatarHtml = this.getHelper().getAvatarHtml(this.getUser().id, 'small', 20, 'avatar-link'); if (avatarHtml) { avatarHtml += ' '; } - /** @type {MenuDataItem[]}*/ - let list = [ + /** @type {MenuDataItem[]} */ + this.menuDataList = [ { - link: '#User/view/' + this.getUser().id, + link: `#User/view/${this.getUser().id}`, html: avatarHtml + this.getHelper().escapeString(this.getUser().get('name')), }, {divider: true} ]; - if (this.getUser().isAdmin()) { - list.push({ - link: '#Admin', - label: this.getLanguage().translate('Administration'), - }); - } + /** + * @type {Record} items + */ + const items = this.getMetadata().get('app.clientNavbar.menuItems') || {}; - list.push({ - link: '#Preferences', - label: this.getLanguage().translate('Preferences'), + const nameList = Object.keys(items).sort((n1, n2) => { + const o1 = items[n1].order; + const o2 = items[n2].order; + + const g1 = items[n1].groupIndex; + const g2 = items[n2].groupIndex; + + if (g2 === g1) { + return o1 - o2; + } + + return g1 - g2; }); - if (!this.getConfig().get('actionHistoryDisabled')) { - list.push({divider: true}); + let currentGroup = 0; - list.push({ - action: 'showLastViewed', - link: '#LastViewed', - label: this.getLanguage().translate('LastViewed', 'scopeNamesPlural'), + for (const name of nameList) { + const item = items[name]; + + if (item.groupIndex !== currentGroup) { + currentGroup = item.groupIndex; + + this.menuDataList.push({divider: true}); + } + + if (item.disabled) { + continue; + } + + if ( + item.configCheck && + !Espo.Utils.checkActionAvailability(this.getHelper(), item) + ) { + continue; + } + + if ( + item.accessDataList && + !Espo.Utils.checkAccessDataList(item.accessDataList, this.getAcl(), this.getUser()) + ) { + continue; + } + + this.menuDataList.push({ + name: name, + link: item.link, + label: this.getLanguage().translatePath(item.labelTranslation), + handler: item.handler, + actionFunction: item.actionFunction, }); } - - list = list.concat([ - { - divider: true - }, - { - link: '#About', - label: this.getLanguage().translate('About') - }, - { - action: 'logout', - label: this.getLanguage().translate('Log Out') - }, - ]); - - return list; - } - - // noinspection JSUnusedGlobalSymbols - actionLogout() { - this.getRouter().logout(); - } - - // noinspection JSUnusedGlobalSymbols - actionShowLastViewed() { - Espo.Ui.notify(' ... '); - - this.createView('dialog', 'views/modals/last-viewed', {}, (view) => { - view.render(); - - Espo.Ui.notify(false); - }); } showMoreTabs() { diff --git a/schema/metadata/app/clientNavbar.json b/schema/metadata/app/clientNavbar.json index cde3180db2..70d020cfca 100644 --- a/schema/metadata/app/clientNavbar.json +++ b/schema/metadata/app/clientNavbar.json @@ -6,7 +6,7 @@ "type": "object", "properties": { "items": { - "description": "Navbar item definitions", + "description": "Navbar item definitions.", "type": "object", "additionalProperties": { "description": "An item.", @@ -30,6 +30,51 @@ } } } + }, + "menuItems": { + "description": "Menu item definitions.", + "type": "object", + "additionalProperties": { + "description": "An item.", + "type": "object", + "properties": { + "labelTranslation": { + "type": "string", + "description": "A label translation path." + }, + "link": { + "type": "string", + "description": "A link." + }, + "order": { + "type": "integer", + "description": "An order position." + }, + "groupIndex": { + "type": "integer", + "description": "A group index. Groups are separated by a divider." + }, + "disabled": { + "type": "boolean", + "description": "An item will be hidden." + }, + "handler": { + "type": "string", + "description": "A handler class." + }, + "actionFunction": { + "type": "string", + "description": "An action function in the handler." + }, + "configCheck": { + "type": "string", + "description": "A config path to check. Path items are separated by a dot. If a config value is not empty, then the action is allowed. The `!` prefix reverses the check." + }, + "accessDataList": { + "$ref": "../clientDefs.json#/definitions/accessDataList" + } + } + } } } }