tab-quick-search
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user