Page MenuHomePhabricator

D12444.id29861.diff
No OneTemporary

D12444.id29861.diff

diff --git a/resources/celerity/map.php b/resources/celerity/map.php
--- a/resources/celerity/map.php
+++ b/resources/celerity/map.php
@@ -8,7 +8,7 @@
return array(
'names' => array(
'core.pkg.css' => '743edc44',
- 'core.pkg.js' => '75d29e8c',
+ 'core.pkg.js' => '42315bf4',
'darkconsole.pkg.js' => '8ab24e01',
'differential.pkg.css' => '3500921f',
'differential.pkg.js' => 'c0506961',
@@ -224,7 +224,7 @@
'rsrc/externals/javelin/lib/behavior.js' => '61cbc29a',
'rsrc/externals/javelin/lib/control/tokenizer/Tokenizer.js' => '0fd17937',
'rsrc/externals/javelin/lib/control/typeahead/Typeahead.js' => '70baed2f',
- 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => '6f7a9da8',
+ 'rsrc/externals/javelin/lib/control/typeahead/normalizer/TypeaheadNormalizer.js' => 'e6e25838',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadCompositeSource.js' => '503e17fd',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadOnDemandSource.js' => '8b3fd187',
'rsrc/externals/javelin/lib/control/typeahead/source/TypeaheadPreloadedSource.js' => '54f314a0',
@@ -427,7 +427,7 @@
'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '9f7309fb',
'rsrc/js/application/transactions/behavior-transaction-list.js' => '13c739ea',
'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '635de1ec',
- 'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '7840bcf8',
+ 'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '93d0c9e3',
'rsrc/js/application/uiexample/JavelinViewExample.js' => 'd4a14807',
'rsrc/js/application/uiexample/ReactorButtonExample.js' => 'd19198c8',
'rsrc/js/application/uiexample/ReactorCheckboxExample.js' => '519705ea',
@@ -450,7 +450,7 @@
'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f',
'rsrc/js/core/MultirowRowManager.js' => 'b5d57730',
'rsrc/js/core/Notification.js' => '0c6946e7',
- 'rsrc/js/core/Prefab.js' => '7df3b8e9',
+ 'rsrc/js/core/Prefab.js' => 'd339753f',
'rsrc/js/core/ShapedRequest.js' => '7cbe244b',
'rsrc/js/core/TextAreaUtils.js' => '5c93c52c',
'rsrc/js/core/Title.js' => 'df5e11d2',
@@ -661,7 +661,7 @@
'javelin-behavior-test-payment-form' => 'fc91ab6c',
'javelin-behavior-toggle-class' => 'e566f52c',
'javelin-behavior-typeahead-browse' => '635de1ec',
- 'javelin-behavior-typeahead-search' => '7840bcf8',
+ 'javelin-behavior-typeahead-search' => '93d0c9e3',
'javelin-behavior-view-placeholder' => '47830651',
'javelin-behavior-workflow' => '0a3f3021',
'javelin-color' => '7e41274a',
@@ -692,7 +692,7 @@
'javelin-tokenizer' => '0fd17937',
'javelin-typeahead' => '70baed2f',
'javelin-typeahead-composite-source' => '503e17fd',
- 'javelin-typeahead-normalizer' => '6f7a9da8',
+ 'javelin-typeahead-normalizer' => 'e6e25838',
'javelin-typeahead-ondemand-source' => '8b3fd187',
'javelin-typeahead-preloaded-source' => '54f314a0',
'javelin-typeahead-source' => '2818f5ce',
@@ -744,7 +744,7 @@
'phabricator-notification-menu-css' => '3c9d8aa1',
'phabricator-object-selector-css' => '029a133d',
'phabricator-phtize' => 'd254d646',
- 'phabricator-prefab' => '7df3b8e9',
+ 'phabricator-prefab' => 'd339753f',
'phabricator-profile-css' => '1a20dcbf',
'phabricator-remarkup-css' => 'bc65f3cc',
'phabricator-search-results-css' => '15c71110',
@@ -1339,9 +1339,6 @@
'javelin-vector',
'javelin-stratcom',
),
- '6f7a9da8' => array(
- 'javelin-install',
- ),
'70baed2f' => array(
'javelin-install',
'javelin-dom',
@@ -1387,12 +1384,6 @@
'javelin-util',
'phabricator-busy',
),
- '7840bcf8' => array(
- 'javelin-behavior',
- 'javelin-stratcom',
- 'javelin-workflow',
- 'javelin-dom',
- ),
'7927a7d3' => array(
'javelin-behavior',
'javelin-quicksand',
@@ -1413,18 +1404,6 @@
'javelin-request',
'javelin-router',
),
- '7df3b8e9' => array(
- 'javelin-install',
- 'javelin-util',
- 'javelin-dom',
- 'javelin-typeahead',
- 'javelin-tokenizer',
- 'javelin-typeahead-preloaded-source',
- 'javelin-typeahead-ondemand-source',
- 'javelin-dom',
- 'javelin-stratcom',
- 'javelin-util',
- ),
'7e41274a' => array(
'javelin-install',
),
@@ -1582,6 +1561,12 @@
'phuix-action-view',
'conpherence-thread-manager',
),
+ '93d0c9e3' => array(
+ 'javelin-behavior',
+ 'javelin-stratcom',
+ 'javelin-workflow',
+ 'javelin-dom',
+ ),
'9414ff18' => array(
'javelin-behavior',
'javelin-resource',
@@ -1808,6 +1793,18 @@
'd254d646' => array(
'javelin-util',
),
+ 'd339753f' => array(
+ 'javelin-install',
+ 'javelin-util',
+ 'javelin-dom',
+ 'javelin-typeahead',
+ 'javelin-tokenizer',
+ 'javelin-typeahead-preloaded-source',
+ 'javelin-typeahead-ondemand-source',
+ 'javelin-dom',
+ 'javelin-stratcom',
+ 'javelin-util',
+ ),
'd4505101' => array(
'javelin-stratcom',
'javelin-install',
@@ -1922,6 +1919,9 @@
'javelin-behavior-device',
'phabricator-keyboard-shortcut',
),
+ 'e6e25838' => array(
+ 'javelin-install',
+ ),
'e723c323' => array(
'javelin-behavior',
'javelin-stratcom',
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 (strpos($item, '(') !== false) {
+ // If this looks like a tokenizer function, assume we'll be able
+ // to resolve 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,113 @@
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) {
+ 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

Mime Type
text/plain
Expires
Mon, May 13, 3:49 AM (1 w, 3 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/la/ya/kdlbosmb3cop3dz7
Default Alt Text
D12444.id29861.diff (38 KB)

Event Timeline