diff --git a/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php b/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php index 73ae32938b..a5f46ddfb0 100644 --- a/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php +++ b/src/applications/almanac/typeahead/AlmanacInterfaceDatasource.php @@ -1,57 +1,61 @@ 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/almanac/typeahead/AlmanacServiceDatasource.php b/src/applications/almanac/typeahead/AlmanacServiceDatasource.php index 4cc0d1376a..e5728e1c26 100644 --- a/src/applications/almanac/typeahead/AlmanacServiceDatasource.php +++ b/src/applications/almanac/typeahead/AlmanacServiceDatasource.php @@ -1,43 +1,47 @@ getViewer(); $raw_query = $this->getRawQuery(); $services = id(new AlmanacServiceQuery()) ->withNamePrefix($raw_query) ->setOrder('name'); $services = $this->executeQuery($services); if ($services) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(mpull($services, '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 ce77b56f9b..4f58acfa59 100644 --- a/src/applications/diffusion/typeahead/DiffusionArcanistProjectDatasource.php +++ b/src/applications/diffusion/typeahead/DiffusionArcanistProjectDatasource.php @@ -1,35 +1,39 @@ 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/DiffusionAuditorDatasource.php b/src/applications/diffusion/typeahead/DiffusionAuditorDatasource.php index 471c8a15b7..54c3aa72d9 100644 --- a/src/applications/diffusion/typeahead/DiffusionAuditorDatasource.php +++ b/src/applications/diffusion/typeahead/DiffusionAuditorDatasource.php @@ -1,22 +1,26 @@ getViewer(); $raw_query = $this->getRawQuery(); $query = id(new PhabricatorRepositoryQuery()) ->setOrder('name') ->withDatasourceQuery($raw_query); $repos = $this->executeQuery($query); $results = array(); foreach ($repos as $repo) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($repo->getMonogram().' '.$repo->getName()) ->setURI('/diffusion/'.$repo->getCallsign().'/') ->setPHID($repo->getPHID()) ->setPriorityString($repo->getMonogram()); } return $results; } } diff --git a/src/applications/diffusion/typeahead/DiffusionSymbolDatasource.php b/src/applications/diffusion/typeahead/DiffusionSymbolDatasource.php index 0203ff2971..be7a084457 100644 --- a/src/applications/diffusion/typeahead/DiffusionSymbolDatasource.php +++ b/src/applications/diffusion/typeahead/DiffusionSymbolDatasource.php @@ -1,54 +1,58 @@ 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 997e129bb3..08b0be774e 100644 --- a/src/applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php +++ b/src/applications/harbormaster/typeahead/HarbormasterBuildDependencyDatasource.php @@ -1,50 +1,54 @@ 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/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php b/src/applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php index ceb4a1ff10..cf5b815c57 100644 --- a/src/applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php +++ b/src/applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php @@ -1,40 +1,44 @@ getViewer(); $raw_query = $this->getRawQuery(); $results = array(); $query = id(new HarbormasterBuildPlanQuery()) ->setOrder('name') ->withDatasourceQuery($raw_query); $plans = $this->executeQuery($query); foreach ($plans as $plan) { $closed = null; if ($plan->isDisabled()) { $closed = pht('Disabled'); } $results[] = id(new PhabricatorTypeaheadResult()) ->setName($plan->getName()) ->setClosed($closed) ->setPHID($plan->getPHID()); } return $results; } } diff --git a/src/applications/legalpad/typeahead/LegalpadDocumentDatasource.php b/src/applications/legalpad/typeahead/LegalpadDocumentDatasource.php index a536843d0b..a0117ee7b0 100644 --- a/src/applications/legalpad/typeahead/LegalpadDocumentDatasource.php +++ b/src/applications/legalpad/typeahead/LegalpadDocumentDatasource.php @@ -1,36 +1,40 @@ 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 1d978683cc..b7b7efd630 100644 --- a/src/applications/macro/typeahead/PhabricatorMacroDatasource.php +++ b/src/applications/macro/typeahead/PhabricatorMacroDatasource.php @@ -1,37 +1,41 @@ getRawQuery(); $query = id(new PhabricatorMacroQuery()) ->setOrder('name') ->withNamePrefix($raw_query); $macros = $this->executeQuery($query); $results = array(); foreach ($macros as $macro) { $closed = null; if ($macro->getIsDisabled()) { $closed = pht('Disabled'); } $results[] = id(new PhabricatorTypeaheadResult()) ->setPHID($macro->getPHID()) ->setClosed($closed) ->setName($macro->getName()); } return $results; } } diff --git a/src/applications/mailinglists/typeahead/PhabricatorMailingListDatasource.php b/src/applications/mailinglists/typeahead/PhabricatorMailingListDatasource.php index b9435e0b4a..cac292e1e5 100644 --- a/src/applications/mailinglists/typeahead/PhabricatorMailingListDatasource.php +++ b/src/applications/mailinglists/typeahead/PhabricatorMailingListDatasource.php @@ -1,35 +1,39 @@ getViewer(); $raw_query = $this->getRawQuery(); $query = id(new PhabricatorMailingListQuery()); $lists = $this->executeQuery($query); $results = array(); foreach ($lists as $list) { $results[] = id(new PhabricatorTypeaheadResult()) ->setName($list->getName()) ->setURI($list->getURI()) ->setPHID($list->getPHID()); } // TODO: It would be slightly preferable to do this as part of the query, // this is just simpler for the moment. return $this->filterResultsAgainstTokens($results); } } diff --git a/src/applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php b/src/applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php index 568aa0636d..59c9a08042 100644 --- a/src/applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php +++ b/src/applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php @@ -1,31 +1,35 @@ 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 $this->filterResultsAgainstTokens($results); } } diff --git a/src/applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php b/src/applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php index 27b3f90386..7ef047e72e 100644 --- a/src/applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php +++ b/src/applications/maniphest/typeahead/ManiphestTaskStatusDatasource.php @@ -1,30 +1,34 @@ 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 $this->filterResultsAgainstTokens($results); } } diff --git a/src/applications/meta/typeahead/PhabricatorApplicationDatasource.php b/src/applications/meta/typeahead/PhabricatorApplicationDatasource.php index 18b6683dfc..b6d4aa3f18 100644 --- a/src/applications/meta/typeahead/PhabricatorApplicationDatasource.php +++ b/src/applications/meta/typeahead/PhabricatorApplicationDatasource.php @@ -1,43 +1,47 @@ 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 $this->filterResultsAgainstTokens($results); } } diff --git a/src/applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php b/src/applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php index 887aecfd80..62c1275c39 100644 --- a/src/applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php +++ b/src/applications/metamta/typeahead/PhabricatorMetaMTAApplicationEmailDatasource.php @@ -1,48 +1,52 @@ 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/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php b/src/applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php index 9d7e2cda7d..804e4498c0 100644 --- a/src/applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php +++ b/src/applications/metamta/typeahead/PhabricatorMetaMTAMailableDatasource.php @@ -1,22 +1,26 @@ 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/people/typeahead/PhabricatorPeopleDatasource.php b/src/applications/people/typeahead/PhabricatorPeopleDatasource.php index a2df2750ad..ab1f11cede 100644 --- a/src/applications/people/typeahead/PhabricatorPeopleDatasource.php +++ b/src/applications/people/typeahead/PhabricatorPeopleDatasource.php @@ -1,79 +1,83 @@ enrichResults = $enrich; return $this; } + public function getBrowseTitle() { + return pht('Browse Users'); + } + public function getPlaceholderText() { return pht('Type a username...'); } public function getDatasourceApplicationClass() { return 'PhabricatorPeopleApplication'; } public function loadResults() { $viewer = $this->getViewer(); $tokens = $this->getTokens(); $query = id(new PhabricatorPeopleQuery()) ->setOrderVector(array('username')); if ($tokens) { $query->withNameTokens($tokens); } $users = $this->executeQuery($query); if ($this->enrichResults && $users) { $phids = mpull($users, 'getPHID'); $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs($phids) ->execute(); } $results = array(); foreach ($users as $user) { $closed = null; if ($user->getIsDisabled()) { $closed = pht('Disabled'); } else if ($user->getIsSystemAgent()) { $closed = pht('Bot/Script'); } $result = id(new PhabricatorTypeaheadResult()) ->setName($user->getFullName()) ->setURI('/p/'.$user->getUsername()) ->setPHID($user->getPHID()) ->setPriorityString($user->getUsername()) ->setPriorityType('user') ->setClosed($closed); if ($this->enrichResults) { $display_type = 'User'; if ($user->getIsAdmin()) { $display_type = 'Administrator'; } $result->setDisplayType($display_type); $result->setImageURI($handles[$user->getPHID()]->getImageURI()); } $results[] = $result; } return $results; } } diff --git a/src/applications/people/typeahead/PhabricatorViewerDatasource.php b/src/applications/people/typeahead/PhabricatorViewerDatasource.php index 92bd48bab0..ef4cbc3a1b 100644 --- a/src/applications/people/typeahead/PhabricatorViewerDatasource.php +++ b/src/applications/people/typeahead/PhabricatorViewerDatasource.php @@ -1,57 +1,61 @@ getViewer()->getPHID()) { $results = array($this->renderViewerFunctionToken()); } else { $results = array(); } return $this->filterResultsAgainstTokens($results); } protected function canEvaluateFunction($function) { if (!$this->getViewer()->getPHID()) { return false; } return ($function == 'viewer'); } protected function evaluateFunction($function, array $argv_list) { $results = array(); foreach ($argv_list as $argv) { $results[] = $this->getViewer()->getPHID(); } return $results; } public function renderFunctionTokens($function, array $argv_list) { $tokens = array(); foreach ($argv_list as $argv) { $tokens[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( $this->renderViewerFunctionToken()); } return $tokens; } private function renderViewerFunctionToken() { return $this->newFunctionResult() ->setName(pht('Current Viewer')) ->setPHID('viewer()') ->setIcon('fa-user') ->setUnique(true); } } diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php index 0466ad9950..d38f6fc5b3 100644 --- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php @@ -1,79 +1,83 @@ getViewer(); $raw_query = $this->getRawQuery(); // Allow users to type "#qa" or "qa" to find "Quality Assurance". $raw_query = ltrim($raw_query, '#'); $tokens = self::tokenizeString($raw_query); $query = id(new PhabricatorProjectQuery()) ->needImages(true) ->needSlugs(true); if ($tokens) { $query->withNameTokens($tokens); } $projs = $this->executeQuery($query); $projs = mpull($projs, null, 'getPHID'); $must_have_cols = $this->getParameter('mustHaveColumns', false); if ($must_have_cols) { $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array_keys($projs)) ->execute(); $has_cols = mgroup($columns, 'getProjectPHID'); } else { $has_cols = array_fill_keys(array_keys($projs), true); } $results = array(); foreach ($projs as $proj) { if (!isset($has_cols[$proj->getPHID()])) { continue; } $closed = null; if ($proj->isArchived()) { $closed = pht('Archived'); } $all_strings = mpull($proj->getSlugs(), 'getSlug'); $all_strings[] = $proj->getName(); $all_strings = implode(' ', $all_strings); $proj_result = id(new PhabricatorTypeaheadResult()) ->setName($all_strings) ->setDisplayName($proj->getName()) ->setDisplayType('Project') ->setURI('/tag/'.$proj->getPrimarySlug().'/') ->setPHID($proj->getPHID()) ->setIcon($proj->getIcon()) ->setColor($proj->getColor()) ->setPriorityType('proj') ->setClosed($closed); $proj_result->setImageURI($proj->getProfileImageURI()); $results[] = $proj_result; } return $results; } } diff --git a/src/applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php b/src/applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php index 18748ce8a7..a3016820a0 100644 --- a/src/applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php @@ -1,32 +1,36 @@ $result) { if (is_string($result)) { $results[$key] = new PhabricatorQueryConstraint( PhabricatorQueryConstraint::OPERATOR_AND, $result); } } return $results; } } diff --git a/src/applications/project/typeahead/PhabricatorProjectLogicalDatasource.php b/src/applications/project/typeahead/PhabricatorProjectLogicalDatasource.php index 17afb30c99..1bce988339 100644 --- a/src/applications/project/typeahead/PhabricatorProjectLogicalDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectLogicalDatasource.php @@ -1,24 +1,28 @@ ) or not()...'); } public function getDatasourceApplicationClass() { return 'PhabricatorProjectApplication'; } public function getComponentDatasources() { return array( new PhabricatorProjectDatasource(), ); } public function getDatasourceFunctions() { return array( 'any' => array( 'name' => pht('Find results in any of several projects.'), ), 'not' => array( 'name' => pht('Find results not in specific projects.'), ), ); } protected function didLoadResults(array $results) { $function = $this->getCurrentFunction(); $return_any = ($function !== 'not'); $return_not = ($function !== 'any'); $return = array(); foreach ($results as $result) { $result ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) ->setIcon('fa-asterisk'); if ($return_any) { $return[] = id(clone $result) ->setPHID('any('.$result->getPHID().')') ->setDisplayName(pht('In Any: %s', $result->getDisplayName())) ->setName($result->getName().' any'); } if ($return_not) { $return[] = id(clone $result) ->setPHID('not('.$result->getPHID().')') ->setDisplayName(pht('Not In: %s', $result->getDisplayName())) ->setName($result->getName().' not'); } } return $return; } protected function evaluateFunction($function, array $argv_list) { $phids = array(); foreach ($argv_list as $argv) { $phids[] = head($argv); } $operator = array( 'any' => PhabricatorQueryConstraint::OPERATOR_OR, 'not' => PhabricatorQueryConstraint::OPERATOR_NOT, ); $results = array(); foreach ($phids as $phid) { $results[] = new PhabricatorQueryConstraint( $operator[$function], $phid); } return $results; } public function renderFunctionTokens($function, array $argv_list) { $phids = array(); foreach ($argv_list as $argv) { $phids[] = head($argv); } $tokens = $this->renderTokens($phids); foreach ($tokens as $token) { if ($token->isInvalid()) { if ($function == 'any') { $token->setValue(pht('In Any: Invalid Project')); } else { $token->setValue(pht('Not In: Invalid Project')); } } else { $token ->setIcon('fa-asterisk') ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION); if ($function == 'any') { $token ->setKey('any('.$token->getKey().')') ->setValue(pht('In Any: %s', $token->getValue())); } else { $token ->setKey('not('.$token->getKey().')') ->setValue(pht('Not In: %s', $token->getValue())); } } } return $tokens; } } diff --git a/src/applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php b/src/applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php index ed1ebdc782..06a638075d 100644 --- a/src/applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectLogicalUserDatasource.php @@ -1,123 +1,127 @@ )...'); } public function getDatasourceApplicationClass() { return 'PhabricatorProjectApplication'; } public function getComponentDatasources() { return array( new PhabricatorPeopleDatasource(), ); } public function getDatasourceFunctions() { return array( 'projects' => array( 'name' => pht("Find results in any of a user's projects."), ), ); } protected function didLoadResults(array $results) { foreach ($results as $result) { $result ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) ->setIcon('fa-asterisk') ->setPHID('projects('.$result->getPHID().')') ->setDisplayName(pht("User's Projects: %s", $result->getDisplayName())) ->setName($result->getName().' projects'); } return $results; } protected function evaluateFunction($function, array $argv_list) { $phids = array(); foreach ($argv_list as $argv) { $phids[] = head($argv); } $phids = $this->resolvePHIDs($phids); $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->withMemberPHIDs($phids) ->execute(); $results = array(); foreach ($projects as $project) { $results[] = new PhabricatorQueryConstraint( PhabricatorQueryConstraint::OPERATOR_OR, $project->getPHID()); } return $results; } public function renderFunctionTokens($function, array $argv_list) { $phids = array(); foreach ($argv_list as $argv) { $phids[] = head($argv); } $phids = $this->resolvePHIDs($phids); $tokens = $this->renderTokens($phids); foreach ($tokens as $token) { if ($token->isInvalid()) { $token ->setValue(pht("User's Projects: Invalid User")); } else { $token ->setIcon('fa-asterisk') ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) ->setKey('projects('.$token->getKey().')') ->setValue(pht("User's Projects: %s", $token->getValue())); } } return $tokens; } private function resolvePHIDs(array $phids) { // If we have a function like `projects(alincoln)`, try to resolve the // username first. This won't happen normally, but can be passed in from // the query string. // The user might also give us an invalid username. In this case, we // preserve it and return it in-place so we get an "invalid" token rendered // in the UI. This shows the user where the issue is and best represents // the user's input. $usernames = array(); foreach ($phids as $key => $phid) { if (phid_get_type($phid) != PhabricatorPeopleUserPHIDType::TYPECONST) { $usernames[$key] = $phid; } } if ($usernames) { $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->getViewer()) ->withUsernames($usernames) ->execute(); $users = mpull($users, null, 'getUsername'); foreach ($usernames as $key => $username) { $user = idx($users, $username); if ($user) { $phids[$key] = $user->getPHID(); } } } return $phids; } } diff --git a/src/applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php b/src/applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php index 7e76cd5762..4285462d54 100644 --- a/src/applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectLogicalViewerDatasource.php @@ -1,80 +1,84 @@ array( 'name' => pht("Find results in any of the current viewer's projects."), ), ); } public function loadResults() { if ($this->getViewer()->getPHID()) { $results = array($this->renderViewerProjectsFunctionToken()); } else { $results = array(); } return $this->filterResultsAgainstTokens($results); } protected function canEvaluateFunction($function) { if (!$this->getViewer()->getPHID()) { return false; } return parent::canEvaluateFunction($function); } protected function evaluateFunction($function, array $argv_list) { $viewer = $this->getViewer(); $projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withMemberPHIDs(array($viewer->getPHID())) ->execute(); $phids = mpull($projects, 'getPHID'); $results = array(); foreach ($phids as $phid) { $results[] = new PhabricatorQueryConstraint( PhabricatorQueryConstraint::OPERATOR_OR, $phid); } return $results; } public function renderFunctionTokens( $function, array $argv_list) { $tokens = array(); foreach ($argv_list as $argv) { $tokens[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( $this->renderViewerProjectsFunctionToken()); } return $tokens; } private function renderViewerProjectsFunctionToken() { return $this->newFunctionResult() ->setName(pht('Current Viewer\'s Projects')) ->setPHID('viewerprojects()') ->setIcon('fa-asterisk') ->setUnique(true); } } diff --git a/src/applications/project/typeahead/PhabricatorProjectMembersDatasource.php b/src/applications/project/typeahead/PhabricatorProjectMembersDatasource.php index 4f8ee3e987..c96d006e56 100644 --- a/src/applications/project/typeahead/PhabricatorProjectMembersDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectMembersDatasource.php @@ -1,86 +1,90 @@ )...'); } public function getDatasourceApplicationClass() { return 'PhabricatorProjectApplication'; } public function getComponentDatasources() { return array( new PhabricatorProjectDatasource(), ); } public function getDatasourceFunctions() { return array( 'members' => array( 'name' => pht('Find results for members of a project.'), ), ); } protected function didLoadResults(array $results) { foreach ($results as $result) { $result ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) ->setIcon('fa-users') ->setPHID('members('.$result->getPHID().')') ->setDisplayName(pht('Members: %s', $result->getDisplayName())) ->setName($result->getName().' members'); } return $results; } protected function evaluateFunction($function, array $argv_list) { $phids = array(); foreach ($argv_list as $argv) { $phids[] = head($argv); } $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->getViewer()) ->needMembers(true) ->withPHIDs($phids) ->execute(); $results = array(); foreach ($projects as $project) { foreach ($project->getMemberPHIDs() as $phid) { $results[$phid] = $phid; } } return array_values($results); } public function renderFunctionTokens($function, array $argv_list) { $phids = array(); foreach ($argv_list as $argv) { $phids[] = head($argv); } $tokens = $this->renderTokens($phids); foreach ($tokens as $token) { if ($token->isInvalid()) { $token ->setValue(pht('Members: Invalid Project')); } else { $token ->setIcon('fa-users') ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) ->setKey('members('.$token->getKey().')') ->setValue(pht('Members: %s', $token->getValue())); } } return $tokens; } } diff --git a/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php b/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php index d4f569e78c..ed24e5e88b 100644 --- a/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php @@ -1,62 +1,66 @@ array( 'name' => pht('Find results which are not in any projects.'), ), ); } public function loadResults() { $results = array( $this->buildNullResult(), ); return $this->filterResultsAgainstTokens($results); } protected function evaluateFunction($function, array $argv_list) { $results = array(); foreach ($argv_list as $argv) { $results[] = new PhabricatorQueryConstraint( PhabricatorQueryConstraint::OPERATOR_NULL, 'empty'); } return $results; } public function renderFunctionTokens($function, array $argv_list) { $results = array(); foreach ($argv_list as $argv) { $results[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( $this->buildNullResult()); } return $results; } private function buildNullResult() { $name = pht('Not In Any Projects'); return $this->newFunctionResult() ->setUnique(true) ->setPHID('null()') ->setIcon('fa-ban') ->setName('null '.$name) ->setDisplayName($name); } } diff --git a/src/applications/project/typeahead/PhabricatorProjectOrUserDatasource.php b/src/applications/project/typeahead/PhabricatorProjectOrUserDatasource.php index 2024fa3b8b..224828ee68 100644 --- a/src/applications/project/typeahead/PhabricatorProjectOrUserDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectOrUserDatasource.php @@ -1,17 +1,21 @@ setEnrichResults(true), new PhabricatorProjectDatasource(), new PhabricatorApplicationDatasource(), new PhabricatorTypeaheadMonogramDatasource(), new DiffusionSymbolDatasource(), ); } } diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php index 773c031e43..dcca0782bb 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -1,322 +1,322 @@ getRequest(); $viewer = $request->getUser(); $query = $request->getStr('q'); $offset = $request->getInt('offset'); $select_phid = null; $is_browse = ($request->getURIData('action') == 'browse'); $select = $request->getStr('select'); if ($select) { $select = phutil_json_decode($select); $query = idx($select, 'q'); $offset = idx($select, 'offset'); $select_phid = idx($select, 'phid'); } // 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); $hard_limit = 1000; $limit = 100; $composite ->setViewer($viewer) ->setQuery($query) ->setRawQuery($raw_query) ->setLimit($limit + 1); if ($is_browse) { if (!$composite->isBrowsable()) { return new Aphront404Response(); } 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 ->setOffset($offset); } $results = $composite->loadResults(); if ($is_browse) { // If this is a request for a specific token after the user clicks // "Select", return the token in wire format so it can be added to // the tokenizer. if ($select_phid) { $map = mpull($results, null, 'getPHID'); $token = idx($map, $select_phid); if (!$token) { return new Aphront404Response(); } $payload = array( 'key' => $token->getPHID(), 'token' => $token->getWireFormat(), ); return id(new AphrontAjaxResponse())->setContent($payload); } $format = $request->getStr('format'); switch ($format) { case 'html': case 'dialog': // These are the acceptable response formats. break; default: // Return a dialog if format information is missing or invalid. $format = 'dialog'; break; } $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) ->setQueryParam('format', 'html'); $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.')); } } $exclude = $request->getStrList('exclude'); $exclude = array_fuse($exclude); $select = array( 'offset' => $offset, 'q' => $query, ); $items = array(); foreach ($results as $result) { $token = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( $result); // Disable already-selected tokens. $disabled = isset($exclude[$result->getPHID()]); $value = $select + array('phid' => $result->getPHID()); $value = json_encode($value); $button = phutil_tag( 'button', array( 'class' => 'small grey', 'name' => 'select', 'value' => $value, 'disabled' => $disabled ? 'disabled' : null, ), pht('Select')); $items[] = phutil_tag( 'div', array( 'class' => 'typeahead-browse-item grouped', ), array( $token, $button, )); } $markup = array( $items, $next_link, ); if ($format == 'html') { $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. + ->setTitle($source->getBrowseTitle()) ->appendChild($browser) ->addCancelButton('/', pht('Close')); } } else if ($is_browse) { return new Aphront404Response(); } else { $results = array(); } $content = mpull($results, 'getWireFormat'); $content = array_values($content); 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/PhabricatorTypeaheadDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php index 63f5737f57..4c51deb3a2 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php @@ -1,413 +1,418 @@ 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; } public function getBrowseURI() { if (!$this->isBrowsable()) { return null; } $uri = new PhutilURI('/typeahead/browse/'.get_class($this).'/'); $uri->setQueryParams($this->parameters); return (string)$uri; } abstract public function getPlaceholderText(); + + public function getBrowseTitle() { + return get_class($this); + } + abstract public function getDatasourceApplicationClass(); abstract public function loadResults(); protected function didLoadResults(array $results) { return $results; } 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; } protected function newFunctionResult() { return id(new PhabricatorTypeaheadResult()) ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_FUNCTION) ->setIcon('fa-asterisk'); } public function newInvalidToken($name) { return id(new PhabricatorTypeaheadTokenView()) ->setValue($name) ->setIcon('fa-exclamation-circle') ->setTokenType(PhabricatorTypeaheadTokenView::TYPE_INVALID); } public function renderTokens(array $values) { $phids = array(); $setup = array(); $tokens = array(); foreach ($values as $key => $value) { if (!self::isFunctionToken($value)) { $phids[$key] = $value; } else { $function = $this->parseFunction($value); if ($function) { $setup[$function['name']][$key] = $function; } else { $name = pht('Invalid Function: %s', $value); $tokens[$key] = $this->newInvalidToken($name) ->setKey($value); } } } if ($phids) { $handles = $this->getViewer()->loadHandles($phids); foreach ($phids as $key => $phid) { $handle = $handles[$phid]; $tokens[$key] = PhabricatorTypeaheadTokenView::newFromHandle($handle); } } if ($setup) { foreach ($setup as $function_name => $argv_list) { // Render the function tokens. $function_tokens = $this->renderFunctionTokens( $function_name, ipull($argv_list, 'argv')); // Rekey the function tokens using the original array keys. $function_tokens = array_combine( array_keys($argv_list), $function_tokens); // For any functions which were invalid, set their value to the // original input value before it was parsed. foreach ($function_tokens as $key => $token) { $type = $token->getTokenType(); if ($type == PhabricatorTypeaheadTokenView::TYPE_INVALID) { $token->setKey($values[$key]); } } $tokens += $function_tokens; } } return array_select_keys($tokens, array_keys($values)); } /* -( Token Functions )---------------------------------------------------- */ /** * @task functions */ public function getDatasourceFunctions() { return array(); } /** * @task functions */ protected function canEvaluateFunction($function) { return $this->shouldStripFunction($function); } /** * @task functions */ protected function shouldStripFunction($function) { $functions = $this->getDatasourceFunctions(); return isset($functions[$function]); } /** * @task functions */ protected function evaluateFunction($function, array $argv_list) { throw new PhutilMethodNotImplementedException(); } /** * @task functions */ public function evaluateTokens(array $tokens) { $results = array(); $evaluate = array(); foreach ($tokens as $token) { if (!self::isFunctionToken($token)) { $results[] = $token; } else { $evaluate[] = $token; } } foreach ($evaluate as $function) { $function = self::parseFunction($function); if (!$function) { throw new PhabricatorTypeaheadInvalidTokenException(); } $name = $function['name']; $argv = $function['argv']; foreach ($this->evaluateFunction($name, array($argv)) as $phid) { $results[] = $phid; } } $results = $this->didEvaluateTokens($results); return $results; } /** * @task functions */ protected function didEvaluateTokens(array $results) { return $results; } /** * @task functions */ public static function isFunctionToken($token) { // We're looking for a "(" so that a string like "members(q" is identified // and parsed as a function call. This allows us to start generating // results immeidately, before the user fully types out "members(quack)". return (strpos($token, '(') !== false); } /** * @task functions */ public function parseFunction($token, $allow_partial = false) { $matches = null; if ($allow_partial) { $ok = preg_match('/^([^(]+)\((.*?)\)?$/', $token, $matches); } else { $ok = preg_match('/^([^(]+)\((.*)\)$/', $token, $matches); } if (!$ok) { return null; } $function = trim($matches[1]); if (!$this->canEvaluateFunction($function)) { return null; } return array( 'name' => $function, 'argv' => array(trim($matches[2])), ); } /** * @task functions */ public function renderFunctionTokens($function, array $argv_list) { throw new PhutilMethodNotImplementedException(); } /** * @task functions */ public function setFunctionStack(array $function_stack) { $this->functionStack = $function_stack; return $this; } /** * @task functions */ public function getFunctionStack() { return $this->functionStack; } /** * @task functions */ protected function getCurrentFunction() { return nonempty(last($this->functionStack), null); } } diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php index ebaf3d39d8..4a530d4be3 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php @@ -1,50 +1,54 @@ 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; } } diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadNoOwnerDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadNoOwnerDatasource.php index 76dd3148d8..61f2c89713 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadNoOwnerDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadNoOwnerDatasource.php @@ -1,28 +1,32 @@ getViewer(); $raw_query = $this->getRawQuery(); $results = array(); $results[] = id(new PhabricatorTypeaheadResult()) ->setName(pht('None')) ->setIcon('fa-ban orange') ->setPHID(ManiphestTaskOwner::OWNER_UP_FOR_GRABS); return $results; } } diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadOwnerDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadOwnerDatasource.php index a2aaf4f991..bfd8b090d6 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadOwnerDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadOwnerDatasource.php @@ -1,17 +1,21 @@