diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2295,6 +2295,7 @@ 'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php', 'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php', 'PhabricatorProjectMemberOfProjectEdgeType' => 'applications/project/edge/PhabricatorProjectMemberOfProjectEdgeType.php', + 'PhabricatorProjectMembersDatasource' => 'applications/project/typeahead/PhabricatorProjectMembersDatasource.php', 'PhabricatorProjectMembersEditController' => 'applications/project/controller/PhabricatorProjectMembersEditController.php', 'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php', 'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php', @@ -2627,6 +2628,7 @@ 'PhabricatorTypeaheadCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php', 'PhabricatorTypeaheadDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php', 'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php', + 'PhabricatorTypeaheadInvalidTokenException' => 'applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php', 'PhabricatorTypeaheadModularDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php', 'PhabricatorTypeaheadMonogramDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadMonogramDatasource.php', 'PhabricatorTypeaheadNoOwnerDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadNoOwnerDatasource.php', @@ -2634,6 +2636,7 @@ 'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php', 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadRuntimeCompositeDatasource.php', 'PhabricatorTypeaheadTokenView' => 'applications/typeahead/view/PhabricatorTypeaheadTokenView.php', + 'PhabricatorTypeaheadUserParameterizedDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadUserParameterizedDatasource.php', 'PhabricatorUIConfigOptions' => 'applications/config/option/PhabricatorUIConfigOptions.php', 'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php', 'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php', @@ -2671,6 +2674,7 @@ 'PhabricatorUsersPolicyRule' => 'applications/policy/rule/PhabricatorUsersPolicyRule.php', 'PhabricatorVCSResponse' => 'applications/repository/response/PhabricatorVCSResponse.php', 'PhabricatorVeryWowEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorVeryWowEnglishTranslation.php', + 'PhabricatorViewerDatasource' => 'applications/people/typeahead/PhabricatorViewerDatasource.php', 'PhabricatorWatcherHasObjectEdgeType' => 'applications/transactions/edges/PhabricatorWatcherHasObjectEdgeType.php', 'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php', 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', @@ -5664,6 +5668,7 @@ 'PhabricatorProjectIcon' => 'Phobject', 'PhabricatorProjectListController' => 'PhabricatorProjectController', 'PhabricatorProjectMemberOfProjectEdgeType' => 'PhabricatorEdgeType', + 'PhabricatorProjectMembersDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController', 'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController', 'PhabricatorProjectMoveController' => 'PhabricatorProjectController', @@ -6026,12 +6031,14 @@ 'PhabricatorTypeaheadCompositeDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorTypeaheadDatasource' => 'Phobject', 'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController', + 'PhabricatorTypeaheadInvalidTokenException' => 'Exception', 'PhabricatorTypeaheadModularDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadMonogramDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorTypeaheadNoOwnerDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorTypeaheadOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorTypeaheadRuntimeCompositeDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorTypeaheadTokenView' => 'AphrontTagView', + 'PhabricatorTypeaheadUserParameterizedDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorUIConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorUIExampleRenderController' => 'PhabricatorController', 'PhabricatorUIExamplesApplication' => 'PhabricatorApplication', @@ -6081,6 +6088,7 @@ 'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule', 'PhabricatorVCSResponse' => 'AphrontResponse', 'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation', + 'PhabricatorViewerDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorWatcherHasObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask', diff --git a/src/applications/differential/query/DifferentialRevisionSearchEngine.php b/src/applications/differential/query/DifferentialRevisionSearchEngine.php --- a/src/applications/differential/query/DifferentialRevisionSearchEngine.php +++ b/src/applications/differential/query/DifferentialRevisionSearchEngine.php @@ -67,7 +67,11 @@ ->needDrafts(true) ->needRelationships(true); + $datasource = id(new PhabricatorTypeaheadUserParameterizedDatasource()) + ->setViewer($this->requireViewer()); + $responsible_phids = $saved->getParameter('responsiblePHIDs', array()); + $responsible_phids = $datasource->evaluateTokens($responsible_phids); if ($responsible_phids) { $query->withResponsibleUsers($responsible_phids); } @@ -129,7 +133,7 @@ id(new AphrontFormTokenizerControl()) ->setLabel(pht('Responsible Users')) ->setName('responsibles') - ->setDatasource(new PhabricatorPeopleDatasource()) + ->setDatasource(new PhabricatorTypeaheadUserParameterizedDatasource()) ->setValue($responsible_phids)) ->appendControl( id(new AphrontFormTokenizerControl()) @@ -208,7 +212,7 @@ switch ($query_key) { case 'active': return $query - ->setParameter('responsiblePHIDs', array($viewer->getPHID())) + ->setParameter('responsiblePHIDs', array('viewer()')) ->setParameter('status', DifferentialRevisionQuery::STATUS_OPEN); case 'authored': return $query diff --git a/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php b/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php --- a/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php +++ b/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php @@ -93,7 +93,7 @@ ); } - public function getPagingValueMap($cursor, array $keys) { + protected function getPagingValueMap($cursor, array $keys) { $plan = $this->loadCursorObject($cursor); return array( 'id' => $plan->getID(), diff --git a/src/applications/macro/query/PhabricatorMacroQuery.php b/src/applications/macro/query/PhabricatorMacroQuery.php --- a/src/applications/macro/query/PhabricatorMacroQuery.php +++ b/src/applications/macro/query/PhabricatorMacroQuery.php @@ -258,7 +258,7 @@ ); } - public function getPagingValueMap($cursor, array $keys) { + protected function getPagingValueMap($cursor, array $keys) { $macro = $this->loadCursorObject($cursor); return array( 'id' => $macro->getID(), diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -338,7 +338,7 @@ ); } - public function getPagingValueMap($cursor, array $keys) { + protected function getPagingValueMap($cursor, array $keys) { $user = $this->loadCursorObject($cursor); return array( 'id' => $user->getID(), diff --git a/src/applications/people/typeahead/PhabricatorViewerDatasource.php b/src/applications/people/typeahead/PhabricatorViewerDatasource.php new file mode 100644 --- /dev/null +++ b/src/applications/people/typeahead/PhabricatorViewerDatasource.php @@ -0,0 +1,56 @@ +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()') + ->setUnique(true); + } + +} diff --git a/src/applications/project/typeahead/PhabricatorProjectMembersDatasource.php b/src/applications/project/typeahead/PhabricatorProjectMembersDatasource.php new file mode 100644 --- /dev/null +++ b/src/applications/project/typeahead/PhabricatorProjectMembersDatasource.php @@ -0,0 +1,122 @@ +)...'); + } + + public function getDatasourceApplicationClass() { + return 'PhabricatorProjectApplication'; + } + + public function loadResults() { + $viewer = $this->getViewer(); + $raw_query = $this->getRawQuery(); + + $pattern = $raw_query; + if (self::isFunctionToken($raw_query)) { + $function = $this->parseFunction($raw_query, $allow_partial = true); + if ($function) { + $pattern = head($function['argv']); + } + } + + // Allow users to type "#qa" or "qa" to find "Quality Assurance". + $pattern = ltrim($pattern, '#'); + $tokens = self::tokenizeString($pattern); + + $query = $this->newQuery(); + if ($tokens) { + $query->withNameTokens($tokens); + } + $projects = $this->executeQuery($query); + + $results = array(); + foreach ($projects as $project) { + $results[] = $this->buildProjectResult($project); + } + + return $results; + } + + protected function canEvaluateFunction($function) { + return ($function == 'members'); + } + + 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); + } + + $projects = $this->newQuery() + ->withPHIDs($phids) + ->execute(); + $projects = mpull($projects, null, 'getPHID'); + + $tokens = array(); + foreach ($phids as $phid) { + $project = idx($projects, $phid); + if ($project) { + $result = $this->buildProjectResult($project); + $tokens[] = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( + $result); + } else { + $tokens[] = $this->newInvalidToken(pht('Members: Invalid Project')); + } + } + + return $tokens; + } + + private function newQuery() { + return id(new PhabricatorProjectQuery()) + ->setViewer($this->getViewer()) + ->needImages(true) + ->needSlugs(true); + } + + private function buildProjectResult(PhabricatorProject $project) { + $closed = null; + if ($project->isArchived()) { + $closed = pht('Archived'); + } + + $all_strings = mpull($project->getSlugs(), 'getSlug'); + $all_strings[] = 'members'; + $all_strings[] = $project->getName(); + $all_strings = implode(' ', $all_strings); + + return $this->newFunctionResult() + ->setName($all_strings) + ->setDisplayName(pht('Members: %s', $project->getName())) + ->setURI('/tag/'.$project->getPrimarySlug().'/') + ->setPHID('members('.$project->getPHID().')') + ->setClosed($closed); + } + +} diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -165,9 +165,6 @@ $errors = $engine->getErrors(); if ($errors) { $run_query = false; - $errors = id(new PHUIInfoView()) - ->setTitle(pht('Query Errors')) - ->setErrors($errors); } $submit = id(new AphrontFormSubmitControl()) @@ -214,46 +211,58 @@ $anchor = id(new PhabricatorAnchorView()) ->setAnchorName('R')); - $query = $engine->buildQueryFromSavedQuery($saved_query); + try { + $query = $engine->buildQueryFromSavedQuery($saved_query); - $pager = $engine->newPagerForSavedQuery($saved_query); - $pager->readFromRequest($request); + $pager = $engine->newPagerForSavedQuery($saved_query); + $pager->readFromRequest($request); - $objects = $engine->executeQuery($query, $pager); + $objects = $engine->executeQuery($query, $pager); - // TODO: To support Dashboard panels, rendering is moving into - // SearchEngines. Move it all the way in and then get rid of this. + // TODO: To support Dashboard panels, rendering is moving into + // SearchEngines. Move it all the way in and then get rid of this. - $interface = 'PhabricatorApplicationSearchResultsControllerInterface'; - if ($parent instanceof $interface) { - $list = $parent->renderResultsList($objects, $saved_query); - } else { - $engine->setRequest($request); + $interface = 'PhabricatorApplicationSearchResultsControllerInterface'; + if ($parent instanceof $interface) { + $list = $parent->renderResultsList($objects, $saved_query); + } else { + $engine->setRequest($request); - $list = $engine->renderResults( - $objects, - $saved_query); - } + $list = $engine->renderResults( + $objects, + $saved_query); + } - $nav->appendChild($list); + $nav->appendChild($list); - // TODO: This is a bit hacky. - if ($list instanceof PHUIObjectItemListView) { - $list->setNoDataString(pht('No results found for this query.')); - $list->setPager($pager); - } else { - if ($pager->willShowPagingControls()) { - $pager_box = id(new PHUIBoxView()) - ->addPadding(PHUI::PADDING_MEDIUM) - ->addMargin(PHUI::MARGIN_LARGE) - ->setBorder(true) - ->appendChild($pager); - $nav->appendChild($pager_box); + // TODO: This is a bit hacky. + if ($list instanceof PHUIObjectItemListView) { + $list->setNoDataString(pht('No results found for this query.')); + $list->setPager($pager); + } else { + if ($pager->willShowPagingControls()) { + $pager_box = id(new PHUIBoxView()) + ->addPadding(PHUI::PADDING_MEDIUM) + ->addMargin(PHUI::MARGIN_LARGE) + ->setBorder(true) + ->appendChild($pager); + $nav->appendChild($pager_box); + } } + } catch (PhabricatorTypeaheadInvalidTokenException $ex) { + $errors[] = pht( + 'This query specifies an invalid parameter. Review the '. + 'query parameters and correct errors.'); } } if ($errors) { + $errors = id(new PHUIInfoView()) + ->setTitle(pht('Query Errors')) + ->setErrors($errors); + } + + if ($errors) { $nav->appendChild($errors); } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -383,7 +383,13 @@ } else if (isset($allow_types[$type])) { $phids[] = $item; } else { - $names[] = $item; + if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) { + // If this is a function, pass it through unchanged; we'll evaluate + // it later. + $phids[] = $item; + } else { + $names[] = $item; + } } } diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -49,14 +49,13 @@ ->setRawQuery($raw_query); $hard_limit = 1000; + $limit = 100; if ($is_browse) { if (!$composite->isBrowsable()) { return new Aphront404Response(); } - $limit = 10; - if (($offset + $limit) >= $hard_limit) { // Offset-based paging is intrinsically slow; hard-cap how far we're // willing to go with it. @@ -140,7 +139,7 @@ $items = array(); foreach ($results as $result) { - $token = PhabricatorTypeaheadTokenView::newForTypeaheadResult( + $token = PhabricatorTypeaheadTokenView::newFromTypeaheadResult( $result); // Disable already-selected tokens. diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php @@ -72,6 +72,7 @@ } } + $source->setViewer($this->getViewer()); $usable[] = $source; } $this->usable = $usable; @@ -80,4 +81,37 @@ return $this->usable; } + + protected function canEvaluateFunction($function) { + foreach ($this->getUsableDatasources() as $source) { + if ($source->canEvaluateFunction($function)) { + return true; + } + } + + return parent::canEvaluateFunction($function); + } + + + protected function evaluateFunction($function, array $argv) { + foreach ($this->getUsableDatasources() as $source) { + if ($source->canEvaluateFunction($function)) { + return $source->evaluateFunction($function, $argv); + } + } + + return parent::evaluateFunction($function, $argv); + } + + public function renderFunctionTokens($function, array $argv_list) { + foreach ($this->getUsableDatasources() as $source) { + if ($source->canEvaluateFunction($function)) { + return $source->renderFunctionTokens($function, $argv_list); + } + } + + return parent::renderFunctionTokens($function, $argv_list); + } + + } diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php @@ -1,5 +1,8 @@ setIcon('fa-magic indigo'); + } + + public function newInvalidToken($name) { + return id(new PhabricatorTypeaheadTokenView()) + ->setKey(PhabricatorTypeaheadTokenView::KEY_INVALID) + ->setValue($name) + ->setIcon('fa-exclamation-circle red'); + } + +/* -( Token Functions )---------------------------------------------------- */ + + + /** + * @task functions + */ + protected function canEvaluateFunction($function) { + return false; + } + + + /** + * @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; + } + } + + 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(); + } + + } diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadUserParameterizedDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadUserParameterizedDatasource.php new file mode 100644 --- /dev/null +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadUserParameterizedDatasource.php @@ -0,0 +1,20 @@ +icon = $icon; @@ -85,8 +86,21 @@ return $this->phid; } + public function setUnique($unique) { + $this->unique = $unique; + return $this; + } + public function getSortKey() { - return phutil_utf8_strtolower($this->getName()); + // Put unique results (special parameter functions) ahead of other + // results. + if ($this->unique) { + $prefix = 'A'; + } else { + $prefix = 'B'; + } + + return $prefix.phutil_utf8_strtolower($this->getName()); } public function getWireFormat() { @@ -102,6 +116,7 @@ $this->getIcon(), $this->closed, $this->imageSprite ? (string)$this->imageSprite : null, + $this->unique ? 1 : null, ); while (end($data) === null) { array_pop($data); diff --git a/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php b/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php --- a/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php +++ b/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php @@ -8,7 +8,9 @@ private $inputName; private $value; - public static function newForTypeaheadResult( + const KEY_INVALID = ''; + + public static function newFromTypeaheadResult( PhabricatorTypeaheadResult $result) { return id(new PhabricatorTypeaheadTokenView()) @@ -17,6 +19,15 @@ ->setValue($result->getDisplayName()); } + public static function newFromHandle( + PhabricatorObjectHandle $handle) { + + return id(new PhabricatorTypeaheadTokenView()) + ->setKey($handle->getPHID()) + ->setValue($handle->getFullName()) + ->setIcon($handle->getIcon()); + } + public function setKey($key) { $this->key = $key; return $this; diff --git a/src/view/control/AphrontTokenizerTemplateView.php b/src/view/control/AphrontTokenizerTemplateView.php --- a/src/view/control/AphrontTokenizerTemplateView.php +++ b/src/view/control/AphrontTokenizerTemplateView.php @@ -18,7 +18,7 @@ } public function setValue(array $value) { - assert_instances_of($value, 'PhabricatorObjectHandle'); + assert_instances_of($value, 'PhabricatorTypeaheadTokenView'); $this->value = $value; return $this; } @@ -41,15 +41,7 @@ $id = $this->id; $name = $this->getName(); - $values = nonempty($this->getValue(), array()); - - $tokens = array(); - foreach ($values as $key => $value) { - $tokens[] = $this->renderToken( - $value->getPHID(), - $value->getFullName(), - $value->getType()); - } + $tokens = nonempty($this->getValue(), array()); $input = javelin_tag( 'input', @@ -125,12 +117,4 @@ return $frame; } - private function renderToken($key, $value, $icon) { - return id(new PhabricatorTypeaheadTokenView()) - ->setKey($key) - ->setValue($value) - ->setIcon($icon) - ->setInputName($this->getName()); - } - } diff --git a/src/view/form/control/AphrontFormTokenizerControl.php b/src/view/form/control/AphrontFormTokenizerControl.php --- a/src/view/form/control/AphrontFormTokenizerControl.php +++ b/src/view/form/control/AphrontFormTokenizerControl.php @@ -50,19 +50,52 @@ $id = celerity_generate_unique_node_id(); } + $datasource = $this->datasource; + $datasource->setViewer($this->getUser()); + $placeholder = null; if (!strlen($this->placeholder)) { - if ($this->datasource) { - $placeholder = $this->datasource->getPlaceholderText(); + if ($datasource) { + $placeholder = $datasource->getPlaceholderText(); } } else { $placeholder = $this->placeholder; } + $tokens = array(); + $values = nonempty($this->getValue(), array()); + foreach ($values as $value) { + if (isset($handles[$value])) { + $token = PhabricatorTypeaheadTokenView::newFromHandle($handles[$value]); + } else { + $token = null; + if ($datasource) { + $function = $datasource->parseFunction($value); + if ($function) { + $token_list = $datasource->renderFunctionTokens( + $function['name'], + array($function['argv'])); + $token = head($token_list); + } + } + + if (!$token) { + $name = pht('Invalid Function: %s', $value); + $token = $datasource->newInvalidToken($name); + } + + if ($token->getKey() == PhabricatorTypeaheadTokenView::KEY_INVALID) { + $token->setKey($value); + } + } + $token->setInputName($this->getName()); + $tokens[] = $token; + } + $template = new AphrontTokenizerTemplateView(); $template->setName($name); $template->setID($id); - $template->setValue($handles); + $template->setValue($tokens); $username = null; if ($this->user) { @@ -71,8 +104,6 @@ $datasource_uri = null; $browse_uri = null; - - $datasource = $this->datasource; if ($datasource) { $datasource->setViewer($this->getUser()); @@ -88,8 +119,8 @@ Javelin::initBehavior('aphront-basic-tokenizer', array( 'id' => $id, 'src' => $datasource_uri, - 'value' => mpull($handles, 'getFullName', 'getPHID'), - 'icons' => mpull($handles, 'getIcon', 'getPHID'), + 'value' => mpull($tokens, 'getValue', 'getKey'), + 'icons' => mpull($tokens, 'getIcon', 'getKey'), 'limit' => $this->limit, 'username' => $username, 'placeholder' => $placeholder, @@ -111,7 +142,15 @@ } $values = nonempty($this->getValue(), array()); - $this->handles = $viewer->loadHandles($values); + + $phids = array(); + foreach ($values as $value) { + if (!PhabricatorTypeaheadDatasource::isFunctionToken($value)) { + $phids[] = $value; + } + } + + $this->handles = $viewer->loadHandles($phids); } return $this->handles; diff --git a/webroot/rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js b/webroot/rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js --- a/webroot/rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js +++ b/webroot/rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js @@ -14,10 +14,14 @@ * @return string Normalized string. */ normalize : function(str) { + + // NOTE: We specifically normalize "(" and ")" into spaces so that + // we can match tokenizer functions like "members(project)". + return ('' + str) .toLocaleLowerCase() - .replace(/[\.,\/#!$%\^&\*;:{}=_`~()]/g, '') - .replace(/[-\[\]]/g, ' ') + .replace(/[\.,\/#!$%\^&\*;:{}=_`~]/g, '') + .replace(/[-\[\]\(\)]/g, ' ') .replace(/ +/g, ' ') .replace(/^\s*|\s*$/g, ''); } diff --git a/webroot/rsrc/js/application/typeahead/behavior-typeahead-search.js b/webroot/rsrc/js/application/typeahead/behavior-typeahead-search.js --- a/webroot/rsrc/js/application/typeahead/behavior-typeahead-search.js +++ b/webroot/rsrc/js/application/typeahead/behavior-typeahead-search.js @@ -10,6 +10,7 @@ var input = JX.$(config.inputID); var frame = JX.$(config.frameID); var last = input.value; + var in_flight = {}; function update() { if (input.value == last) { @@ -30,9 +31,17 @@ return; } + if (value in in_flight) { + // We've already sent a request for this query. + return; + } + in_flight[value] = true; + JX.DOM.alterClass(frame, 'loading', true); new JX.Workflow(config.uri, {q: value, format: 'html'}) .setHandler(function(r) { + delete in_flight[value]; + if (value != input.value) { // The user typed some more stuff while the request was in flight, // so ignore the response. diff --git a/webroot/rsrc/js/core/Prefab.js b/webroot/rsrc/js/core/Prefab.js --- a/webroot/rsrc/js/core/Prefab.js +++ b/webroot/rsrc/js/core/Prefab.js @@ -287,7 +287,8 @@ icon: icon, closed: closed, type: fields[5], - sprite: fields[10] + sprite: fields[10], + unique: fields[11] || false }; },