diff --git a/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php b/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php index 3609cb06f6..73ae32938b 100644 --- a/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php +++ b/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php @@ -3,6 +3,12 @@ final class AlmanacInterfaceDatasource extends PhabricatorTypeaheadDatasource { + public function isBrowsable() { + // TODO: We should make this browsable, but need to make the result set + // orderable by device name. + return false; + } + public function getPlaceholderText() { return pht('Type an interface name...'); } diff --git a/src/applications/diffusion/typeahead/DiffusionArcanistProjectDatasource.php b/src/applications/diffusion/typeahead/DiffusionArcanistProjectDatasource.php index 963c0b2ca9..ce77b56f9b 100644 --- a/src/applications/diffusion/typeahead/DiffusionArcanistProjectDatasource.php +++ b/src/applications/diffusion/typeahead/DiffusionArcanistProjectDatasource.php @@ -3,6 +3,11 @@ final class DiffusionArcanistProjectDatasource extends PhabricatorTypeaheadDatasource { + public function isBrowsable() { + // TODO: We should probably make this browsable, or maybe remove it. + return false; + } + public function getPlaceholderText() { return pht('Type an arcanist project name...'); } diff --git a/src/applications/diffusion/typeahead/DiffusionSymbolDatasource.php b/src/applications/diffusion/typeahead/DiffusionSymbolDatasource.php index 97ab83f44d..0203ff2971 100644 --- a/src/applications/diffusion/typeahead/DiffusionSymbolDatasource.php +++ b/src/applications/diffusion/typeahead/DiffusionSymbolDatasource.php @@ -3,6 +3,12 @@ final class DiffusionSymbolDatasource extends PhabricatorTypeaheadDatasource { + public function isBrowsable() { + // This is slightly involved to make browsable, and browsing symbols + // does not seem likely to be very useful in any real software project. + return false; + } + public function getPlaceholderText() { return pht('Type a symbol name...'); } diff --git a/src/applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php b/src/applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php index c1228c9446..997e129bb3 100644 --- a/src/applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php +++ b/src/applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php @@ -3,6 +3,11 @@ final class HarbormasterBuildDependencyDatasource extends PhabricatorTypeaheadDatasource { + public function isBrowsable() { + // TODO: This should be browsable, but fixing it is involved. + return false; + } + public function getPlaceholderText() { return pht('Type another build step name...'); } diff --git a/src/applications/legalpad/typeahead/LegalpadDocumentDatasource.php b/src/applications/legalpad/typeahead/LegalpadDocumentDatasource.php index 853d775184..a536843d0b 100644 --- a/src/applications/legalpad/typeahead/LegalpadDocumentDatasource.php +++ b/src/applications/legalpad/typeahead/LegalpadDocumentDatasource.php @@ -2,6 +2,11 @@ final class LegalpadDocumentDatasource extends PhabricatorTypeaheadDatasource { + public function isBrowsable() { + // TODO: This should be made browsable. + return false; + } + public function getPlaceholderText() { return pht('Type a document name...'); } diff --git a/src/applications/macro/typeahead/PhabricatorMacroDatasource.php b/src/applications/macro/typeahead/PhabricatorMacroDatasource.php index 339be08fa7..4b8d52b4d4 100644 --- a/src/applications/macro/typeahead/PhabricatorMacroDatasource.php +++ b/src/applications/macro/typeahead/PhabricatorMacroDatasource.php @@ -2,6 +2,11 @@ final class PhabricatorMacroDatasource extends PhabricatorTypeaheadDatasource { + public function isBrowsable() { + // TODO: This should be made browsable. + return false; + } + public function getPlaceholderText() { return pht('Type a macro name...'); } diff --git a/src/applications/mailinglists/typeahead/PhabricatorMailingListDatasource.php b/src/applications/mailinglists/typeahead/PhabricatorMailingListDatasource.php index 53f7d82e0e..c08da2a30d 100644 --- a/src/applications/mailinglists/typeahead/PhabricatorMailingListDatasource.php +++ b/src/applications/mailinglists/typeahead/PhabricatorMailingListDatasource.php @@ -3,6 +3,11 @@ final class PhabricatorMailingListDatasource extends PhabricatorTypeaheadDatasource { + public function isBrowsable() { + // TODO: Make this browsable if we don't delete it before then. + return false; + } + public function getPlaceholderText() { return pht('Type a mailing list name...'); } diff --git a/src/applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php b/src/applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php index 172caea1a6..568aa0636d 100644 --- a/src/applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php +++ b/src/applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php @@ -25,7 +25,7 @@ final class ManiphestTaskPriorityDatasource ->setName($name); } - return $results; + return $this->filterResultsAgainstTokens($results); } } diff --git a/src/applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php b/src/applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php index e7eba2923c..27b3f90386 100644 --- a/src/applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php +++ b/src/applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php @@ -25,6 +25,6 @@ final class ManiphestTaskStatusDatasource ->setName($name); } - return $results; + return $this->filterResultsAgainstTokens($results); } } diff --git a/src/applications/meta/typeahead/PhabricatorApplicationDatasource.php b/src/applications/meta/typeahead/PhabricatorApplicationDatasource.php index 19b40f727a..18b6683dfc 100644 --- a/src/applications/meta/typeahead/PhabricatorApplicationDatasource.php +++ b/src/applications/meta/typeahead/PhabricatorApplicationDatasource.php @@ -37,7 +37,7 @@ final class PhabricatorApplicationDatasource ->setImageSprite('phabricator-search-icon '.$img); } - return $results; + return $this->filterResultsAgainstTokens($results); } } diff --git a/src/applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php b/src/applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php index bbe5e0d8f6..887aecfd80 100644 --- a/src/applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php +++ b/src/applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php @@ -3,6 +3,11 @@ final class PhabricatorMetaMTAApplicationEmailDatasource extends PhabricatorTypeaheadDatasource { + public function isBrowsable() { + // TODO: Make this browsable. + return false; + } + public function getPlaceholderText() { return pht('Type an application email address...'); } diff --git a/src/applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php b/src/applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php index c0a6c92589..a8658549ec 100644 --- a/src/applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php +++ b/src/applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php @@ -3,6 +3,11 @@ final class PhabricatorOwnersPackageDatasource extends PhabricatorTypeaheadDatasource { + public function isBrowsable() { + // TODO: Make this browsable. + return false; + } + public function getPlaceholderText() { return pht('Type a package name...'); } diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php index e2a6de0e43..2672e7d79a 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -25,6 +25,7 @@ final class PhabricatorTypeaheadModularDatasourceController if (isset($sources[$class])) { $source = $sources[$class]; $source->setParameters($request->getRequestData()); + $source->setViewer($viewer); // NOTE: Wrapping the source in a Composite datasource ensures we perform // application visibility checks for the viewer, so we do not need to do @@ -40,6 +41,10 @@ final class PhabricatorTypeaheadModularDatasourceController $hard_limit = 1000; if ($is_browse) { + if (!$composite->isBrowsable()) { + return new Aphront404Response(); + } + $limit = 10; $offset = $request->getInt('offset'); diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php index b1f55190c9..334d9354de 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php @@ -3,8 +3,20 @@ abstract class PhabricatorTypeaheadCompositeDatasource extends PhabricatorTypeaheadDatasource { + private $usable; + abstract public function getComponentDatasources(); + public function isBrowsable() { + foreach ($this->getUsableDatasources() as $datasource) { + if (!$datasource->isBrowsable()) { + return false; + } + } + + return parent::isBrowsable(); + } + public function getDatasourceApplicationClass() { return null; } @@ -43,26 +55,29 @@ abstract class PhabricatorTypeaheadCompositeDatasource } private function getUsableDatasources() { - $sources = $this->getComponentDatasources(); + if ($this->usable === null) { + $sources = $this->getComponentDatasources(); - $usable = array(); - foreach ($sources as $source) { - $application_class = $source->getDatasourceApplicationClass(); + $usable = array(); + foreach ($sources as $source) { + $application_class = $source->getDatasourceApplicationClass(); - if ($application_class) { - $result = id(new PhabricatorApplicationQuery()) - ->setViewer($this->getViewer()) - ->withClasses(array($application_class)) - ->execute(); - if (!$result) { - continue; + if ($application_class) { + $result = id(new PhabricatorApplicationQuery()) + ->setViewer($this->getViewer()) + ->withClasses(array($application_class)) + ->execute(); + if (!$result) { + continue; + } } - } - $usable[] = $source; + $usable[] = $source; + } + $this->usable = $usable; } - return $usable; + return $this->usable; } } diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php index 3d6d2b75f2..a15a248e23 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php @@ -102,4 +102,75 @@ abstract class PhabricatorTypeaheadDatasource extends Phobject { ->execute(); } + + /** + * Can the user browse through results from this datasource? + * + * Browsable datasources allow the user to switch from typeahead mode to + * a browse mode where they can scroll through all results. + * + * By default, datasources are browsable, but some datasources can not + * generate a meaningful result set or can't filter results on the server. + * + * @return bool + */ + public function isBrowsable() { + return true; + } + + + /** + * Filter a list of results, removing items which don't match the query + * tokens. + * + * This is useful for datasources which return a static list of hard-coded + * or configured results and can't easily do query filtering in a real + * query class. Instead, they can just build the entire result set and use + * this method to filter it. + * + * For datasources backed by database objects, this is often much less + * efficient than filtering at the query level. + * + * @param list List of typeahead results. + * @return list Filtered results. + */ + protected function filterResultsAgainstTokens(array $results) { + $tokens = $this->getTokens(); + if (!$tokens) { + return $results; + } + + $map = array(); + foreach ($tokens as $token) { + $map[$token] = strlen($token); + } + + foreach ($results as $key => $result) { + $rtokens = self::tokenizeString($result->getName()); + + // For each token in the query, we need to find a match somewhere + // in the result name. + foreach ($map as $token => $length) { + // Look for a match. + $match = false; + foreach ($rtokens as $rtoken) { + if (!strncmp($rtoken, $token, $length)) { + // This part of the result name has the query token as a prefix. + $match = true; + break; + } + } + + if (!$match) { + // We didn't find a match for this query token, so throw the result + // away. Try with the next result. + unset($results[$key]); + break; + } + } + } + + return $results; + } + } diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php index 267976b773..ebaf3d39d8 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php @@ -3,6 +3,13 @@ final class PhabricatorTypeaheadMonogramDatasource extends PhabricatorTypeaheadDatasource { + public function isBrowsable() { + // This source isn't meaningfully browsable. Although it's technically + // possible to let users browse through every object on an install, there + // is no use case for it and it doesn't seem worth building. + return false; + } + public function getPlaceholderText() { return pht('Type an object name...'); }