tab-quick-search

This commit is contained in:
Yuri Kuznetsov
2024-10-07 15:37:08 +03:00
parent 7714a299aa
commit 0476812791
7 changed files with 317 additions and 43 deletions
@@ -88,6 +88,7 @@ return [
'sqlFailed' => false,
],
'authenticationMethod' => 'Espo',
'tabQuickSearch' => true,
'globalSearchEntityList' => [
'Account',
'Contact',
@@ -97,19 +97,22 @@
"url": "#Admin/users",
"label": "Users",
"iconClass": "fas fa-user",
"description": "users"
"description": "users",
"tabQuickSearch": true
},
{
"url": "#Admin/teams",
"label": "Teams",
"iconClass": "fas fa-users",
"description": "teams"
"description": "teams",
"tabQuickSearch": true
},
{
"url": "#Admin/roles",
"label": "Roles",
"iconClass": "fas fa-key",
"description": "roles"
"description": "roles",
"tabQuickSearch": true
},
{
"url": "#Admin/authLog",
@@ -145,7 +148,8 @@
"url": "#Admin/entityManager",
"label": "Entity Manager",
"iconClass": "fas fa-tools",
"description": "entityManager"
"description": "entityManager",
"tabQuickSearch": true
},
{
"url": "#Admin/layouts",
@@ -238,7 +242,8 @@
"url": "#Admin/portalUsers",
"label": "Portal Users",
"iconClass": "fas fa-user",
"description": "portalUsers"
"description": "portalUsers",
"tabQuickSearch": true
},
{
"url": "#Admin/portalRoles",
@@ -256,7 +261,8 @@
"url": "#Admin/workingTimeCalendar",
"label": "Working Time Calendars",
"iconClass": "far fa-calendar-alt",
"description": "workingTimeCalendars"
"description": "workingTimeCalendars",
"tabQuickSearch": true
},
{
"url": "#Admin/layoutSets",
@@ -6,6 +6,7 @@
autocomplete="espo-global-search"
spellcheck="false"
>
{{#if hasSearchButton}}
<div class="input-group-btn">
<a
class="btn btn-link global-search-button"
@@ -13,5 +14,6 @@
title="{{translate 'Search'}}"
><span class="fas fa-search icon"></span></a>
</div>
{{/if}}
</div>
<div class="global-search-panel-container"></div>
+74 -2
View File
@@ -34,8 +34,9 @@ export default class TabsHelper {
* @param {import('models/user').default} user
* @param {import('acl-manager').default} acl
* @param {import('metadata').default} metadata
* @param {import('language').default} language
*/
constructor(config, preferences, user, acl, metadata) {
constructor(config, preferences, user, acl, metadata, language) {
/** @private */
this.config = config;
/** @private */
@@ -46,12 +47,22 @@ export default class TabsHelper {
this.acl = acl;
/** @private */
this.metadata = metadata;
/** @private */
this.language = language;
}
/**
* @typedef {Object} TabsHelper~item
* @property {string} [url]
* @property {string} [text]
* @property {'url'|'divider'} [type]
* @property {(TabsHelper~item|string)[]} [itemList]
*/
/**
* Get the tab list.
*
* @return {(Object|string)[]}
* @return {(TabsHelper~item|string)[]}
*/
getTabList() {
let tabList = this.preferences.get('useCustomTabList') && !this.preferences.get('addCustomTabs') ?
@@ -96,6 +107,67 @@ export default class TabsHelper {
}
/**
* Is a tab a group.
*
* @param {string|{type?: string}} item
*/
isTabGroup(item) {
if (!this.isTabDivider(item) && !this.isTabUrl(item) && typeof item === 'object') {
return true;
}
return false;
}
/**
* Is a tab a scope.
*
* @param {string|{type?: string}} item
*/
isTabScope(item) {
if (typeof item === 'object' || this.isTabMoreDelimiter(item) || item === 'Home') {
return false;
}
return true;
}
/**
* Get a translated tab label.
*
* @param {{text?: string}|string} item
*/
getTranslatedTabLabel(item) {
const translateLabel = label => {
if (label.indexOf('$') === 0) {
return this.language.translate(label.slice(1), 'navbarTabs');
}
return label;
};
if (this.isTabDivider(item) || this.isTabUrl(item) || this.isTabUrl(item) || this.isTabGroup(item)) {
if (item.text) {
return translateLabel(item.text);
}
return ''
}
if (item === 'Home') {
return this.language.translate('Home');
}
if (typeof item === 'object') {
return '';
}
return this.language.translate(item, 'scopeNamesPlural');
}
/**
* Check tab access.
*
* @param {Record|string} item
* @return {boolean}
*/
@@ -27,6 +27,10 @@
************************************************************************/
import View from 'view';
import Autocomplete from 'ui/autocomplete';
import TabsHelper from 'helpers/site/tabs';
/** @module views/global-search/global-search */
class GlobalSearchView extends View {
@@ -38,6 +42,48 @@ class GlobalSearchView extends View {
*/
containerElement
/**
* @private
* @type {HTMLInputElement}
*/
inputElement
/**
* @private
* @type {boolean}
*/
tabQuickSearch
/**
* @private
* @type {boolean}
*/
hasGlobalSearch
/**
* @private
* @type {TabsHelper}
*/
tabsHelper
/**
* @private
* @type {Autocomplete}
*/
autocomplete
/**
* @private
* @type {module:views/global-search/global-search~tabData[]}
*/
tabDataList
data() {
return {
hasSearchButton: this.hasGlobalSearch,
};
}
setup() {
this.addHandler('keydown', 'input.global-search-input', 'onKeydown');
this.addHandler('focus', 'input.global-search-input', 'onFocus');
@@ -54,10 +100,25 @@ class GlobalSearchView extends View {
this.onMouseUpBind = this.onMouseUp.bind(this);
this.onClickBind = this.onClick.bind(this);
this.tabQuickSearch = this.getConfig().get('tabQuickSearch') || false;
this.hasGlobalSearch = (this.getConfig().get('globalSearchEntityList') || []).length > 0;
this.tabsHelper = new TabsHelper(
this.getConfig(),
this.getPreferences(),
this.getUser(),
this.getAcl(),
this.getMetadata(),
this.getLanguage()
);
this.tabDataList = this.getTabDataList();
}
/**
* @param {MouseEvent} e
* @private
*/
onFocus(e) {
const inputElement = /** @type {HTMLInputElement} */e.target;
@@ -67,8 +128,13 @@ class GlobalSearchView extends View {
/**
* @param {KeyboardEvent} e
* @private
*/
onKeydown(e) {
if (!this.hasGlobalSearch) {
return;
}
const key = Espo.Utils.getKeyFromKeyEvent(e);
if (e.code === 'Enter' || key === 'Enter' || key === 'Control+Enter') {
@@ -84,6 +150,53 @@ class GlobalSearchView extends View {
afterRender() {
this.$input = this.$el.find('input.global-search-input');
this.inputElement = this.$input.get(0);
if (this.tabQuickSearch) {
this.autocomplete = new Autocomplete(this.inputElement, {
minChars: 1,
lookupFunction: async query => {
const lower = query.toLowerCase();
return this.tabDataList
.filter(it => {
if (it.words.find(word => word.startsWith(lower))) {
return true;
}
if (it.lowerLabel.toLowerCase().startsWith(lower)) {
return true;
}
return false;
})
.map(it => ({
value: it.label,
url: it.url,
}));
},
formatResult: /** {value: string, url: string} */item => {
const a = document.createElement('a');
a.text = item.value;
a.href = item.url;
a.classList.add('text-default');
return a.outerHTML;
},
onSelect: /** {value: string, url: string} */item => {
window.location.href =item.url;
this.inputElement.value = '';
},
});
this.once('render remove', () => {
this.autocomplete.dispose();
this.autocomplete = undefined;
});
}
}
/**
@@ -151,6 +264,10 @@ class GlobalSearchView extends View {
showPanel() {
this.closePanel();
if (this.autocomplete) {
this.autocomplete.hide();
}
if (this.closeNavbarOnShow) {
this.$el.closest('.navbar-body').removeClass('in');
}
@@ -189,6 +306,97 @@ class GlobalSearchView extends View {
document.removeEventListener('mouseup', this.onMouseUpBind);
document.removeEventListener('click', this.onClickBind);
}
/**
* @typedef {Object} module:views/global-search/global-search~tabData
* @property {string} url
* @property {string} label
* @property {string} lowerLabel
* @property {string[]} words
*/
/**
* @private
* @return {module:views/global-search/global-search~tabData[]}
*/
getTabDataList() {
/** @type {module:views/global-search/global-search~tabData[]}*/
const list = [];
/**
*
* @param {string|TabsHelper~item} item
* @return {module:views/global-search/global-search~tabData}
*/
const toData = (item) => {
const label = this.tabsHelper.getTranslatedTabLabel(item);
const url = this.tabsHelper.isTabScope(item) ? `#${item}` : item.url;
return {
url: url,
label: label,
words: label.split(' ').map(it => it.toLowerCase()),
lowerLabel: label.toLowerCase(),
};
};
const checkTab = (item) => {
return (this.tabsHelper.isTabScope(item) || this.tabsHelper.isTabUrl(item)) &&
this.tabsHelper.checkTabAccess(item)
}
for (const item of this.tabsHelper.getTabList()) {
if (checkTab(item)) {
list.push(toData(item));
continue;
}
if (this.tabsHelper.isTabGroup(item)) {
for (const subItem of item.itemList) {
if (checkTab(subItem)) {
list.push(toData(subItem));
}
}
}
}
if (this.getUser().isAdmin()) {
/** @type {
* Record<string, {
* order?: number,
* itemList: {
* url: string,
* tabQuickSearch: boolean,
* label: string,
* }[]
* }>
* } panels */
const panels = this.getMetadata().get(`app.adminPanel`) || {};
Object.entries(panels)
.map(it => it[1])
.sort((a, b) => a.order - b.order)
.forEach(it => {
it.itemList
.filter(it => it.tabQuickSearch && it.label)
.forEach(it => {
const label = this.translate(it.label, 'labels', 'Admin');
list.push({
label: this.translate(it.label, 'labels', 'Admin'),
url: it.url,
lowerLabel: label.toLowerCase(),
words: label.split(' ').map(it => it.toLowerCase()),
});
});
});
}
return list.filter((item, index) => list.findIndex(it => it.lowerLabel === item.lowerLabel) === index)
}
}
export default GlobalSearchView;
+16 -35
View File
@@ -386,7 +386,8 @@ class NavbarSiteView extends View {
this.getPreferences(),
this.getUser(),
this.getAcl(),
this.getMetadata()
this.getMetadata(),
this.getLanguage()
);
const itemDefs = this.getMetadata().get('app.clientNavbar.items') || {};
@@ -506,6 +507,10 @@ class NavbarSiteView extends View {
* @private
*/
setupGlobalSearch() {
if (this.getConfig().get('tabQuickSearch')) {
return;
}
let isAvailable = false;
/** @type {string[]} */
@@ -1154,7 +1159,7 @@ class NavbarSiteView extends View {
* colorsDisabled: boolean,
* tabIconsDisabled: boolean,
* }} params
* @param {Object|string} tab
* @param {Record|string} tab
* @param {number} i
* @param {Object} vars
* @return {{
@@ -1172,7 +1177,6 @@ class NavbarSiteView extends View {
* }}
*/
prepareTabItemDefs(params, tab, i, vars) {
let label;
let link;
let iconClass = null;
@@ -1183,62 +1187,39 @@ class NavbarSiteView extends View {
let name = tab;
let aClassName = 'nav-link';
const translateLabel = label => {
if (label.indexOf('$') === 0) {
return this.translate(label.slice(1), 'navbarTabs');
}
return label;
};
const label = this.tabsHelper.getTranslatedTabLabel(tab);
if (tab === 'Home') {
label = this.getLanguage().translate(tab);
link = '#';
} else if (typeof tab === 'object' && tab.type === 'divider') {
} else if (this.tabsHelper.isTabDivider(tab)) {
isDivider = true;
label = tab.text;
aClassName = 'nav-divider-text';
name = 'divider-' + i;
if (label) {
label = translateLabel(label);
}
} else if (typeof tab === 'object' && tab.type === 'url') {
aClassName = 'nav-divider-text';
name = `divider-${i}`;
} else if (this.tabsHelper.isTabUrl(tab)) {
isUrl = true;
label = tab.text || '#';
name = 'url-' + i;
name = `url-${i}`;
link = tab.url || '#';
color = tab.color;
iconClass = tab.iconClass;
if (label) {
label = translateLabel(label);
}
this.urlList.push({name: name, url: link});
} else if (typeof tab === 'object') {
} else if (this.tabsHelper.isTabGroup(tab)) {
isGroup = true;
label = tab.text || '';
color = tab.color;
iconClass = tab.iconClass;
name = 'group-' + i;
name = `group-${i}`;
link = null;
aClassName = 'nav-link-group';
if (label) {
label = translateLabel(label);
}
} else {
label = this.getLanguage().translate(tab, 'scopeNamesPlural');
link = '#' + tab;
}
label = label || '';
const shortLabel = label.substring(0, 2);
if (!params.colorsDisabled && !isGroup && !isDivider) {
+4
View File
@@ -44,6 +44,10 @@
"recordView": {
"type": "string",
"description": "A record view. Optional. Needed if you defined the URL like #Admin/{myName}. The record view displays settings fields. https://docs.espocrm.com/development/custom-config-parameters/"
},
"tabQuickSearch": {
"type": "boolean",
"description": "Makes available in the navbar tab quick search. As if v8.5."
}
}
}