mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-22 23:02:42 +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:
parent
a4261f41c2
commit
845466b49b
21 changed files with 512 additions and 73 deletions
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorTypeaheadInvalidTokenException extends Exception {}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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, '');
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -287,7 +287,8 @@ JX.install('Prefab', {
|
|||
icon: icon,
|
||||
closed: closed,
|
||||
type: fields[5],
|
||||
sprite: fields[10]
|
||||
sprite: fields[10],
|
||||
unique: fields[11] || false
|
||||
};
|
||||
},
|
||||
|
||||
|
|
Loading…
Reference in a new issue