@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement viewer() and members(project) typeahead functions

Summary:
Ref T4100. This is still a bit rough around the edges, but mostly does what we're after.

- Implements viewer() and members(...) functions.
- The new browse workflow makes these discoverable.

Test Plan: {F374201}

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: chad, epriestley

Maniphest Tasks: T4100

Differential Revision: https://secure.phabricator.com/D12444

+510 -71
+8
src/__phutil_library_map__.php
··· 2295 2295 'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php', 2296 2296 'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php', 2297 2297 'PhabricatorProjectMemberOfProjectEdgeType' => 'applications/project/edge/PhabricatorProjectMemberOfProjectEdgeType.php', 2298 + 'PhabricatorProjectMembersDatasource' => 'applications/project/typeahead/PhabricatorProjectMembersDatasource.php', 2298 2299 'PhabricatorProjectMembersEditController' => 'applications/project/controller/PhabricatorProjectMembersEditController.php', 2299 2300 'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php', 2300 2301 'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php', ··· 2627 2628 'PhabricatorTypeaheadCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php', 2628 2629 'PhabricatorTypeaheadDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php', 2629 2630 'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php', 2631 + 'PhabricatorTypeaheadInvalidTokenException' => 'applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php', 2630 2632 'PhabricatorTypeaheadModularDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php', 2631 2633 'PhabricatorTypeaheadMonogramDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php', 2632 2634 'PhabricatorTypeaheadNoOwnerDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadNoOwnerDatasource.php', ··· 2634 2636 'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php', 2635 2637 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadRuntimeCompositeDatasource.php', 2636 2638 'PhabricatorTypeaheadTokenView' => 'applications/typeahead/view/PhabricatorTypeaheadTokenView.php', 2639 + 'PhabricatorTypeaheadUserParameterizedDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadUserParameterizedDatasource.php', 2637 2640 'PhabricatorUIConfigOptions' => 'applications/config/option/PhabricatorUIConfigOptions.php', 2638 2641 'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php', 2639 2642 'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php', ··· 2671 2674 'PhabricatorUsersPolicyRule' => 'applications/policy/rule/PhabricatorUsersPolicyRule.php', 2672 2675 'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php', 2673 2676 'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php', 2677 + 'PhabricatorViewerDatasource' => 'applications/people/typeahead/PhabricatorViewerDatasource.php', 2674 2678 'PhabricatorWatcherHasObjectEdgeType' => 'applications/transactions/edges/PhabricatorWatcherHasObjectEdgeType.php', 2675 2679 'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php', 2676 2680 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', ··· 5664 5668 'PhabricatorProjectIcon' => 'Phobject', 5665 5669 'PhabricatorProjectListController' => 'PhabricatorProjectController', 5666 5670 'PhabricatorProjectMemberOfProjectEdgeType' => 'PhabricatorEdgeType', 5671 + 'PhabricatorProjectMembersDatasource' => 'PhabricatorTypeaheadDatasource', 5667 5672 'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController', 5668 5673 'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController', 5669 5674 'PhabricatorProjectMoveController' => 'PhabricatorProjectController', ··· 6026 6031 'PhabricatorTypeaheadCompositeDatasource' => 'PhabricatorTypeaheadDatasource', 6027 6032 'PhabricatorTypeaheadDatasource' => 'Phobject', 6028 6033 'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController', 6034 + 'PhabricatorTypeaheadInvalidTokenException' => 'Exception', 6029 6035 'PhabricatorTypeaheadModularDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 6030 6036 'PhabricatorTypeaheadMonogramDatasource' => 'PhabricatorTypeaheadDatasource', 6031 6037 'PhabricatorTypeaheadNoOwnerDatasource' => 'PhabricatorTypeaheadDatasource', 6032 6038 'PhabricatorTypeaheadOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 6033 6039 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 6034 6040 'PhabricatorTypeaheadTokenView' => 'AphrontTagView', 6041 + 'PhabricatorTypeaheadUserParameterizedDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 6035 6042 'PhabricatorUIConfigOptions' => 'PhabricatorApplicationConfigOptions', 6036 6043 'PhabricatorUIExampleRenderController' => 'PhabricatorController', 6037 6044 'PhabricatorUIExamplesApplication' => 'PhabricatorApplication', ··· 6081 6088 'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule', 6082 6089 'PhabricatorVCSResponse' => 'AphrontResponse', 6083 6090 'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation', 6091 + 'PhabricatorViewerDatasource' => 'PhabricatorTypeaheadDatasource', 6084 6092 'PhabricatorWatcherHasObjectEdgeType' => 'PhabricatorEdgeType', 6085 6093 'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider', 6086 6094 'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask',
+6 -2
src/applications/differential/query/DifferentialRevisionSearchEngine.php
··· 67 67 ->needDrafts(true) 68 68 ->needRelationships(true); 69 69 70 + $datasource = id(new PhabricatorTypeaheadUserParameterizedDatasource()) 71 + ->setViewer($this->requireViewer()); 72 + 70 73 $responsible_phids = $saved->getParameter('responsiblePHIDs', array()); 74 + $responsible_phids = $datasource->evaluateTokens($responsible_phids); 71 75 if ($responsible_phids) { 72 76 $query->withResponsibleUsers($responsible_phids); 73 77 } ··· 129 133 id(new AphrontFormTokenizerControl()) 130 134 ->setLabel(pht('Responsible Users')) 131 135 ->setName('responsibles') 132 - ->setDatasource(new PhabricatorPeopleDatasource()) 136 + ->setDatasource(new PhabricatorTypeaheadUserParameterizedDatasource()) 133 137 ->setValue($responsible_phids)) 134 138 ->appendControl( 135 139 id(new AphrontFormTokenizerControl()) ··· 208 212 switch ($query_key) { 209 213 case 'active': 210 214 return $query 211 - ->setParameter('responsiblePHIDs', array($viewer->getPHID())) 215 + ->setParameter('responsiblePHIDs', array('viewer()')) 212 216 ->setParameter('status', DifferentialRevisionQuery::STATUS_OPEN); 213 217 case 'authored': 214 218 return $query
+1 -1
src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php
··· 93 93 ); 94 94 } 95 95 96 - public function getPagingValueMap($cursor, array $keys) { 96 + protected function getPagingValueMap($cursor, array $keys) { 97 97 $plan = $this->loadCursorObject($cursor); 98 98 return array( 99 99 'id' => $plan->getID(),
+1 -1
src/applications/macro/query/PhabricatorMacroQuery.php
··· 258 258 ); 259 259 } 260 260 261 - public function getPagingValueMap($cursor, array $keys) { 261 + protected function getPagingValueMap($cursor, array $keys) { 262 262 $macro = $this->loadCursorObject($cursor); 263 263 return array( 264 264 'id' => $macro->getID(),
+1 -1
src/applications/people/query/PhabricatorPeopleQuery.php
··· 338 338 ); 339 339 } 340 340 341 - public function getPagingValueMap($cursor, array $keys) { 341 + protected function getPagingValueMap($cursor, array $keys) { 342 342 $user = $this->loadCursorObject($cursor); 343 343 return array( 344 344 'id' => $user->getID(),
+56
src/applications/people/typeahead/PhabricatorViewerDatasource.php
··· 1 + <?php 2 + 3 + final class PhabricatorViewerDatasource 4 + extends PhabricatorTypeaheadDatasource { 5 + 6 + public function getPlaceholderText() { 7 + return pht('Type viewer()...'); 8 + } 9 + 10 + public function getDatasourceApplicationClass() { 11 + return 'PhabricatorPeopleApplication'; 12 + } 13 + 14 + public function loadResults() { 15 + if ($this->getViewer()->getPHID()) { 16 + $results = array($this->renderViewerFunctionToken()); 17 + } else { 18 + $results = array(); 19 + } 20 + 21 + return $this->filterResultsAgainstTokens($results); 22 + } 23 + 24 + protected function canEvaluateFunction($function) { 25 + if (!$this->getViewer()->getPHID()) { 26 + return false; 27 + } 28 + 29 + return ($function == 'viewer'); 30 + } 31 + 32 + protected function evaluateFunction($function, array $argv_list) { 33 + $results = array(); 34 + foreach ($argv_list as $argv) { 35 + $results[] = $this->getViewer()->getPHID(); 36 + } 37 + return $results; 38 + } 39 + 40 + public function renderFunctionTokens($function, array $argv_list) { 41 + $tokens = array(); 42 + foreach ($argv_list as $argv) { 43 + $tokens[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( 44 + $this->renderViewerFunctionToken()); 45 + } 46 + return $tokens; 47 + } 48 + 49 + private function renderViewerFunctionToken() { 50 + return $this->newFunctionResult() 51 + ->setName(pht('Current Viewer')) 52 + ->setPHID('viewer()') 53 + ->setUnique(true); 54 + } 55 + 56 + }
+122
src/applications/project/typeahead/PhabricatorProjectMembersDatasource.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectMembersDatasource 4 + extends PhabricatorTypeaheadDatasource { 5 + 6 + public function getPlaceholderText() { 7 + return pht('Type members(<project>)...'); 8 + } 9 + 10 + public function getDatasourceApplicationClass() { 11 + return 'PhabricatorProjectApplication'; 12 + } 13 + 14 + public function loadResults() { 15 + $viewer = $this->getViewer(); 16 + $raw_query = $this->getRawQuery(); 17 + 18 + $pattern = $raw_query; 19 + if (self::isFunctionToken($raw_query)) { 20 + $function = $this->parseFunction($raw_query, $allow_partial = true); 21 + if ($function) { 22 + $pattern = head($function['argv']); 23 + } 24 + } 25 + 26 + // Allow users to type "#qa" or "qa" to find "Quality Assurance". 27 + $pattern = ltrim($pattern, '#'); 28 + $tokens = self::tokenizeString($pattern); 29 + 30 + $query = $this->newQuery(); 31 + if ($tokens) { 32 + $query->withNameTokens($tokens); 33 + } 34 + $projects = $this->executeQuery($query); 35 + 36 + $results = array(); 37 + foreach ($projects as $project) { 38 + $results[] = $this->buildProjectResult($project); 39 + } 40 + 41 + return $results; 42 + } 43 + 44 + protected function canEvaluateFunction($function) { 45 + return ($function == 'members'); 46 + } 47 + 48 + protected function evaluateFunction($function, array $argv_list) { 49 + $phids = array(); 50 + foreach ($argv_list as $argv) { 51 + $phids[] = head($argv); 52 + } 53 + 54 + $projects = id(new PhabricatorProjectQuery()) 55 + ->setViewer($this->getViewer()) 56 + ->needMembers(true) 57 + ->withPHIDs($phids) 58 + ->execute(); 59 + 60 + $results = array(); 61 + foreach ($projects as $project) { 62 + foreach ($project->getMemberPHIDs() as $phid) { 63 + $results[$phid] = $phid; 64 + } 65 + } 66 + 67 + return array_values($results); 68 + } 69 + 70 + public function renderFunctionTokens($function, array $argv_list) { 71 + $phids = array(); 72 + foreach ($argv_list as $argv) { 73 + $phids[] = head($argv); 74 + } 75 + 76 + $projects = $this->newQuery() 77 + ->withPHIDs($phids) 78 + ->execute(); 79 + $projects = mpull($projects, null, 'getPHID'); 80 + 81 + $tokens = array(); 82 + foreach ($phids as $phid) { 83 + $project = idx($projects, $phid); 84 + if ($project) { 85 + $result = $this->buildProjectResult($project); 86 + $tokens[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( 87 + $result); 88 + } else { 89 + $tokens[] = $this->newInvalidToken(pht('Members: Invalid Project')); 90 + } 91 + } 92 + 93 + return $tokens; 94 + } 95 + 96 + private function newQuery() { 97 + return id(new PhabricatorProjectQuery()) 98 + ->setViewer($this->getViewer()) 99 + ->needImages(true) 100 + ->needSlugs(true); 101 + } 102 + 103 + private function buildProjectResult(PhabricatorProject $project) { 104 + $closed = null; 105 + if ($project->isArchived()) { 106 + $closed = pht('Archived'); 107 + } 108 + 109 + $all_strings = mpull($project->getSlugs(), 'getSlug'); 110 + $all_strings[] = 'members'; 111 + $all_strings[] = $project->getName(); 112 + $all_strings = implode(' ', $all_strings); 113 + 114 + return $this->newFunctionResult() 115 + ->setName($all_strings) 116 + ->setDisplayName(pht('Members: %s', $project->getName())) 117 + ->setURI('/tag/'.$project->getPrimarySlug().'/') 118 + ->setPHID('members('.$project->getPHID().')') 119 + ->setClosed($closed); 120 + } 121 + 122 + }
+40 -31
src/applications/search/controller/PhabricatorApplicationSearchController.php
··· 165 165 $errors = $engine->getErrors(); 166 166 if ($errors) { 167 167 $run_query = false; 168 - $errors = id(new PHUIInfoView()) 169 - ->setTitle(pht('Query Errors')) 170 - ->setErrors($errors); 171 168 } 172 169 173 170 $submit = id(new AphrontFormSubmitControl()) ··· 214 211 $anchor = id(new PhabricatorAnchorView()) 215 212 ->setAnchorName('R')); 216 213 217 - $query = $engine->buildQueryFromSavedQuery($saved_query); 214 + try { 215 + $query = $engine->buildQueryFromSavedQuery($saved_query); 218 216 219 - $pager = $engine->newPagerForSavedQuery($saved_query); 220 - $pager->readFromRequest($request); 217 + $pager = $engine->newPagerForSavedQuery($saved_query); 218 + $pager->readFromRequest($request); 221 219 222 - $objects = $engine->executeQuery($query, $pager); 220 + $objects = $engine->executeQuery($query, $pager); 223 221 224 - // TODO: To support Dashboard panels, rendering is moving into 225 - // SearchEngines. Move it all the way in and then get rid of this. 222 + // TODO: To support Dashboard panels, rendering is moving into 223 + // SearchEngines. Move it all the way in and then get rid of this. 226 224 227 - $interface = 'PhabricatorApplicationSearchResultsControllerInterface'; 228 - if ($parent instanceof $interface) { 229 - $list = $parent->renderResultsList($objects, $saved_query); 230 - } else { 231 - $engine->setRequest($request); 225 + $interface = 'PhabricatorApplicationSearchResultsControllerInterface'; 226 + if ($parent instanceof $interface) { 227 + $list = $parent->renderResultsList($objects, $saved_query); 228 + } else { 229 + $engine->setRequest($request); 232 230 233 - $list = $engine->renderResults( 234 - $objects, 235 - $saved_query); 236 - } 231 + $list = $engine->renderResults( 232 + $objects, 233 + $saved_query); 234 + } 237 235 238 - $nav->appendChild($list); 236 + $nav->appendChild($list); 239 237 240 - // TODO: This is a bit hacky. 241 - if ($list instanceof PHUIObjectItemListView) { 242 - $list->setNoDataString(pht('No results found for this query.')); 243 - $list->setPager($pager); 244 - } else { 245 - if ($pager->willShowPagingControls()) { 246 - $pager_box = id(new PHUIBoxView()) 247 - ->addPadding(PHUI::PADDING_MEDIUM) 248 - ->addMargin(PHUI::MARGIN_LARGE) 249 - ->setBorder(true) 250 - ->appendChild($pager); 251 - $nav->appendChild($pager_box); 238 + // TODO: This is a bit hacky. 239 + if ($list instanceof PHUIObjectItemListView) { 240 + $list->setNoDataString(pht('No results found for this query.')); 241 + $list->setPager($pager); 242 + } else { 243 + if ($pager->willShowPagingControls()) { 244 + $pager_box = id(new PHUIBoxView()) 245 + ->addPadding(PHUI::PADDING_MEDIUM) 246 + ->addMargin(PHUI::MARGIN_LARGE) 247 + ->setBorder(true) 248 + ->appendChild($pager); 249 + $nav->appendChild($pager_box); 250 + } 252 251 } 252 + } catch (PhabricatorTypeaheadInvalidTokenException $ex) { 253 + $errors[] = pht( 254 + 'This query specifies an invalid parameter. Review the '. 255 + 'query parameters and correct errors.'); 253 256 } 257 + } 258 + 259 + if ($errors) { 260 + $errors = id(new PHUIInfoView()) 261 + ->setTitle(pht('Query Errors')) 262 + ->setErrors($errors); 254 263 } 255 264 256 265 if ($errors) {
+7 -1
src/applications/search/engine/PhabricatorApplicationSearchEngine.php
··· 383 383 } else if (isset($allow_types[$type])) { 384 384 $phids[] = $item; 385 385 } else { 386 - $names[] = $item; 386 + if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) { 387 + // If this is a function, pass it through unchanged; we'll evaluate 388 + // it later. 389 + $phids[] = $item; 390 + } else { 391 + $names[] = $item; 392 + } 387 393 } 388 394 } 389 395
+2 -3
src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php
··· 49 49 ->setRawQuery($raw_query); 50 50 51 51 $hard_limit = 1000; 52 + $limit = 100; 52 53 53 54 if ($is_browse) { 54 55 if (!$composite->isBrowsable()) { 55 56 return new Aphront404Response(); 56 57 } 57 - 58 - $limit = 10; 59 58 60 59 if (($offset + $limit) >= $hard_limit) { 61 60 // Offset-based paging is intrinsically slow; hard-cap how far we're ··· 140 139 141 140 $items = array(); 142 141 foreach ($results as $result) { 143 - $token = PhabricatorTypeaheadTokenView::newForTypeaheadResult( 142 + $token = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( 144 143 $result); 145 144 146 145 // Disable already-selected tokens.
+34
src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php
··· 72 72 } 73 73 } 74 74 75 + $source->setViewer($this->getViewer()); 75 76 $usable[] = $source; 76 77 } 77 78 $this->usable = $usable; ··· 79 80 80 81 return $this->usable; 81 82 } 83 + 84 + 85 + protected function canEvaluateFunction($function) { 86 + foreach ($this->getUsableDatasources() as $source) { 87 + if ($source->canEvaluateFunction($function)) { 88 + return true; 89 + } 90 + } 91 + 92 + return parent::canEvaluateFunction($function); 93 + } 94 + 95 + 96 + protected function evaluateFunction($function, array $argv) { 97 + foreach ($this->getUsableDatasources() as $source) { 98 + if ($source->canEvaluateFunction($function)) { 99 + return $source->evaluateFunction($function, $argv); 100 + } 101 + } 102 + 103 + return parent::evaluateFunction($function, $argv); 104 + } 105 + 106 + public function renderFunctionTokens($function, array $argv_list) { 107 + foreach ($this->getUsableDatasources() as $source) { 108 + if ($source->canEvaluateFunction($function)) { 109 + return $source->renderFunctionTokens($function, $argv_list); 110 + } 111 + } 112 + 113 + return parent::renderFunctionTokens($function, $argv_list); 114 + } 115 + 82 116 83 117 }
+115
src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php
··· 1 1 <?php 2 2 3 + /** 4 + * @task functions Token Functions 5 + */ 3 6 abstract class PhabricatorTypeaheadDatasource extends Phobject { 4 7 5 8 private $viewer; ··· 182 185 183 186 return $results; 184 187 } 188 + 189 + protected function newFunctionResult() { 190 + // TODO: Find a more consistent design. 191 + return id(new PhabricatorTypeaheadResult()) 192 + ->setIcon('fa-magic indigo'); 193 + } 194 + 195 + public function newInvalidToken($name) { 196 + return id(new PhabricatorTypeaheadTokenView()) 197 + ->setKey(PhabricatorTypeaheadTokenView::KEY_INVALID) 198 + ->setValue($name) 199 + ->setIcon('fa-exclamation-circle red'); 200 + } 201 + 202 + /* -( Token Functions )---------------------------------------------------- */ 203 + 204 + 205 + /** 206 + * @task functions 207 + */ 208 + protected function canEvaluateFunction($function) { 209 + return false; 210 + } 211 + 212 + 213 + /** 214 + * @task functions 215 + */ 216 + protected function evaluateFunction($function, array $argv_list) { 217 + throw new PhutilMethodNotImplementedException(); 218 + } 219 + 220 + 221 + /** 222 + * @task functions 223 + */ 224 + public function evaluateTokens(array $tokens) { 225 + $results = array(); 226 + $evaluate = array(); 227 + foreach ($tokens as $token) { 228 + if (!self::isFunctionToken($token)) { 229 + $results[] = $token; 230 + } else { 231 + $evaluate[] = $token; 232 + } 233 + } 234 + 235 + foreach ($evaluate as $function) { 236 + $function = self::parseFunction($function); 237 + if (!$function) { 238 + throw new PhabricatorTypeaheadInvalidTokenException(); 239 + } 240 + 241 + $name = $function['name']; 242 + $argv = $function['argv']; 243 + 244 + foreach ($this->evaluateFunction($name, array($argv)) as $phid) { 245 + $results[] = $phid; 246 + } 247 + } 248 + 249 + return $results; 250 + } 251 + 252 + 253 + /** 254 + * @task functions 255 + */ 256 + public static function isFunctionToken($token) { 257 + // We're looking for a "(" so that a string like "members(q" is identified 258 + // and parsed as a function call. This allows us to start generating 259 + // results immeidately, before the user fully types out "members(quack)". 260 + return (strpos($token, '(') !== false); 261 + } 262 + 263 + 264 + /** 265 + * @task functions 266 + */ 267 + public function parseFunction($token, $allow_partial = false) { 268 + $matches = null; 269 + 270 + if ($allow_partial) { 271 + $ok = preg_match('/^([^(]+)\((.*)$/', $token, $matches); 272 + } else { 273 + $ok = preg_match('/^([^(]+)\((.*)\)$/', $token, $matches); 274 + } 275 + 276 + if (!$ok) { 277 + return null; 278 + } 279 + 280 + $function = trim($matches[1]); 281 + 282 + if (!$this->canEvaluateFunction($function)) { 283 + return null; 284 + } 285 + 286 + return array( 287 + 'name' => $function, 288 + 'argv' => array(trim($matches[2])), 289 + ); 290 + } 291 + 292 + 293 + /** 294 + * @task functions 295 + */ 296 + public function renderFunctionTokens($function, array $argv_list) { 297 + throw new PhutilMethodNotImplementedException(); 298 + } 299 + 185 300 186 301 }
+20
src/applications/typeahead/datasource/PhabricatorTypeaheadUserParameterizedDatasource.php
··· 1 + <?php 2 + 3 + final class PhabricatorTypeaheadUserParameterizedDatasource 4 + extends PhabricatorTypeaheadCompositeDatasource { 5 + 6 + public function getPlaceholderText() { 7 + return pht('Type a username or selector...'); 8 + } 9 + 10 + public function getComponentDatasources() { 11 + $sources = array( 12 + new PhabricatorViewerDatasource(), 13 + new PhabricatorPeopleDatasource(), 14 + new PhabricatorProjectMembersDatasource(), 15 + ); 16 + 17 + return $sources; 18 + } 19 + 20 + }
+3
src/applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php
··· 1 + <?php 2 + 3 + final class PhabricatorTypeaheadInvalidTokenException extends Exception {}
+16 -1
src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
··· 13 13 private $imageSprite; 14 14 private $icon; 15 15 private $closed; 16 + private $unique; 16 17 17 18 public function setIcon($icon) { 18 19 $this->icon = $icon; ··· 85 86 return $this->phid; 86 87 } 87 88 89 + public function setUnique($unique) { 90 + $this->unique = $unique; 91 + return $this; 92 + } 93 + 88 94 public function getSortKey() { 89 - return phutil_utf8_strtolower($this->getName()); 95 + // Put unique results (special parameter functions) ahead of other 96 + // results. 97 + if ($this->unique) { 98 + $prefix = 'A'; 99 + } else { 100 + $prefix = 'B'; 101 + } 102 + 103 + return $prefix.phutil_utf8_strtolower($this->getName()); 90 104 } 91 105 92 106 public function getWireFormat() { ··· 102 116 $this->getIcon(), 103 117 $this->closed, 104 118 $this->imageSprite ? (string)$this->imageSprite : null, 119 + $this->unique ? 1 : null, 105 120 ); 106 121 while (end($data) === null) { 107 122 array_pop($data);
+12 -1
src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php
··· 8 8 private $inputName; 9 9 private $value; 10 10 11 - public static function newForTypeaheadResult( 11 + const KEY_INVALID = '<invalid>'; 12 + 13 + public static function newFromTypeaheadResult( 12 14 PhabricatorTypeaheadResult $result) { 13 15 14 16 return id(new PhabricatorTypeaheadTokenView()) 15 17 ->setKey($result->getPHID()) 16 18 ->setIcon($result->getIcon()) 17 19 ->setValue($result->getDisplayName()); 20 + } 21 + 22 + public static function newFromHandle( 23 + PhabricatorObjectHandle $handle) { 24 + 25 + return id(new PhabricatorTypeaheadTokenView()) 26 + ->setKey($handle->getPHID()) 27 + ->setValue($handle->getFullName()) 28 + ->setIcon($handle->getIcon()); 18 29 } 19 30 20 31 public function setKey($key) {
+2 -18
src/view/control/AphrontTokenizerTemplateView.php
··· 18 18 } 19 19 20 20 public function setValue(array $value) { 21 - assert_instances_of($value, 'PhabricatorObjectHandle'); 21 + assert_instances_of($value, 'PhabricatorTypeaheadTokenView'); 22 22 $this->value = $value; 23 23 return $this; 24 24 } ··· 41 41 42 42 $id = $this->id; 43 43 $name = $this->getName(); 44 - $values = nonempty($this->getValue(), array()); 45 - 46 - $tokens = array(); 47 - foreach ($values as $key => $value) { 48 - $tokens[] = $this->renderToken( 49 - $value->getPHID(), 50 - $value->getFullName(), 51 - $value->getType()); 52 - } 44 + $tokens = nonempty($this->getValue(), array()); 53 45 54 46 $input = javelin_tag( 55 47 'input', ··· 123 115 ))); 124 116 125 117 return $frame; 126 - } 127 - 128 - private function renderToken($key, $value, $icon) { 129 - return id(new PhabricatorTypeaheadTokenView()) 130 - ->setKey($key) 131 - ->setValue($value) 132 - ->setIcon($icon) 133 - ->setInputName($this->getName()); 134 118 } 135 119 136 120 }
+47 -8
src/view/form/control/AphrontFormTokenizerControl.php
··· 50 50 $id = celerity_generate_unique_node_id(); 51 51 } 52 52 53 + $datasource = $this->datasource; 54 + $datasource->setViewer($this->getUser()); 55 + 53 56 $placeholder = null; 54 57 if (!strlen($this->placeholder)) { 55 - if ($this->datasource) { 56 - $placeholder = $this->datasource->getPlaceholderText(); 58 + if ($datasource) { 59 + $placeholder = $datasource->getPlaceholderText(); 57 60 } 58 61 } else { 59 62 $placeholder = $this->placeholder; 60 63 } 61 64 65 + $tokens = array(); 66 + $values = nonempty($this->getValue(), array()); 67 + foreach ($values as $value) { 68 + if (isset($handles[$value])) { 69 + $token = PhabricatorTypeaheadTokenView::newFromHandle($handles[$value]); 70 + } else { 71 + $token = null; 72 + if ($datasource) { 73 + $function = $datasource->parseFunction($value); 74 + if ($function) { 75 + $token_list = $datasource->renderFunctionTokens( 76 + $function['name'], 77 + array($function['argv'])); 78 + $token = head($token_list); 79 + } 80 + } 81 + 82 + if (!$token) { 83 + $name = pht('Invalid Function: %s', $value); 84 + $token = $datasource->newInvalidToken($name); 85 + } 86 + 87 + if ($token->getKey() == PhabricatorTypeaheadTokenView::KEY_INVALID) { 88 + $token->setKey($value); 89 + } 90 + } 91 + $token->setInputName($this->getName()); 92 + $tokens[] = $token; 93 + } 94 + 62 95 $template = new AphrontTokenizerTemplateView(); 63 96 $template->setName($name); 64 97 $template->setID($id); 65 - $template->setValue($handles); 98 + $template->setValue($tokens); 66 99 67 100 $username = null; 68 101 if ($this->user) { ··· 71 104 72 105 $datasource_uri = null; 73 106 $browse_uri = null; 74 - 75 - $datasource = $this->datasource; 76 107 if ($datasource) { 77 108 $datasource->setViewer($this->getUser()); 78 109 ··· 88 119 Javelin::initBehavior('aphront-basic-tokenizer', array( 89 120 'id' => $id, 90 121 'src' => $datasource_uri, 91 - 'value' => mpull($handles, 'getFullName', 'getPHID'), 92 - 'icons' => mpull($handles, 'getIcon', 'getPHID'), 122 + 'value' => mpull($tokens, 'getValue', 'getKey'), 123 + 'icons' => mpull($tokens, 'getIcon', 'getKey'), 93 124 'limit' => $this->limit, 94 125 'username' => $username, 95 126 'placeholder' => $placeholder, ··· 111 142 } 112 143 113 144 $values = nonempty($this->getValue(), array()); 114 - $this->handles = $viewer->loadHandles($values); 145 + 146 + $phids = array(); 147 + foreach ($values as $value) { 148 + if (!PhabricatorTypeaheadDatasource::isFunctionToken($value)) { 149 + $phids[] = $value; 150 + } 151 + } 152 + 153 + $this->handles = $viewer->loadHandles($phids); 115 154 } 116 155 117 156 return $this->handles;
+6 -2
webroot/rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js
··· 14 14 * @return string Normalized string. 15 15 */ 16 16 normalize : function(str) { 17 + 18 + // NOTE: We specifically normalize "(" and ")" into spaces so that 19 + // we can match tokenizer functions like "members(project)". 20 + 17 21 return ('' + str) 18 22 .toLocaleLowerCase() 19 - .replace(/[\.,\/#!$%\^&\*;:{}=_`~()]/g, '') 20 - .replace(/[-\[\]]/g, ' ') 23 + .replace(/[\.,\/#!$%\^&\*;:{}=_`~]/g, '') 24 + .replace(/[-\[\]\(\)]/g, ' ') 21 25 .replace(/ +/g, ' ') 22 26 .replace(/^\s*|\s*$/g, ''); 23 27 }
+9
webroot/rsrc/js/application/typeahead/behavior-typeahead-search.js
··· 10 10 var input = JX.$(config.inputID); 11 11 var frame = JX.$(config.frameID); 12 12 var last = input.value; 13 + var in_flight = {}; 13 14 14 15 function update() { 15 16 if (input.value == last) { ··· 30 31 return; 31 32 } 32 33 34 + if (value in in_flight) { 35 + // We've already sent a request for this query. 36 + return; 37 + } 38 + in_flight[value] = true; 39 + 33 40 JX.DOM.alterClass(frame, 'loading', true); 34 41 new JX.Workflow(config.uri, {q: value, format: 'html'}) 35 42 .setHandler(function(r) { 43 + delete in_flight[value]; 44 + 36 45 if (value != input.value) { 37 46 // The user typed some more stuff while the request was in flight, 38 47 // so ignore the response.
+2 -1
webroot/rsrc/js/core/Prefab.js
··· 287 287 icon: icon, 288 288 closed: closed, 289 289 type: fields[5], 290 - sprite: fields[10] 290 + sprite: fields[10], 291 + unique: fields[11] || false 291 292 }; 292 293 }, 293 294