1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

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
This commit is contained in:
epriestley 2015-04-16 15:30:41 -07:00
parent a4261f41c2
commit 845466b49b
21 changed files with 512 additions and 73 deletions

View file

@ -2295,6 +2295,7 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'PhabricatorProjectIcon' => 'Phobject',
'PhabricatorProjectListController' => 'PhabricatorProjectController',
'PhabricatorProjectMemberOfProjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorProjectMembersDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController',
'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController',
'PhabricatorProjectMoveController' => 'PhabricatorProjectController',
@ -6026,12 +6031,14 @@ phutil_register_library_map(array(
'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 @@ phutil_register_library_map(array(
'PhabricatorUsersPolicyRule' => 'PhabricatorPolicyRule',
'PhabricatorVCSResponse' => 'AphrontResponse',
'PhabricatorVeryWowEnglishTranslation' => 'PhutilTranslation',
'PhabricatorViewerDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorWatcherHasObjectEdgeType' => 'PhabricatorEdgeType',
'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask',

View file

@ -67,7 +67,11 @@ final class DifferentialRevisionSearchEngine
->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 @@ final class DifferentialRevisionSearchEngine
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 @@ final class DifferentialRevisionSearchEngine
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

View file

@ -93,7 +93,7 @@ final class HarbormasterBuildPlanQuery
);
}
public function getPagingValueMap($cursor, array $keys) {
protected function getPagingValueMap($cursor, array $keys) {
$plan = $this->loadCursorObject($cursor);
return array(
'id' => $plan->getID(),

View file

@ -258,7 +258,7 @@ final class PhabricatorMacroQuery
);
}
public function getPagingValueMap($cursor, array $keys) {
protected function getPagingValueMap($cursor, array $keys) {
$macro = $this->loadCursorObject($cursor);
return array(
'id' => $macro->getID(),

View file

@ -338,7 +338,7 @@ final class PhabricatorPeopleQuery
);
}
public function getPagingValueMap($cursor, array $keys) {
protected function getPagingValueMap($cursor, array $keys) {
$user = $this->loadCursorObject($cursor);
return array(
'id' => $user->getID(),

View file

@ -0,0 +1,56 @@
<?php
final class PhabricatorViewerDatasource
extends PhabricatorTypeaheadDatasource {
public function getPlaceholderText() {
return pht('Type viewer()...');
}
public function getDatasourceApplicationClass() {
return 'PhabricatorPeopleApplication';
}
public function loadResults() {
if ($this->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);
}
}

View file

@ -0,0 +1,122 @@
<?php
final class PhabricatorProjectMembersDatasource
extends PhabricatorTypeaheadDatasource {
public function getPlaceholderText() {
return pht('Type members(<project>)...');
}
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);
}
}

View file

@ -165,9 +165,6 @@ final class PhabricatorApplicationSearchController
$errors = $engine->getErrors();
if ($errors) {
$run_query = false;
$errors = id(new PHUIInfoView())
->setTitle(pht('Query Errors'))
->setErrors($errors);
}
$submit = id(new AphrontFormSubmitControl())
@ -214,45 +211,57 @@ final class PhabricatorApplicationSearchController
$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);
}
$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);
$list = $engine->renderResults(
$objects,
$saved_query);
}
$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);
}
}
} 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);
}

View file

@ -383,7 +383,13 @@ abstract class PhabricatorApplicationSearchEngine {
} 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;
}
}
}

View file

@ -49,14 +49,13 @@ final class PhabricatorTypeaheadModularDatasourceController
->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 @@ final class PhabricatorTypeaheadModularDatasourceController
$items = array();
foreach ($results as $result) {
$token = PhabricatorTypeaheadTokenView::newForTypeaheadResult(
$token = PhabricatorTypeaheadTokenView::newFromTypeaheadResult(
$result);
// Disable already-selected tokens.

View file

@ -72,6 +72,7 @@ abstract class PhabricatorTypeaheadCompositeDatasource
}
}
$source->setViewer($this->getViewer());
$usable[] = $source;
}
$this->usable = $usable;
@ -80,4 +81,37 @@ abstract class PhabricatorTypeaheadCompositeDatasource
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);
}
}

