Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F18090545
D12444.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
33 KB
Referenced Files
None
Subscribers
None
D12444.diff
View Options
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 @@
+<?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);
+ }
+
+}
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 @@
+<?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);
+ }
+
+}
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 @@
<?php
+/**
+ * @task functions Token Functions
+ */
abstract class PhabricatorTypeaheadDatasource extends Phobject {
private $viewer;
@@ -183,4 +186,116 @@
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();
+ }
+
+
}
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 @@
+<?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;
+ }
+
+}
diff --git a/src/applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php b/src/applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php
new file mode 100644
--- /dev/null
+++ b/src/applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php
@@ -0,0 +1,3 @@
+<?php
+
+final class PhabricatorTypeaheadInvalidTokenException extends Exception {}
diff --git a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
--- a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
+++ b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php
@@ -13,6 +13,7 @@
private $imageSprite;
private $icon;
private $closed;
+ private $unique;
public function setIcon($icon) {
$this->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 = '<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
};
},
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Thu, Aug 7, 4:11 PM (1 w, 4 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/ia/w7/4g7rsmrus525thzi
Default Alt Text
D12444.diff (33 KB)
Attached To
Mode
D12444: Implement viewer() and members(project) typeahead functions
Attached
Detach File
Event Timeline
Log In to Comment