diff --git a/api/js/etemplate/Et2Select/Et2SelectAccount.ts b/api/js/etemplate/Et2Select/Et2SelectAccount.ts index 285208d4f7..b6180f450b 100644 --- a/api/js/etemplate/Et2Select/Et2SelectAccount.ts +++ b/api/js/etemplate/Et2Select/Et2SelectAccount.ts @@ -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'; } diff --git a/api/js/etemplate/Et2Select/SearchMixin.ts b/api/js/etemplate/Et2Select/SearchMixin.ts index afbb289b0b..0156641674 100644 --- a/api/js/etemplate/Et2Select/SearchMixin.ts +++ b/api/js/etemplate/Et2Select/SearchMixin.ts @@ -230,8 +230,29 @@ export const Et2WithSearchMixin = >(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 = []; @@ -1019,8 +1040,13 @@ export const Et2WithSearchMixin = >(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 = >(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 = >(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); + } }); } } diff --git a/api/src/Accounts.php b/api/src/Accounts.php index ae18fdf508..b6a505d7bb 100644 --- a/api/src/Accounts.php +++ b/api/src/Accounts.php @@ -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; } diff --git a/api/src/Etemplate/Widget/Taglist.php b/api/src/Etemplate/Widget/Taglist.php index 8ac0bcaade..4fcd2d0011 100644 --- a/api/src/Etemplate/Widget/Taglist.php +++ b/api/src/Etemplate/Widget/Taglist.php @@ -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'); diff --git a/api/src/Framework.php b/api/src/Framework.php index 69e4bc5739..c2bfca1f2b 100644 --- a/api/src/Framework.php +++ b/api/src/Framework.php @@ -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'];