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 @@ -1,51 +1,57 @@ getViewer(); $raw_query = $this->getRawQuery(); $devices = id(new AlmanacDeviceQuery()) ->setViewer($viewer) ->withNamePrefix($raw_query) ->execute(); if ($devices) { $interfaces = id(new AlmanacInterfaceQuery()) ->setViewer($viewer) ->withDevicePHIDs(mpull($devices, 'getPHID')) ->execute(); } else { $interfaces = array(); } if ($interfaces) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(mpull($interfaces, 'getPHID')) ->execute(); } else { $handles = array(); } $results = array(); foreach ($handles as $handle) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($handle->getName()) ->setPHID($handle->getPHID()); } return $results; } } 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 @@ -1,30 +1,35 @@ getViewer(); $raw_query = $this->getRawQuery(); $results = array(); $arcprojs = id(new PhabricatorRepositoryArcanistProject())->loadAll(); foreach ($arcprojs as $proj) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($proj->getName()) ->setPHID($proj->getPHID()); } return $results; } } 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 @@ -1,48 +1,54 @@ getViewer(); $raw_query = $this->getRawQuery(); $results = array(); if (strlen($raw_query)) { $symbols = id(new DiffusionSymbolQuery()) ->setViewer($viewer) ->setNamePrefix($raw_query) ->setLimit(15) ->needArcanistProjects(true) ->needRepositories(true) ->needPaths(true) ->execute(); foreach ($symbols as $symbol) { $lang = $symbol->getSymbolLanguage(); $name = $symbol->getSymbolName(); $type = $symbol->getSymbolType(); $proj = $symbol->getArcanistProject()->getName(); $results[] = id(new PhabricatorTypeaheadResult()) ->setName($name) ->setURI($symbol->getURI()) ->setPHID(md5($symbol->getURI())) // Just needs to be unique. ->setDisplayName($name) ->setDisplayType(strtoupper($lang).' '.ucwords($type).' ('.$proj.')') ->setPriorityType('symb'); } } return $results; } } 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 @@ -1,45 +1,50 @@ getViewer(); $plan_phid = $this->getParameter('planPHID'); $step_phid = $this->getParameter('stepPHID'); $steps = id(new HarbormasterBuildStepQuery()) ->setViewer($viewer) ->withBuildPlanPHIDs(array($plan_phid)) ->execute(); $steps = mpull($steps, null, 'getPHID'); if (count($steps) === 0) { return array(); } $results = array(); foreach ($steps as $phid => $step) { if ($step->getPHID() === $step_phid) { continue; } $results[] = id(new PhabricatorTypeaheadResult()) ->setName($step->getName()) ->setURI('/') ->setPHID($phid); } return $results; } } 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 @@ -1,31 +1,36 @@ getViewer(); $raw_query = $this->getRawQuery(); $results = array(); $documents = id(new LegalpadDocumentQuery()) ->setViewer($viewer) ->execute(); foreach ($documents as $document) { $results[] = id(new PhabricatorTypeaheadResult()) ->setPHID($document->getPHID()) ->setName($document->getMonogram().' '.$document->getTitle()); } return $results; } } 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 @@ -1,33 +1,38 @@ getViewer(); $raw_query = $this->getRawQuery(); $results = array(); $macros = id(new PhabricatorMacroQuery()) ->setViewer($viewer) ->withStatus(PhabricatorMacroQuery::STATUS_ACTIVE) ->execute(); foreach ($macros as $macro) { $results[] = id(new PhabricatorTypeaheadResult()) ->setPHID($macro->getPHID()) ->setName($macro->getName()); } return $results; } } 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 @@ -1,33 +1,38 @@ getViewer(); $raw_query = $this->getRawQuery(); $results = array(); $lists = id(new PhabricatorMailingListQuery()) ->setViewer($viewer) ->execute(); foreach ($lists as $list) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($list->getName()) ->setURI($list->getURI()) ->setPHID($list->getPHID()); } return $results; } } 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 @@ -1,31 +1,31 @@ getViewer(); $raw_query = $this->getRawQuery(); $results = array(); $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); foreach ($priority_map as $value => $name) { // NOTE: $value is not a PHID but is unique. This'll work. $results[] = id(new PhabricatorTypeaheadResult()) ->setPHID($value) ->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 @@ -1,30 +1,30 @@ getViewer(); $raw_query = $this->getRawQuery(); $results = array(); $status_map = ManiphestTaskStatus::getTaskStatusMap(); foreach ($status_map as $value => $name) { // NOTE: $value is not a PHID but is unique. This'll work. $results[] = id(new PhabricatorTypeaheadResult()) ->setPHID($value) ->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 @@ -1,43 +1,43 @@ getViewer(); $raw_query = $this->getRawQuery(); $results = array(); $applications = PhabricatorApplication::getAllInstalledApplications(); foreach ($applications as $application) { $uri = $application->getTypeaheadURI(); if (!$uri) { continue; } $name = $application->getName().' '.$application->getShortDescription(); $img = 'phui-font-fa phui-icon-view '.$application->getFontIcon(); $results[] = id(new PhabricatorTypeaheadResult()) ->setName($name) ->setURI($uri) ->setPHID($application->getPHID()) ->setPriorityString($application->getName()) ->setDisplayName($application->getName()) ->setDisplayType($application->getShortDescription()) ->setImageuRI($application->getIconURI()) ->setPriorityType('apps') ->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 @@ -1,43 +1,48 @@ getViewer(); $raw_query = $this->getRawQuery(); $emails = id(new PhabricatorMetaMTAApplicationEmailQuery()) ->setViewer($viewer) ->withAddressPrefix($raw_query) ->setLimit($this->getLimit()) ->execute(); if ($emails) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(mpull($emails, 'getPHID')) ->execute(); } else { $handles = array(); } $results = array(); foreach ($handles as $handle) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($handle->getName()) ->setPHID($handle->getPHID()); } return $results; } } 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 @@ -1,34 +1,39 @@ getViewer(); $raw_query = $this->getRawQuery(); $results = array(); $packages = id(new PhabricatorOwnersPackageQuery()) ->setViewer($viewer) ->execute(); foreach ($packages as $package) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($package->getName()) ->setURI('/owners/package/'.$package->getID().'/') ->setPHID($package->getPHID()); } return $results; } } 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 @@ -1,250 +1,255 @@ getRequest(); $viewer = $request->getUser(); $query = $request->getStr('q'); $is_browse = ($request->getURIData('action') == 'browse'); // Default this to the query string to make debugging a little bit easier. $raw_query = nonempty($request->getStr('raw'), $query); // This makes form submission easier in the debug view. $class = nonempty($request->getURIData('class'), $request->getStr('class')); $sources = id(new PhutilSymbolLoader()) ->setAncestorClass('PhabricatorTypeaheadDatasource') ->loadObjects(); 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 // those separately. $composite = new PhabricatorTypeaheadRuntimeCompositeDatasource(); $composite->addDatasource($source); $composite ->setViewer($viewer) ->setQuery($query) ->setRawQuery($raw_query); $hard_limit = 1000; if ($is_browse) { + if (!$composite->isBrowsable()) { + return new Aphront404Response(); + } + $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); } $results = $composite->loadResults(); if ($is_browse) { $next_link = null; if (count($results) > $limit) { $results = array_slice($results, 0, $limit, $preserve_keys = true); 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(); foreach ($results as $result) { $token = PhabricatorTypeaheadTokenView::newForTypeaheadResult( $result); $items[] = phutil_tag( 'div', array( 'class' => 'grouped', ), $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'); $input_id = celerity_generate_unique_node_id(); $frame_id = celerity_generate_unique_node_id(); $config = array( 'inputID' => $input_id, 'frameID' => $frame_id, 'uri' => (string)$request->getRequestURI(), ); $this->initBehavior('typeahead-search', $config); $search = javelin_tag( 'input', array( 'type' => 'text', 'id' => $input_id, 'class' => 'typeahead-browse-input', 'autocomplete' => 'off', 'placeholder' => $source->getPlaceholderText(), )); $frame = phutil_tag( 'div', array( 'class' => 'typeahead-browse-frame', 'id' => $frame_id, ), $markup); $browser = array( phutil_tag( 'div', array( 'class' => 'typeahead-browse-header', ), $search), $frame, ); return $this->newDialog() ->setWidth(AphrontDialogView::WIDTH_FORM) ->setRenderDialogAsDiv(true) ->setTitle(get_class($source)) // TODO: Provide nice names. ->appendChild($browser) ->addCancelButton('/', pht('Close')); } } else if ($is_browse) { return new Aphront404Response(); } else { $results = array(); } $content = mpull($results, 'getWireFormat'); if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent($content); } // If there's a non-Ajax request to this endpoint, show results in a tabular // format to make it easier to debug typeahead output. foreach ($sources as $key => $source) { // This can happen with composite sources like user or project, as well // generic ones like NoOwner if (!$source->getDatasourceApplicationClass()) { continue; } if (!PhabricatorApplication::isClassInstalledForViewer( $source->getDatasourceApplicationClass(), $viewer)) { unset($sources[$key]); } } $options = array_fuse(array_keys($sources)); asort($options); $form = id(new AphrontFormView()) ->setUser($viewer) ->setAction('/typeahead/class/') ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Source Class')) ->setName('class') ->setValue($class) ->setOptions($options)) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Query')) ->setName('q') ->setValue($request->getStr('q'))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Raw Query')) ->setName('raw') ->setValue($request->getStr('raw'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Query'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Token Query')) ->setForm($form); $table = new AphrontTableView($content); $table->setHeaders( array( pht('Name'), pht('URI'), pht('PHID'), pht('Priority'), pht('Display Name'), pht('Display Type'), pht('Image URI'), pht('Priority Type'), pht('Icon'), pht('Closed'), pht('Sprite'), )); $result_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Token Results (%s)', $class)) ->appendChild($table); return $this->buildApplicationPage( array( $form_box, $result_box, ), array( 'title' => pht('Typeahead Results'), 'device' => false, )); } } 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 @@ -1,68 +1,83 @@ getUsableDatasources() as $datasource) { + if (!$datasource->isBrowsable()) { + return false; + } + } + + return parent::isBrowsable(); + } + public function getDatasourceApplicationClass() { return null; } public function loadResults() { $offset = $this->getOffset(); $limit = $this->getLimit(); $results = array(); foreach ($this->getUsableDatasources() as $source) { $source ->setRawQuery($this->getRawQuery()) ->setQuery($this->getQuery()) ->setViewer($this->getViewer()); if ($limit) { $source->setLimit($offset + $limit); } $results[] = $source->loadResults(); } $results = array_mergev($results); $results = msort($results, 'getSortKey'); $count = count($results); if ($offset || $limit) { if (!$limit) { $limit = count($results); } $results = array_slice($results, $offset, $limit, $preserve_keys = true); } return $results; } private function getUsableDatasources() { - $sources = $this->getComponentDatasources(); - - $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 ($this->usable === null) { + $sources = $this->getComponentDatasources(); + + $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; + } } - } - $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 @@ -1,105 +1,176 @@ limit = $limit; return $this; } public function getLimit() { return $this->limit; } public function setOffset($offset) { $this->offset = $offset; return $this; } public function getOffset() { return $this->offset; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setRawQuery($raw_query) { $this->rawQuery = $raw_query; return $this; } public function getRawQuery() { return $this->rawQuery; } public function setQuery($query) { $this->query = $query; return $this; } public function getQuery() { return $this->query; } public function setParameters(array $params) { $this->parameters = $params; return $this; } public function getParameters() { return $this->parameters; } public function getParameter($name, $default = null) { return idx($this->parameters, $name, $default); } public function getDatasourceURI() { $uri = new PhutilURI('/typeahead/class/'.get_class($this).'/'); $uri->setQueryParams($this->parameters); return (string)$uri; } abstract public function getPlaceholderText(); abstract public function getDatasourceApplicationClass(); abstract public function loadResults(); public static function tokenizeString($string) { $string = phutil_utf8_strtolower($string); $string = trim($string); if (!strlen($string)) { return array(); } $tokens = preg_split('/\s+|[-\[\]]/', $string); return array_unique($tokens); } public function getTokens() { return self::tokenizeString($this->getRawQuery()); } protected function executeQuery( PhabricatorCursorPagedPolicyAwareQuery $query) { return $query ->setViewer($this->getViewer()) ->setOffset($this->getOffset()) ->setLimit($this->getLimit()) ->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 @@ -1,43 +1,50 @@ getViewer(); $raw_query = $this->getRawQuery(); $results = array(); $objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withNames(array($raw_query)) ->execute(); if ($objects) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(mpull($objects, 'getPHID')) ->execute(); $handle = head($handles); if ($handle) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($handle->getFullName()) ->setDisplayType($handle->getTypeName()) ->setURI($handle->getURI()) ->setPHID($handle->getPHID()) ->setPriorityType('jump'); } } return $results; } }