View file

@ -1,5 +1,8 @@
<?php
/**
* @task functions Token Functions
*/
abstract class PhabricatorTypeaheadDatasource extends Phobject {
private $viewer;
@ -183,4 +186,116 @@ abstract class PhabricatorTypeaheadDatasource extends Phobject {
return $results;
}
protected function newFunctionResult() {
// TODO: Find a more consistent design.
return id(new PhabricatorTypeaheadResult())
->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();
}
}

View file

@ -0,0 +1,20 @@
<?php
final class PhabricatorTypeaheadUserParameterizedDatasource
extends PhabricatorTypeaheadCompositeDatasource {
public function getPlaceholderText() {
return pht('Type a username or selector...');
}
public function getComponentDatasources() {
$sources = array(
new PhabricatorViewerDatasource(),
new PhabricatorPeopleDatasource(),
new PhabricatorProjectMembersDatasource(),
);
return $sources;
}
}

View file

@ -0,0 +1,3 @@
<?php
final class PhabricatorTypeaheadInvalidTokenException extends Exception {}

View file

@ -13,6 +13,7 @@ final class PhabricatorTypeaheadResult {
private $imageSprite;
private $icon;
private $closed;
private $unique;
public function setIcon($icon) {
$this->icon = $icon;
@ -85,8 +86,21 @@ final class PhabricatorTypeaheadResult {
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 @@ final class PhabricatorTypeaheadResult {
$this->getIcon(),
$this->closed,
$this->imageSprite ? (string)$this->imageSprite : null,
$this->unique ? 1 : null,
);
while (end($data) === null) {
array_pop($data);

View file

@ -8,7 +8,9 @@ final class PhabricatorTypeaheadTokenView
private $inputName;
private $value;
public static function newForTypeaheadResult(
const KEY_INVALID = '<invalid>';
public static function newFromTypeaheadResult(
PhabricatorTypeaheadResult $result) {
return id(new PhabricatorTypeaheadTokenView())
@ -17,6 +19,15 @@ final class PhabricatorTypeaheadTokenView
->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;

View file

@ -18,7 +18,7 @@ final class AphrontTokenizerTemplateView extends AphrontView {
}
public function setValue(array $value) {
assert_instances_of($value, 'PhabricatorObjectHandle');
assert_instances_of($value, 'PhabricatorTypeaheadTokenView');
$this->value = $value;
return $this;
}
@ -41,15 +41,7 @@ final class AphrontTokenizerTemplateView extends AphrontView {
$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 @@ final class AphrontTokenizerTemplateView extends AphrontView {
return $frame;
}
private function renderToken($key, $value, $icon) {
return id(new PhabricatorTypeaheadTokenView())
->setKey($key)
->setValue($value)
->setIcon($icon)
->setInputName($this->getName());
}
}

View file

@ -50,19 +50,52 @@ final class AphrontFormTokenizerControl extends AphrontFormControl {
$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 @@ final class AphrontFormTokenizerControl extends AphrontFormControl {
$datasource_uri = null;
$browse_uri = null;
$datasource = $this->datasource;
if ($datasource) {
$datasource->setViewer($this->getUser());
@ -88,8 +119,8 @@ final class AphrontFormTokenizerControl extends AphrontFormControl {
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 @@ final class AphrontFormTokenizerControl extends AphrontFormControl {
}
$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;

View file

@ -14,10 +14,14 @@ JX.install('TypeaheadNormalizer', {
* @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, '');
}

View file

@ -10,6 +10,7 @@ JX.behavior('typeahead-search', function(config) {
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 @@ JX.behavior('typeahead-search', function(config) {
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.

View file

@ -287,7 +287,8 @@ JX.install('Prefab', {
icon: icon,
closed: closed,
type: fields[5],
sprite: fields[10]
sprite: fields[10],
unique: fields[11] || false
};
},