diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -37,9 +37,18 @@ ->setQuery($query) ->setRawQuery($raw_query); + $hard_limit = 1000; + if ($is_browse) { $limit = 10; $offset = $request->getInt('offset'); + + if (($offset + $limit) >= $hard_limit) { + // Offset-based paging is intrinsically slow; hard-cap how far we're + // willing to go with it. + return new Aphront404Response(); + } + $composite ->setLimit($limit + 1) ->setOffset($offset); @@ -49,15 +58,32 @@ if ($is_browse) { $next_link = null; + if (count($results) > $limit) { $results = array_slice($results, 0, $limit, $preserve_keys = true); - $next_link = phutil_tag( - 'a', - array( - 'href' => id(new PhutilURI($request->getRequestURI())) - ->setQueryParam('offset', $offset + $limit), - ), - pht('Next Page')); + if (($offset + (2 * $limit)) < $hard_limit) { + $next_uri = id(new PhutilURI($request->getRequestURI())) + ->setQueryParam('offset', $offset + $limit); + + $next_link = javelin_tag( + 'a', + array( + 'href' => $next_uri, + 'class' => 'typeahead-browse-more', + 'sigil' => 'typeahead-browse-more', + 'mustcapture' => true, + ), + pht('More Results')); + } else { + // If the user has paged through more than 1K results, don't + // offer to page any further. + $next_link = javelin_tag( + 'div', + array( + 'class' => 'typeahead-browse-hard-limit', + ), + pht('You reach the edge of the abyss.')); + } } $items = array(); @@ -72,11 +98,32 @@ $token); } + $markup = array( + $items, + $next_link, + ); + + if ($request->isAjax()) { + $content = array( + 'markup' => hsprintf('%s', $markup), + ); + return id(new AphrontAjaxResponse())->setContent($content); + } + + $this->requireResource('typeahead-browse-css'); + $this->initBehavior('typeahead-browse'); + + $markup = phutil_tag( + 'div', + array( + 'class' => 'typeahead-browse-frame', + ), + $markup); + return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(get_class($source)) // TODO: Provide nice names. - ->appendChild($items) - ->appendChild($next_link) + ->appendChild($markup) ->addCancelButton('/', pht('Close')); } diff --git a/webroot/rsrc/css/aphront/typeahead-browse.css b/webroot/rsrc/css/aphront/typeahead-browse.css new file mode 100644 --- /dev/null +++ b/webroot/rsrc/css/aphront/typeahead-browse.css @@ -0,0 +1,34 @@ +/** + * @provides typeahead-browse-css + */ + +.typeahead-browse-more, +.typeahead-browse-hard-limit { + display: block; + padding: 8px; + margin: 8px 0 0; + text-align: center; +} + +.typeahead-browse-more { + background: {$lightblue}; + border: 1px solid {$lightblueborder}; +} + +.typeahead-browse-more.loading { + opacity: 0.8; +} + +.typeahead-browse-hard-limit { + background: {$lightgreybackground}; + border: 1px solid {$lightgreyborder}; + color: {$lightgreytext}; +} + +.typeahead-browse-frame { + overflow-x: hidden; + overflow-y: auto; + padding: 4px; + height: 260px; + border: 1px solid {$lightgreyborder}; +} diff --git a/webroot/rsrc/js/application/typeahead/behavior-typeahead-browse.js b/webroot/rsrc/js/application/typeahead/behavior-typeahead-browse.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/application/typeahead/behavior-typeahead-browse.js @@ -0,0 +1,31 @@ +/** + * @provides javelin-behavior-typeahead-browse + * @requires javelin-behavior + * javelin-stratcom + * javelin-workflow + * javelin-dom + */ + +JX.behavior('typeahead-browse', function() { + var loading = false; + + JX.Stratcom.listen('click', 'typeahead-browse-more', function(e) { + e.kill(); + + if (loading) { + return; + } + var link = e.getTarget(); + + loading = true; + JX.DOM.alterClass(link, 'loading', true); + + JX.Workflow.newFromLink(link) + .setHandler(function(r) { + loading = false; + JX.DOM.replace(link, JX.$H(r.markup)); + }) + .start(); + }); + +});