Skip to content

Commit

Permalink
stored text search
Browse files Browse the repository at this point in the history
  • Loading branch information
yurikuzn committed Apr 25, 2023
1 parent 1f2c895 commit 26634de
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 1 deletion.
148 changes: 148 additions & 0 deletions client/src/helpers/misc/stored-text-search.js
@@ -0,0 +1,148 @@
/************************************************************************
* 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.
************************************************************************/

define('helpers/misc/stored-text-search', [], function () {

/**
* @memberOf module:helpers/misc/stored-text-search
*/
class Class {
/**
* @param {module:storage.Class} storage
* @param {string} scope
* @param {Number} [maxCount]
*/
constructor(scope, storage, maxCount) {
this.scope = scope;
this.storage = storage;
this.key = 'textSearches';
this.maxCount = maxCount || 100;
/** @type {string[]|null} */
this.list = null;
}

/**
* Match.
*
* @param {string} text
* @param {Number} [limit]
* @return {string[]}
*/
match(text, limit) {
text = text.toLowerCase().trim();

let list = this.get();
let matchedList = [];

for (let item of list) {
if (item.toLowerCase().startsWith(text)) {
matchedList.push(item);
}

if (limit !== undefined && matchedList.length === limit) {
break;
}
}

return matchedList;
}

/**
* Get stored text filters.
*
* @private
* @return {string[]}
*/
get() {
if (this.list === null) {
this.list = this.getFromStorage();
}

return this.list;
}

/**
* @private
* @return {string[]}
*/
getFromStorage() {
/** @var {string[]} */
return this.storage.get(this.key, this.scope) || [];
}

/**
* Store a text filter.
*
* @param {string} text
*/
store(text) {
text = text.trim();

let list = this.getFromStorage();

let index = list.indexOf(text);

if (index !== -1) {
list.splice(index, 1);
}

list.unshift(text);

if (list.length > this.maxCount) {
list = list.slice(0, this.maxCount);
}

this.list = list;
this.storage.set(this.key, this.scope, list);
}

/**
* Remove a text filter.
*
* @param {string} text
*/
remove(text) {
text = text.trim();

let list = this.getFromStorage();

let index = list.indexOf(text);

if (index === -1) {
return;
}

list.splice(index, 1);

this.list = list;
this.storage.set(this.key, this.scope, list);
}
}

return Class;
});
3 changes: 3 additions & 0 deletions client/src/search-manager.js
Expand Up @@ -349,6 +349,9 @@ define('search-manager', [], function () {
this.data = data;

if (this.storage) {
data = Espo.Utils.clone(data);
delete data['textFilter'];

this.storage.set(this.type + 'Search', this.scope, data);
}
},
Expand Down
92 changes: 91 additions & 1 deletion client/src/views/record/search.js
Expand Up @@ -26,7 +26,7 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/

define('views/record/search', ['view'], function (Dep) {
define('views/record/search', ['view', 'helpers/misc/stored-text-search'], function (Dep, StoredTextSearch) {

/**
* @class
Expand All @@ -39,6 +39,7 @@ define('views/record/search', ['view'], function (Dep) {
template: 'record/search',

scope: null,
/** @type {module:search-manager.Class} */
searchManager: null,
fieldFilterList: null,
/** @type {Object.<string, string>|null}*/
Expand All @@ -63,6 +64,8 @@ define('views/record/search', ['view'], function (Dep) {

FIELD_QUICK_SEARCH_COUNT_THRESHOLD: 4,

autocompleteLimit: 7,

data: function () {
return {
scope: this.scope,
Expand Down Expand Up @@ -90,8 +93,15 @@ define('views/record/search', ['view'], function (Dep) {
this.entityType = this.collection.name;
this.scope = this.options.scope || this.entityType;

/** @type {module:search-manager.Class} */
this.searchManager = this.options.searchManager;

/**
* @type {module:helpers/misc/stored-text-search.Class}
* @private
*/
this.storedTextSearchHelper = new StoredTextSearch(this.scope, this.getHelper().storage);

this.textFilterDisabled = this.options.textFilterDisabled || this.textFilterDisabled ||
this.getMetadata().get(['clientDefs', this.scope, 'textFilterDisabled']);

Expand Down Expand Up @@ -671,6 +681,8 @@ define('views/record/search', ['view'], function (Dep) {
this.$fieldQuickSearch = this.$filterList.find('input.field-filter-quick-search-input');
/** @type {JQuery} */
this.$addFilterButton = this.$el.find('button.add-filter-button');
/** @type {JQuery} */
this.$textFilter = this.$el.find('input.text-filter');

this.updateAddFilterButton();

Expand All @@ -679,6 +691,75 @@ define('views/record/search', ['view'], function (Dep) {
this.manageLabels();
this.controlResetButtonVisibility();
this.initQuickSearchUi();
this.initTextSearchAutocomplete();
},

initTextSearchAutocomplete: function () {
/*this.$textFilter.on('blur.autocomplete', e => {
e.stopPropagation();
e.preventDefault();
console.log('b');
});*/

let options = {
minChars: 0,
noCache: true,
triggerSelectOnValidInput: false,
beforeRender: ($container) => {
$container.addClass('text-search-suggestions');

$container.find('a[data-action="clearStoredTextSearch"]').on('click', e => {
e.stopPropagation();
e.preventDefault();

let text = e.currentTarget.getAttribute('data-value');

this.storedTextSearchHelper.remove(text);

setTimeout(() => {
this.$textFilter.focus();
}, 205);
});
},
formatResult: item => {
return $('<span>')
.append(
$('<a>')
.attr('data-action', 'clearStoredTextSearch')
.attr('role', 'button')
.attr('data-value', item.value)
.attr('title', this.translate('Remove'))
.html('<span class="fas fa-times fa-sm"></span>')
.addClass('pull-right text-soft'),
$('<span>')
.text(item.value)
)
.get(0).innerHTML;
},
lookup: (text, done) => {
let suggestions = this.storedTextSearchHelper.match(text, this.autocompleteLimit)
.map(item => {
return {value: item};
});

done({suggestions: suggestions});
},
onSelect: () => {
this.$textFilter.focus();
this.$textFilter.autocomplete('hide');
},
};

this.$textFilter.autocomplete(options);

this.$textFilter.on('focus', () => {
if (this.$textFilter.val()) {
this.$textFilter.autocomplete('hide');
}
});

this.once('render', () => this.$textFilter.autocomplete('dispose'));
this.once('remove', () => this.$textFilter.autocomplete('dispose'));
},

initQuickSearchUi: function () {
Expand Down Expand Up @@ -839,6 +920,7 @@ define('views/record/search', ['view'], function (Dep) {
this.updateSearch();
this.updateCollection();
this.controlResetButtonVisibility();
this.storeTextSearch();

this.isSearchedWithAdvancedFilter = this.hasAdvancedFilter();
},
Expand Down Expand Up @@ -1197,5 +1279,13 @@ define('views/record/search', ['view'], function (Dep) {
.find('[data-toggle="dropdown"]')
.dropdown('toggle');
},

storeTextSearch: function () {
if (!this.textFilter) {
return;
}

this.storedTextSearchHelper.store(this.textFilter);
},
});
});
21 changes: 21 additions & 0 deletions frontend/less/espo/custom.less
Expand Up @@ -3748,6 +3748,27 @@ a.close:hover {
}
}

body > .autocomplete-suggestions.text-search-suggestions {
> .autocomplete-suggestion {
padding-top: 0;
padding-bottom: 0;

> span {
text-overflow: ellipsis;
overflow: hidden;
display: inline-block;
width: ~"calc(100% - 20px)";
height: 24px;
padding-top: 4px;
padding-bottom: 4px;
}

> a {
margin-top: 3px;
}
}
}

@import "elements/site.less";
@import "elements/modal.less";
@import "elements/buttons.less";
Expand Down

0 comments on commit 26634de

Please sign in to comment.