Limit number of accounts (& other select/taglist search results) sent to client

This commit is contained in:
nathan 2022-10-04 15:49:07 -06:00
parent 7bb4332961
commit f8e56edc20
5 changed files with 78 additions and 22 deletions

View File

@ -33,17 +33,14 @@ export class Et2SelectAccount extends Et2Select
{
super();
// currently only account_selection "Primary group and search" needs the search,
// all other types have the accounts fully local
if (this.egw().preference('account_selection', 'common') === 'primary_group')
// all types can search the server. If there are a lot of accounts, local list will
// not be complete
if(this.egw().preference('account_selection', 'common') !== 'none')
{
this.searchUrl = "EGroupware\\Api\\Etemplate\\Widget\\Taglist::ajax_search";
}
else // always allow local search
{
this.search = true;
}
this.searchOptions = { type: 'account', account_type: 'accounts' };
this.searchOptions = {type: 'account', account_type: 'accounts'};
this.__accountType = 'accounts';
}

View File

@ -230,8 +230,29 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
protected validators : Validator[];
private _searchTimeout : number;
/**
* When user is typing, we wait this long for them to be finished before we start the search
* @type {number}
* @protected
*/
protected static SEARCH_TIMEOUT = 500;
/**
* We need at least this many characters before we start the search
*
* @type {number}
* @protected
*/
protected static MIN_CHARS = 2;
/**
* Limit server searches to 100 results, matches Link::DEFAULT_NUM_ROWS
* @type {number}
*/
static RESULT_LIMIT : number = 100;
// Hold the original option data from earlier search results, since we discard on subsequent search
private _selected_remote = <SelectOption[]>[];
@ -1019,8 +1040,13 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
*/
protected remoteQuery(search : string, options : object)
{
// Include a limit, even if options don't, to avoid massive lists breaking the UI
let sendOptions = {
num_rows: Et2WidgetWithSearch.RESULT_LIMIT,
...options
}
return this.egw().request(this.egw().link(this.egw().ajaxUrl(this.egw().decodePath(this.searchUrl)),
{query: search, ...options}), [search, options]).then((result) =>
{query: search, ...sendOptions}), [search, sendOptions]).then((result) =>
{
this.processRemoteResults(result);
});
@ -1033,8 +1059,16 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
*/
protected processRemoteResults(results)
{
// If results have a total included, pull it out.
// It will cause errors if left in the results
let total = null;
if(typeof results.total !== "undefined")
{
total = results.total;
delete results.total;
}
let entries = cleanSelectOptions(results);
let resultCount = entries.length;
if(entries.length == 0)
{
@ -1073,12 +1107,23 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
temp_target.querySelectorAll(":scope > *").forEach((item) =>
{
// Avoid duplicate error
if(!target.querySelector("[value='" + item.value.replace(/'/g, "\\\'") + "']"))
if(!target.querySelector("[value='" + ('' + item.value).replace(/'/g, "\\\'") + "']"))
{
target.appendChild(item);
}
})
this.handleMenuSlotChange();
})
.then(() =>
{
if(total && total > resultCount)
{
// More results available that were not sent
let count = document.createElement("span")
count.classList.add("remote");
count.textContent = this.egw().lang("%1 more...", total - resultCount);
target.appendChild(count);
}
});
}
}

View File

@ -427,11 +427,12 @@ class Accounts
}
$accounts = array();
foreach(self::getInstance()->search(array(
'type' => $options['filter']['group'] < 0 ? $options['filter']['group'] : $type,
'query' => $pattern,
'query_type' => 'all',
'order' => $order,
)) as $account)
'type' => $options['filter']['group'] < 0 ? $options['filter']['group'] : $type,
'query' => $pattern,
'query_type' => 'all',
'order' => $order,
'offset' => $options['num_rows']
)) as $account)
{
$displayName = self::format_username($account['account_lid'],
$account['account_firstname'],$account['account_lastname'],$account['account_id']);
@ -452,6 +453,11 @@ class Accounts
$accounts[$account['account_id']] = $displayName;
}
}
// If limited rows were requested, send the total number of rows
if(array_key_exists('num_rows', $options))
{
$options['total'] = self::getInstance()->total;
}
return $accounts;
}

View File

@ -87,13 +87,20 @@ class Taglist extends Etemplate\Widget
$results[] = ['value' => $id, 'label' => $name];
}
}
usort($results, static function ($a, $b) use ($query) {
similar_text($query, $a["label"], $percent_a);
similar_text($query, $b["label"], $percent_b);
return $percent_a === $percent_b ? 0 : ($percent_a > $percent_b ? -1 : 1);
usort($results, static function ($a, $b) use ($query)
{
similar_text($query, $a["label"], $percent_a);
similar_text($query, $b["label"], $percent_b);
return $percent_a === $percent_b ? 0 : ($percent_a > $percent_b ? -1 : 1);
});
// switch regular JSON response handling off
// If we have a total, include it too so client knows if results were limited
if(array_key_exists('total', $options))
{
$results['total'] = intval($options['total']);
}
// switch regular JSON response handling off
Api\Json\Request::isJSONRequest(false);
header('Content-Type: application/json; charset=utf-8');

View File

@ -1662,7 +1662,8 @@ abstract class Framework extends Framework\Extra
// close session now, to not block other user actions
$GLOBALS['egw']->session->commit_session();
$list = array('accounts' => array(),'groups' => array(), 'owngroups' => array());
$list = array('accounts' => array('num_rows' => Link::DEFAULT_NUM_ROWS), 'groups' => array(),
'owngroups' => array());
if($GLOBALS['egw_info']['user']['preferences']['common']['account_selection'] == 'primary_group')
{
$list['accounts']['filter']['group'] = $GLOBALS['egw_info']['user']['account_primary_group'];