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

View File

@ -230,8 +230,29 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
protected validators : Validator[]; protected validators : Validator[];
private _searchTimeout : number; 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; 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; 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 // Hold the original option data from earlier search results, since we discard on subsequent search
private _selected_remote = <SelectOption[]>[]; private _selected_remote = <SelectOption[]>[];
@ -1019,8 +1040,13 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
*/ */
protected remoteQuery(search : string, options : object) 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)), 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); this.processRemoteResults(result);
}); });
@ -1033,8 +1059,16 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
*/ */
protected processRemoteResults(results) 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 entries = cleanSelectOptions(results);
let resultCount = entries.length;
if(entries.length == 0) if(entries.length == 0)
{ {
@ -1073,12 +1107,23 @@ export const Et2WithSearchMixin = <T extends Constructor<LitElement>>(superclass
temp_target.querySelectorAll(":scope > *").forEach((item) => temp_target.querySelectorAll(":scope > *").forEach((item) =>
{ {
// Avoid duplicate error // Avoid duplicate error
if(!target.querySelector("[value='" + item.value.replace(/'/g, "\\\'") + "']")) if(!target.querySelector("[value='" + ('' + item.value).replace(/'/g, "\\\'") + "']"))
{ {
target.appendChild(item); target.appendChild(item);
} }
}) })
this.handleMenuSlotChange(); 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(); $accounts = array();
foreach(self::getInstance()->search(array( foreach(self::getInstance()->search(array(
'type' => $options['filter']['group'] < 0 ? $options['filter']['group'] : $type, 'type' => $options['filter']['group'] < 0 ? $options['filter']['group'] : $type,
'query' => $pattern, 'query' => $pattern,
'query_type' => 'all', 'query_type' => 'all',
'order' => $order, 'order' => $order,
)) as $account) 'offset' => $options['num_rows']
)) as $account)
{ {
$displayName = self::format_username($account['account_lid'], $displayName = self::format_username($account['account_lid'],
$account['account_firstname'],$account['account_lastname'],$account['account_id']); $account['account_firstname'],$account['account_lastname'],$account['account_id']);
@ -452,6 +453,11 @@ class Accounts
$accounts[$account['account_id']] = $displayName; $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; return $accounts;
} }

View File

@ -87,13 +87,20 @@ class Taglist extends Etemplate\Widget
$results[] = ['value' => $id, 'label' => $name]; $results[] = ['value' => $id, 'label' => $name];
} }
} }
usort($results, static function ($a, $b) use ($query) { usort($results, static function ($a, $b) use ($query)
similar_text($query, $a["label"], $percent_a); {
similar_text($query, $b["label"], $percent_b); similar_text($query, $a["label"], $percent_a);
return $percent_a === $percent_b ? 0 : ($percent_a > $percent_b ? -1 : 1); 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); Api\Json\Request::isJSONRequest(false);
header('Content-Type: application/json; charset=utf-8'); 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 // close session now, to not block other user actions
$GLOBALS['egw']->session->commit_session(); $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') if($GLOBALS['egw_info']['user']['preferences']['common']['account_selection'] == 'primary_group')
{ {
$list['accounts']['filter']['group'] = $GLOBALS['egw_info']['user']['account_primary_group']; $list['accounts']['filter']['group'] = $GLOBALS['egw_info']['user']['account_primary_group'];