diff --git a/.idea/jsonSchemas.xml b/.idea/jsonSchemas.xml
index 9a57f9a1e3..46e872e544 100644
--- a/.idea/jsonSchemas.xml
+++ b/.idea/jsonSchemas.xml
@@ -854,6 +854,25 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.vscode/settings.json b/.vscode/settings.json
index eee86920e9..5be8a352d8 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -352,6 +352,12 @@
],
"url": "./schema/metadata/app/metadata.json"
},
+ {
+ "fileMatch": [
+ "*/Resources/metadata/app/clientNavbar.json"
+ ],
+ "url": "./schema/metadata/app/clientNavbar.json"
+ },
{
"fileMatch": [
"*/Resources/metadata/app/orm.json"
diff --git a/application/Espo/Resources/metadata/app/clientNavbar.json b/application/Espo/Resources/metadata/app/clientNavbar.json
new file mode 100644
index 0000000000..8a4284e07f
--- /dev/null
+++ b/application/Espo/Resources/metadata/app/clientNavbar.json
@@ -0,0 +1,11 @@
+{
+ "itemDefs": {
+ "quickCreate": {
+ "view": "views/site/navbar/quick-create",
+ "class": "dropdown hidden-xs quick-create-container"
+ }
+ },
+ "itemList": [
+ "quickCreate"
+ ]
+}
diff --git a/client/res/templates/site/navbar.tpl b/client/res/templates/site/navbar.tpl
index f5108abc28..94e23b293f 100644
--- a/client/res/templates/site/navbar.tpl
+++ b/client/res/templates/site/navbar.tpl
@@ -174,28 +174,9 @@
{{{globalSearch}}}
- {{#if enableQuickCreate}}
-
-
-
-
- {{/if}}
+ {{#each itemDataList}}
+ {{{var key ../this}}}
+ {{/each}}
{{{notificationsBadge}}}
diff --git a/client/src/views/site/navbar.js b/client/src/views/site/navbar.js
index 365730a15e..02b92d0efc 100644
--- a/client/src/views/site/navbar.js
+++ b/client/src/views/site/navbar.js
@@ -56,14 +56,6 @@ class NavbarSiteView extends View {
this.xsCollapse();
},
/** @this NavbarSiteView */
- 'click a[data-action="quick-create"]': function (e) {
- e.preventDefault();
-
- const scope = $(e.currentTarget).data('name');
-
- this.quickCreate(scope);
- },
- /** @this NavbarSiteView */
'click a.minimizer': function () {
this.switchMinimizer();
},
@@ -100,10 +92,9 @@ class NavbarSiteView extends View {
tabDefsList2: this.tabDefsList.filter(item => item.isInMore),
title: this.options.title,
menuDataList: this.getMenuDataList(),
- quickCreateList: this.quickCreateList,
- enableQuickCreate: this.quickCreateList.length > 0,
userId: this.getUser().id,
logoSrc: this.getLogoSrc(),
+ itemDataList: this.getItemDataList(),
};
}
@@ -360,10 +351,6 @@ class NavbarSiteView extends View {
return tabList;
}
- getQuickCreateList() {
- return this.getConfig().get('quickCreateList') || [];
- }
-
setup() {
this.getRouter().on('routed', (e) => {
if (e.controller) {
@@ -375,30 +362,29 @@ class NavbarSiteView extends View {
this.selectTab(false);
});
+ /** @type {string[]} */
+ this.itemList = this.getMetadata().get(['app', 'clientNavbar', 'itemList']) || [];
+
this.createView('notificationsBadge', 'views/notification/badge', {
selector: '.notifications-badge-container',
});
const setup = () => {
- this.setupQuickCreateList();
this.setupTabDefsList();
+
+ return Promise
+ .all(this.itemList.map(item => this.createItemView(item)));
+ };
+
+ const update = () => {
+ setup().then(() => this.reRender());
};
this.setupGlobalSearch();
-
setup();
- this.listenTo(this.getHelper().settings, 'sync', () => {
- setup();
-
- this.reRender();
- });
-
- this.listenTo(this.getHelper().language, 'sync', () => {
- setup();
-
- this.reRender();
- });
+ this.listenTo(this.getHelper().settings, 'sync', () => update());
+ this.listenTo(this.getHelper().language, 'sync', () => update());
this.once('remove', () => {
$(window).off('resize.navbar');
@@ -409,24 +395,61 @@ class NavbarSiteView extends View {
});
}
- setupQuickCreateList() {
- const scopes = this.getMetadata().get('scopes') || {};
+ getItemDataList() {
+ const defsMap = {};
- this.quickCreateList = this.getQuickCreateList().filter(scope =>{
- if (!scopes[scope]) {
- return false;
- }
-
- if ((scopes[scope] || {}).disabled) {
- return;
- }
-
- if ((scopes[scope] || {}).acl) {
- return this.getAcl().check(scope, 'create');
- }
-
- return true;
+ this.itemList.forEach(name => {
+ defsMap[name] = this.getItemDefs(name);
});
+
+ return this.itemList
+ .filter(name => {
+ const defs = defsMap[name];
+
+ if (!defs) {
+ return false;
+ }
+
+ const view = this.getView(name + 'Item');
+
+ if ('isAvailable' in view) {
+ return view.isAvailable();
+ }
+
+ return true;
+ })
+ .map(name => {
+ return {
+ key: name + 'Item',
+ name: name,
+ class: defsMap[name].class || '',
+ };
+ });
+ }
+
+ /**
+ *
+ * @param {string} name
+ * @return {{view: string, class: string}}
+ */
+ getItemDefs(name) {
+ return this.getMetadata().get(['app', 'clientNavbar', 'itemDefs', name]);
+ }
+
+ /**
+ * @param {string} name
+ * @return {Promise}
+ */
+ createItemView(name) {
+ const defs = this.getItemDefs(name)
+
+ if (!defs || !defs.view) {
+ return Promise.resolve();
+ }
+
+ const key = name + 'Item';
+
+ return this.createView(key, defs.view, {selector: `[data-item="${name}"]`});
}
filterTabItem(scope) {
@@ -1200,19 +1223,6 @@ class NavbarSiteView extends View {
return list;
}
- quickCreate(scope) {
- Espo.Ui.notify(' ... ');
-
- const type = this.getMetadata().get(['clientDefs', scope, 'quickCreateModalType']) || 'edit';
- const viewName = this.getMetadata().get(['clientDefs', scope, 'modalViews', type]) || 'views/modals/edit';
-
- this.createView('quickCreate', viewName , {scope: scope}, (view) => {
- view.once('after:render', () => Espo.Ui.notify(false));
-
- view.render();
- });
- }
-
// noinspection JSUnusedGlobalSymbols
actionLogout() {
this.getRouter().logout();
diff --git a/client/src/views/site/navbar/quick-create.js b/client/src/views/site/navbar/quick-create.js
new file mode 100644
index 0000000000..a55ad72459
--- /dev/null
+++ b/client/src/views/site/navbar/quick-create.js
@@ -0,0 +1,105 @@
+/************************************************************************
+ * 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.
+ ************************************************************************/
+
+import View from 'view';
+
+class QuickCreateNavbarView extends View {
+
+ templateContent = `
+
+
+ `
+
+ data() {
+ return {
+ list: this.list,
+ };
+ }
+
+ setup() {
+ this.addActionHandler('quickCreate', (e, element) => {
+ e.preventDefault();
+
+ this.processCreate(element.dataset.name);
+ });
+
+ const scopes = this.getMetadata().get('scopes') || {};
+
+ /** @type {string[]} */
+ const list = this.getConfig().get('quickCreateList') || [];
+
+ this.list = list.filter(scope => {
+ if (!scopes[scope]) {
+ return false;
+ }
+
+ if ((scopes[scope] || {}).disabled) {
+ return;
+ }
+
+ if ((scopes[scope] || {}).acl) {
+ return this.getAcl().check(scope, 'create');
+ }
+
+ return true;
+ });
+ }
+
+ isAvailable() {
+ return this.list.length > 0;
+ }
+
+ processCreate(scope) {
+ Espo.Ui.notify(' ... ');
+
+ const type = this.getMetadata().get(['clientDefs', scope, 'quickCreateModalType']) || 'edit';
+ const viewName = this.getMetadata().get(['clientDefs', scope, 'modalViews', type]) || 'views/modals/edit';
+
+ this.createView('dialog', viewName , {scope: scope})
+ .then(view => view.render())
+ .then(() => Espo.Ui.notify(false));
+ }
+}
+
+export default QuickCreateNavbarView;
diff --git a/schema/metadata/app/clientNavbar.json b/schema/metadata/app/clientNavbar.json
new file mode 100644
index 0000000000..5d11fff53d
--- /dev/null
+++ b/schema/metadata/app/clientNavbar.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://www.espocrm.com/schema/metadata/app/client.json",
+ "title": "app/navbar",
+ "description": "Navbar definitions.",
+ "type": "object",
+ "properties": {
+ "itemList": {
+ "type": "array",
+ "description": "Navbar items.",
+ "items": {
+ "type": "string",
+ "anyOf": [
+ {"type": "string"},
+ {"const": "__APPEND__"}
+ ]
+ }
+ },
+ "itemDefs": {
+ "description": "Navbar item definitions",
+ "type": "object",
+ "additionalProperties": {
+ "description": "An item.",
+ "type": "object",
+ "properties": {
+ "view": {
+ "type": "string",
+ "description": "A view."
+ },
+ "class": {
+ "type": "string",
+ "description": "A CSS class."
+ }
+ }
+ }
+ }
+ }
+}