1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-20 20:40:56 +01:00

Use ApplicationSearch to power primary search

Summary:
Ref T4365. Drive primary search through ApplicationSearch instead of through a bunch of custom nonsense. Notably, this allows you to save searches, notably.

The one thing this doesn't do -- which I'd like it to -- is carry your query text across searches. When you search for "quack", I want to overwrite the query in your default filter and give you those results, so you can turn the search into an "Open Tasks" search by default by reordering the queries. I'll probably do that next. It feels a little hacky but I want to try it out.

Test Plan: {F106932}

Reviewers: btrahan, chad

Reviewed By: btrahan

CC: aran, bigo

Maniphest Tasks: T4365

Differential Revision: https://secure.phabricator.com/D8123
This commit is contained in:
epriestley 2014-02-03 12:52:47 -08:00
parent a82f573102
commit b262ccdaff
9 changed files with 410 additions and 350 deletions

View file

@ -4692,7 +4692,11 @@ phutil_register_library_map(array(
'PhabricatorSearchAttachController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchAttachController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchBaseController' => 'PhabricatorController', 'PhabricatorSearchBaseController' => 'PhabricatorController',
'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions',
'PhabricatorSearchController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchController' =>
array(
0 => 'PhabricatorSearchBaseController',
1 => 'PhabricatorApplicationSearchResultsControllerInterface',
),
'PhabricatorSearchDAO' => 'PhabricatorLiskDAO', 'PhabricatorSearchDAO' => 'PhabricatorLiskDAO',
'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchDeleteController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchDocument' => 'PhabricatorSearchDAO', 'PhabricatorSearchDocument' => 'PhabricatorSearchDAO',

View file

@ -29,8 +29,7 @@ final class PhabricatorApplicationSearch extends PhabricatorApplication {
public function getRoutes() { public function getRoutes() {
return array( return array(
'/search/' => array( '/search/' => array(
'' => 'PhabricatorSearchController', '(?:query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorSearchController',
'(?P<key>[^/]+)/' => 'PhabricatorSearchController',
'attach/(?P<phid>[^/]+)/(?P<type>\w+)/(?:(?P<action>\w+)/)?' 'attach/(?P<phid>[^/]+)/(?P<type>\w+)/(?:(?P<action>\w+)/)?'
=> 'PhabricatorSearchAttachController', => 'PhabricatorSearchAttachController',
'select/(?P<type>\w+)/' 'select/(?P<type>\w+)/'

View file

@ -29,6 +29,7 @@ final class PhabricatorSearchConfigOptions
"your documents in some search engine which does not have ". "your documents in some search engine which does not have ".
"default support.")), "default support.")),
$this->newOption('search.elastic.host', 'string', null) $this->newOption('search.elastic.host', 'string', null)
->setLocked(true)
->setDescription(pht("Elastic Search host.")) ->setDescription(pht("Elastic Search host."))
->addExample('http://elastic.example.com:9200/', pht('Valid Setting')), ->addExample('http://elastic.example.com:9200/', pht('Valid Setting')),
); );

View file

@ -1,266 +1,83 @@
<?php <?php
/**
* @group search
*/
final class PhabricatorSearchController final class PhabricatorSearchController
extends PhabricatorSearchBaseController { extends PhabricatorSearchBaseController
implements PhabricatorApplicationSearchResultsControllerInterface {
private $key; private $queryKey;
public function shouldAllowPublic() { public function shouldAllowPublic() {
return true; return true;
} }
public function willProcessRequest(array $data) { public function willProcessRequest(array $data) {
$this->key = idx($data, 'key'); $this->queryKey = idx($data, 'queryKey');
} }
public function processRequest() { public function processRequest() {
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $viewer = $request->getUser();
if ($this->key) {
$query = id(new PhabricatorSavedQuery())->loadOneWhere(
'queryKey = %s',
$this->key);
if (!$query) {
return new Aphront404Response();
}
} else {
$query = id(new PhabricatorSavedQuery())
->setEngineClassName('PhabricatorSearchApplicationSearchEngine');
if ($request->isFormPost()) {
$query_str = $request->getStr('query');
if ($request->getStr('jump') != 'no') {
$pref_jump = PhabricatorUserPreferences::PREFERENCE_SEARCHBAR_JUMP; $pref_jump = PhabricatorUserPreferences::PREFERENCE_SEARCHBAR_JUMP;
if ($request->getStr('jump') != 'no' && if ($viewer->loadPreferences($pref_jump, 1)) {
$user && $user->loadPreferences()->getPreference($pref_jump, 1)) {
$response = PhabricatorJumpNavHandler::getJumpResponse( $response = PhabricatorJumpNavHandler::getJumpResponse(
$user, $viewer,
$query_str); $request->getStr('query'));
} else {
$response = null;
}
if ($response) { if ($response) {
return $response; return $response;
} else {
$query->setParameter('query', $query_str);
if ($request->getStr('scope')) {
switch ($request->getStr('scope')) {
case PhabricatorSearchScope::SCOPE_OPEN_REVISIONS:
$query->setParameter('open', 1);
$query->setParameter(
'type',
DifferentialPHIDTypeRevision::TYPECONST);
break;
case PhabricatorSearchScope::SCOPE_OPEN_TASKS:
$query->setParameter('open', 1);
$query->setParameter(
'type',
ManiphestPHIDTypeTask::TYPECONST);
break;
case PhabricatorSearchScope::SCOPE_WIKI:
$query->setParameter(
'type',
PhrictionPHIDTypeDocument::TYPECONST);
break;
case PhabricatorSearchScope::SCOPE_COMMITS:
$query->setParameter(
'type',
PhabricatorRepositoryPHIDTypeCommit::TYPECONST);
break;
default:
break;
}
} else {
if (strlen($request->getStr('type'))) {
$query->setParameter('type', $request->getStr('type'));
}
if ($request->getArr('author')) {
$query->setParameter('author', $request->getArr('author'));
}
if ($request->getArr('owner')) {
$query->setParameter('owner', $request->getArr('owner'));
}
if ($request->getArr('subscribers')) {
$query->setParameter('subscribers',
$request->getArr('subscribers'));
}
if ($request->getInt('open')) {
$query->setParameter('open', $request->getInt('open'));
}
if ($request->getArr('project')) {
$query->setParameter('project', $request->getArr('project'));
}
}
try {
$query->save();
} catch (AphrontQueryDuplicateKeyException $ex) {
// Someone has already executed this query.
}
return id(new AphrontRedirectResponse())
->setURI('/search/'.$query->getQueryKey().'/');
} }
} }
} }
$options = array( $controller = id(new PhabricatorApplicationSearchController($request))
'' => 'All Documents', ->setQueryKey($this->queryKey)
) + PhabricatorSearchAbstractDocument::getSupportedTypes(); ->setSearchEngine(new PhabricatorSearchApplicationSearchEngine())
->setUseOffsetPaging(true)
->setNavigation($this->buildSideNavView());
$status_options = array( return $this->delegateToController($controller);
0 => 'Open and Closed Documents',
1 => 'Open Documents',
);
$phids = array_merge(
$query->getParameter('author', array()),
$query->getParameter('owner', array()),
$query->getParameter('subscribers', array()),
$query->getParameter('project', array()));
$handles = $this->loadViewerHandles($phids);
$author_value = array_select_keys(
$handles,
$query->getParameter('author', array()));
$owner_value = array_select_keys(
$handles,
$query->getParameter('owner', array()));
$subscribers_value = array_select_keys(
$handles,
$query->getParameter('subscribers', array()));
$project_value = array_select_keys(
$handles,
$query->getParameter('project', array()));
$search_form = new AphrontFormView();
$search_form
->setUser($user)
->setAction('/search/')
->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'jump',
'value' => 'no',
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Search')
->setName('query')
->setValue($query->getParameter('query')))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Document Type')
->setName('type')
->setOptions($options)
->setValue($query->getParameter('type')))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Document Status')
->setName('open')
->setOptions($status_options)
->setValue($query->getParameter('open')))
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('author')
->setLabel('Author')
->setDatasource('/typeahead/common/users/')
->setValue($author_value))
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('owner')
->setLabel('Owner')
->setDatasource('/typeahead/common/searchowner/')
->setValue($owner_value)
->setCaption(
'Tip: search for "Up For Grabs" to find unowned documents.'))
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('subscribers')
->setLabel('Subscribers')
->setDatasource('/typeahead/common/users/')
->setValue($subscribers_value))
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('project')
->setLabel('Project')
->setDatasource('/typeahead/common/projects/')
->setValue($project_value))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Search'));
$search_panel = new AphrontListFilterView();
$search_panel->appendChild($search_form);
require_celerity_resource('phabricator-search-results-css');
if ($query->getID()) {
$limit = 20;
$pager = new AphrontPagerView();
$pager->setURI($request->getRequestURI(), 'page');
$pager->setPageSize($limit);
$pager->setOffset($request->getInt('page'));
$query->setParameter('limit', $limit + 1);
$query->setParameter('offset', $pager->getOffset());
$engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
$results = $engine->executeSearch($query);
$results = $pager->sliceResults($results);
// If there are any objects which match the query by name, and we're
// not paging through the results, prefix the results with the named
// objects.
if (!$request->getInt('page')) {
$named = id(new PhabricatorObjectQuery())
->setViewer($user)
->withNames(array($query->getParameter('queyr')))
->execute();
if ($named) {
$results = array_merge(array_keys($named), $results);
} }
public function buildSideNavView($for_app = false) {
$viewer = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
id(new PhabricatorSearchApplicationSearchEngine())
->setViewer($viewer)
->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
} }
public function renderResultsList(
array $results,
PhabricatorSavedQuery $query) {
$viewer = $this->getRequest()->getUser();
if ($results) { if ($results) {
$handles = id(new PhabricatorHandleQuery())
->setViewer($user)
->withPHIDs($results)
->execute();
$objects = id(new PhabricatorObjectQuery()) $objects = id(new PhabricatorObjectQuery())
->setViewer($user) ->setViewer($viewer)
->withPHIDs($results) ->withPHIDs(mpull($results, 'getPHID'))
->execute(); ->execute();
$results = array();
foreach ($handles as $phid => $handle) { $output = array();
foreach ($results as $phid => $handle) {
$view = id(new PhabricatorSearchResultView()) $view = id(new PhabricatorSearchResultView())
->setHandle($handle) ->setHandle($handle)
->setQuery($query) ->setQuery($query)
->setObject(idx($objects, $phid)); ->setObject(idx($objects, $phid));
$results[] = $view->render(); $output[] = $view->render();
} }
$results = phutil_tag_div('phabricator-search-result-list', array( $results = phutil_tag_div(
phutil_implode_html("\n", $results), 'phabricator-search-result-list',
phutil_tag_div('search-results-pager', $pager->render()), $output);
));
} else { } else {
$results = phutil_tag_div( $results = phutil_tag_div(
'phabricator-search-result-list', 'phabricator-search-result-list',
@ -269,30 +86,13 @@ final class PhabricatorSearchController
array('class' => 'phabricator-search-no-results'), array('class' => 'phabricator-search-no-results'),
pht('No search results.'))); pht('No search results.')));
} }
$results = id(new PHUIBoxView())
return id(new PHUIBoxView())
->addMargin(PHUI::MARGIN_LARGE) ->addMargin(PHUI::MARGIN_LARGE)
->addPadding(PHUI::PADDING_LARGE) ->addPadding(PHUI::PADDING_LARGE)
->setShadow(true) ->setShadow(true)
->appendChild($results) ->appendChild($results)
->addClass('phabricator-search-result-box'); ->addClass('phabricator-search-result-box');
} else {
$results = null;
} }
$crumbs = $this->buildApplicationCrumbs();
$crumbs->addTextCrumb(pht('Search'));
return $this->buildApplicationPage(
array(
$crumbs,
$search_panel,
$results,
),
array(
'title' => pht('Search Results'),
'device' => true,
));
}
} }

View file

@ -95,7 +95,7 @@ final class PhabricatorSearchEngineElastic extends PhabricatorSearchEngine {
$spec = array(); $spec = array();
$filter = array(); $filter = array();
if ($query->getParameter('query') != '') { if (strlen($query->getParameter('query'))) {
$spec[] = array( $spec[] = array(
'field' => array( 'field' => array(
'field.corpus' => $query->getParameter('query'), 'field.corpus' => $query->getParameter('query'),
@ -114,17 +114,41 @@ final class PhabricatorSearchEngineElastic extends PhabricatorSearchEngine {
); );
} }
$rel_mapping = array( $relationship_map = array(
'author' => PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR, PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR =>
'open' => PhabricatorSearchRelationship::RELATIONSHIP_OPEN, $query->getParameter('authorPHIDs', array()),
'owner' => PhabricatorSearchRelationship::RELATIONSHIP_OWNER, PhabricatorSearchRelationship::RELATIONSHIP_OWNER =>
'subscribers' => PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER, $query->getParameter('ownerPHIDs', array()),
'project' => PhabricatorSearchRelationship::RELATIONSHIP_PROJECT, PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER =>
'repository' => PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY, $query->getParameter('subscriberPHIDs', array()),
PhabricatorSearchRelationship::RELATIONSHIP_PROJECT =>
$query->getParameter('projectPHIDs', array()),
PhabricatorSearchRelationship::RELATIONSHIP_REPOSITORY =>
$query->getParameter('repositoryPHIDs', array()),
); );
foreach ($rel_mapping as $name => $field) {
$param = $query->getParameter($name); $statuses = $query->getParameter('statuses', array());
if (is_array($param)) { $statuses = array_fuse($statuses);
$rel_open = PhabricatorSearchRelationship::RELATIONSHIP_OPEN;
$rel_closed = PhabricatorSearchRelationship::RELATIONSHIP_CLOSED;
$rel_unowned = PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED;
$include_open = !empty($statuses[$rel_open]);
$include_closed = !empty($statuses[$rel_closed]);
if ($include_open && !$include_closed) {
$relationship_map[$rel_open] = true;
} else if (!$include_open && $include_closed) {
$relationship_map[$rel_closed] = true;
}
if ($query->getParameter('withUnowned')) {
$relationship_map[$rel_unowned] = true;
}
foreach ($relationship_map as $field => $param) {
if (is_array($param) && $param) {
$should = array(); $should = array();
foreach ($param as $val) { foreach ($param as $val) {
$should[] = array( $should[] = array(
@ -177,16 +201,16 @@ final class PhabricatorSearchEngineElastic extends PhabricatorSearchEngine {
} }
public function executeSearch(PhabricatorSavedQuery $query) { public function executeSearch(PhabricatorSavedQuery $query) {
$type = $query->getParameter('type'); $types = $query->getParameter('types');
if ($type) { if (!$types) {
$uri = "/phabricator/{$type}/_search"; $types = array_keys(
} else { PhabricatorSearchApplicationSearchEngine::getIndexableDocumentTypes());
}
// Don't use '/phabricator/_search' for the case that there is something // Don't use '/phabricator/_search' for the case that there is something
// else in the index (for example if 'phabricator' is only an alias to // else in the index (for example if 'phabricator' is only an alias to
// some bigger index). // some bigger index).
$types = PhabricatorSearchAbstractDocument::getSupportedTypes(); $uri = '/phabricator/'.implode(',', $types).'/_search';
$uri = '/phabricator/' . implode(',', array_keys($types)) . '/_search';
}
try { try {
$response = $this->executeRequest($uri, $this->buildSpec($query)); $response = $this->executeRequest($uri, $this->buildSpec($query));
@ -194,7 +218,7 @@ final class PhabricatorSearchEngineElastic extends PhabricatorSearchEngine {
// elasticsearch probably uses Lucene query syntax: // elasticsearch probably uses Lucene query syntax:
// http://lucene.apache.org/core/3_6_1/queryparsersyntax.html // http://lucene.apache.org/core/3_6_1/queryparsersyntax.html
// Try literal search if operator search fails. // Try literal search if operator search fails.
if (!$query->getParameter('query')) { if (!strlen($query->getParameter('query'))) {
throw $ex; throw $ex;
} }
$query = clone $query; $query = clone $query;

View file

@ -179,7 +179,7 @@ final class PhabricatorSearchEngineMySQL extends PhabricatorSearchEngine {
$q); $q);
$field = $query->getParameter('field'); $field = $query->getParameter('field');
if ($field/* && $field != AdjutantQuery::FIELD_ALL*/) { if ($field) {
$where[] = qsprintf( $where[] = qsprintf(
$conn_r, $conn_r,
'field.field = %s', 'field.field = %s',
@ -192,49 +192,74 @@ final class PhabricatorSearchEngineMySQL extends PhabricatorSearchEngine {
$where[] = qsprintf($conn_r, 'document.phid != %s', $exclude); $where[] = qsprintf($conn_r, 'document.phid != %s', $exclude);
} }
if ($query->getParameter('type')) { $types = $query->getParameter('types');
if ($types) {
if (strlen($q)) { if (strlen($q)) {
// TODO: verify that this column actually does something useful in query
// plans once we have nontrivial amounts of data.
$where[] = qsprintf( $where[] = qsprintf(
$conn_r, $conn_r,
'field.phidType = %s', 'field.phidType IN (%Ls)',
$query->getParameter('type')); $types);
} }
$where[] = qsprintf( $where[] = qsprintf(
$conn_r, $conn_r,
'document.documentType = %s', 'document.documentType IN (%Ls)',
$query->getParameter('type')); $types);
} }
$join[] = $this->joinRelationship( $join[] = $this->joinRelationship(
$conn_r, $conn_r,
$query, $query,
'author', 'authorPHIDs',
PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR); PhabricatorSearchRelationship::RELATIONSHIP_AUTHOR);
$join[] = $this->joinRelationship( $statuses = $query->getParameter('statuses', array());
$conn_r, $statuses = array_fuse($statuses);
$query, $open_rel = PhabricatorSearchRelationship::RELATIONSHIP_OPEN;
'open', $closed_rel = PhabricatorSearchRelationship::RELATIONSHIP_CLOSED;
PhabricatorSearchRelationship::RELATIONSHIP_OPEN); $include_open = !empty($statuses[$open_rel]);
$include_closed = !empty($statuses[$closed_rel]);
if ($include_open && !$include_closed) {
$join[] = $this->joinRelationship( $join[] = $this->joinRelationship(
$conn_r, $conn_r,
$query, $query,
'owner', 'statuses',
$open_rel,
true);
} else if ($include_closed && !$include_open) {
$join[] = $this->joinRelationship(
$conn_r,
$query,
'statuses',
$closed_rel,
true);
}
if ($query->getParameter('withUnowned')) {
$join[] = $this->joinRelationship(
$conn_r,
$query,
'withUnowned',
PhabricatorSearchRelationship::RELATIONSHIP_UNOWNED,
true);
} else {
$join[] = $this->joinRelationship(
$conn_r,
$query,
'ownerPHIDs',
PhabricatorSearchRelationship::RELATIONSHIP_OWNER); PhabricatorSearchRelationship::RELATIONSHIP_OWNER);
}
$join[] = $this->joinRelationship( $join[] = $this->joinRelationship(
$conn_r, $conn_r,
$query, $query,
'subscribers', 'subscriberPHIDs',
PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER); PhabricatorSearchRelationship::RELATIONSHIP_SUBSCRIBER);
$join[] = $this->joinRelationship( $join[] = $this->joinRelationship(
$conn_r, $conn_r,
$query, $query,
'project', 'projectPHIDs',
PhabricatorSearchRelationship::RELATIONSHIP_PROJECT); PhabricatorSearchRelationship::RELATIONSHIP_PROJECT);
$join[] = $this->joinRelationship( $join[] = $this->joinRelationship(
@ -283,19 +308,8 @@ final class PhabricatorSearchEngineMySQL extends PhabricatorSearchEngine {
AphrontDatabaseConnection $conn, AphrontDatabaseConnection $conn,
PhabricatorSavedQuery $query, PhabricatorSavedQuery $query,
$field, $field,
$type) { $type,
$is_existence = false) {
$phids = $query->getParameter($field, array());
if (!$phids) {
return null;
}
$is_existence = false;
switch ($type) {
case PhabricatorSearchRelationship::RELATIONSHIP_OPEN:
$is_existence = true;
break;
}
$sql = qsprintf( $sql = qsprintf(
$conn, $conn,
@ -307,6 +321,10 @@ final class PhabricatorSearchEngineMySQL extends PhabricatorSearchEngine {
$type); $type);
if (!$is_existence) { if (!$is_existence) {
$phids = $query->getParameter($field, array());
if (!$phids) {
return null;
}
$sql .= qsprintf( $sql .= qsprintf(
$conn, $conn,
' AND %C.relatedPHID in (%Ls)', ' AND %C.relatedPHID in (%Ls)',

View file

@ -1,8 +1,5 @@
<?php <?php
/**
* @group search
*/
final class PhabricatorSearchAbstractDocument { final class PhabricatorSearchAbstractDocument {
private $phid; private $phid;
@ -13,17 +10,6 @@ final class PhabricatorSearchAbstractDocument {
private $fields = array(); private $fields = array();
private $relationships = array(); private $relationships = array();
public static function getSupportedTypes() {
return array(
DifferentialPHIDTypeRevision::TYPECONST => 'Differential Revisions',
PhabricatorRepositoryPHIDTypeCommit::TYPECONST => 'Repository Commits',
ManiphestPHIDTypeTask::TYPECONST => 'Maniphest Tasks',
PhrictionPHIDTypeDocument::TYPECONST => 'Phriction Documents',
PhabricatorPeoplePHIDTypeUser::TYPECONST => 'Phabricator Users',
PonderPHIDTypeQuestion::TYPECONST => 'Ponder Questions',
);
}
public function setPHID($phid) { public function setPHID($phid) {
$this->phid = $phid; $this->phid = $phid;
return $this; return $this;

View file

@ -6,19 +6,155 @@ final class PhabricatorSearchApplicationSearchEngine
public function buildSavedQueryFromRequest(AphrontRequest $request) { public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery(); $saved = new PhabricatorSavedQuery();
$saved->setParameter('query', $request->getStr('query'));
$saved->setParameter(
'statuses',
$this->readListFromRequest($request, 'statuses'));
$saved->setParameter(
'types',
$this->readListFromRequest($request, 'types'));
$saved->setParameter(
'authorPHIDs',
$this->readUsersFromRequest($request, 'authorPHIDs'));
$saved->setParameter(
'ownerPHIDs',
$this->readUsersFromRequest($request, 'ownerPHIDs'));
$saved->setParameter(
'withUnowned',
$this->readBoolFromRequest($request, 'withUnowned'));
$saved->setParameter(
'subscriberPHIDs',
$this->readPHIDsFromRequest($request, 'subscriberPHIDs'));
$saved->setParameter(
'projectPHIDs',
$this->readPHIDsFromRequest($request, 'projectPHIDs'));
return $saved; return $saved;
} }
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new PhabricatorSearchDocumentQuery()); $query = id(new PhabricatorSearchDocumentQuery())
->withSavedQuery($saved);
return $query; return $query;
} }
public function buildSearchForm( public function buildSearchForm(
AphrontFormView $form, AphrontFormView $form,
PhabricatorSavedQuery $saved_query) { PhabricatorSavedQuery $saved) {
return;
$options = array();
$author_value = null;
$owner_value = null;
$subscribers_value = null;
$project_value = null;
$author_phids = $saved->getParameter('authorPHIDs', array());
$owner_phids = $saved->getParameter('ownerPHIDs', array());
$subscriber_phids = $saved->getParameter('subscriberPHIDs', array());
$project_phids = $saved->getParameter('projectPHIDs', array());
$all_phids = array_merge(
$author_phids,
$owner_phids,
$subscriber_phids,
$project_phids);
$all_handles = id(new PhabricatorHandleQuery())
->setViewer($this->requireViewer())
->withPHIDs($all_phids)
->execute();
$author_handles = array_select_keys($all_handles, $author_phids);
$owner_handles = array_select_keys($all_handles, $owner_phids);
$subscriber_handles = array_select_keys($all_handles, $subscriber_phids);
$project_handles = array_select_keys($all_handles, $project_phids);
$with_unowned = $saved->getParameter('withUnowned', array());
$status_values = $saved->getParameter('statuses', array());
$status_values = array_fuse($status_values);
$statuses = array(
PhabricatorSearchRelationship::RELATIONSHIP_OPEN => pht('Open'),
PhabricatorSearchRelationship::RELATIONSHIP_CLOSED => pht('Closed'),
);
$status_control = id(new AphrontFormCheckboxControl())
->setLabel(pht('Document Status'));
foreach ($statuses as $status => $name) {
$status_control->addCheckbox(
'statuses[]',
$status,
$name,
isset($status_values[$status]));
}
$type_values = $saved->getParameter('types', array());
$type_values = array_fuse($type_values);
$types = self::getIndexableDocumentTypes();
$types_control = id(new AphrontFormCheckboxControl())
->setLabel(pht('Document Types'));
foreach ($types as $type => $name) {
$types_control->addCheckbox(
'types[]',
$type,
$name,
isset($type_values[$type]));
}
$form
->appendChild(
phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => 'jump',
'value' => 'no',
)))
->appendChild(
id(new AphrontFormTextControl())
->setLabel('Query')
->setName('query')
->setValue($saved->getParameter('query')))
->appendChild($status_control)
->appendChild($types_control)
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('authorPHIDs')
->setLabel('Authors')
->setDatasource('/typeahead/common/users/')
->setValue($author_handles))
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('ownerPHIDs')
->setLabel('Owners')
->setDatasource('/typeahead/common/searchowner/')
->setValue($owner_handles))
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'withUnowned',
1,
pht('Show only unowned documents.'),
$with_unowned))
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('subscriberPHIDs')
->setLabel('Subscribers')
->setDatasource('/typeahead/common/users/')
->setValue($subscriber_handles))
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('projectPHIDs')
->setLabel('In Any Project')
->setDatasource('/typeahead/common/projects/')
->setValue($project_handles));
} }
protected function getURI($path) { protected function getURI($path) {
@ -28,6 +164,8 @@ final class PhabricatorSearchApplicationSearchEngine
public function getBuiltinQueryNames() { public function getBuiltinQueryNames() {
$names = array( $names = array(
'all' => pht('All Documents'), 'all' => pht('All Documents'),
'open' => pht('Open Documents'),
'open-tasks' => pht('Open Tasks'),
); );
return $names; return $names;
@ -41,9 +179,45 @@ final class PhabricatorSearchApplicationSearchEngine
switch ($query_key) { switch ($query_key) {
case 'all': case 'all':
return $query; return $query;
case 'open':
return $query->setParameter('statuses', array('open'));
case 'open-tasks':
return $query
->setParameter('statuses', array('open'))
->setParameter('types', array(ManiphestPHIDTypeTask::TYPECONST));
} }
return parent::buildSavedQueryFromBuiltin($query_key); return parent::buildSavedQueryFromBuiltin($query_key);
} }
public static function getIndexableDocumentTypes() {
// TODO: This is inelegant and not very efficient, but gets us reasonable
// results. It would be nice to do this more elegantly.
// TODO: We should hide types associated with applications the user can
// not access. There's no reasonable way to do this right now.
$indexers = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorSearchDocumentIndexer')
->loadObjects();
$types = PhabricatorPHIDType::getAllTypes();
$results = array();
foreach ($types as $type) {
$typeconst = $type->getTypeConstant();
foreach ($indexers as $indexer) {
$fake_phid = 'PHID-'.$typeconst.'-fake';
if ($indexer->shouldIndexDocumentByPHID($fake_phid)) {
$results[$typeconst] = $type->getTypeName();
}
}
}
asort($results);
return $results;
}
} }

View file

@ -3,12 +3,66 @@
final class PhabricatorSearchDocumentQuery final class PhabricatorSearchDocumentQuery
extends PhabricatorCursorPagedPolicyAwareQuery { extends PhabricatorCursorPagedPolicyAwareQuery {
private $savedQuery;
public function withSavedQuery(PhabricatorSavedQuery $query) {
$this->savedQuery = $query;
return $this;
}
protected function loadPage() { protected function loadPage() {
return array(); $phids = $this->loadDocumentPHIDsWithoutPolicyChecks();
$handles = id(new PhabricatorHandleQuery())
->setViewer($this->getViewer())
->withPHIDs($phids)
->execute();
// Retain engine order.
$handles = array_select_keys($handles, $phids);
return $handles;
}
protected function willFilterPage(array $handles) {
foreach ($handles as $key => $handle) {
if (!$handle->isComplete()) {
unset($handles[$key]);
continue;
}
if ($handle->getPolicyFiltered()) {
unset($handles[$key]);
continue;
}
}
return $handles;
}
public function loadDocumentPHIDsWithoutPolicyChecks() {
$query = id(clone($this->savedQuery))
->setParameter('offset', $this->getOffset())
->setParameter('limit', $this->getRawResultLimit());
$engine = PhabricatorSearchEngineSelector::newSelector()->newEngine();
return $engine->executeSearch($query);
} }
public function getQueryApplicationClass() { public function getQueryApplicationClass() {
return 'PhabricatorApplicationSearch'; return 'PhabricatorApplicationSearch';
} }
protected function getPagingValue($result) {
throw new Exception(
pht(
'This query does not support cursor paging; it must be offset '.
'paged.'));
}
protected function nextPage(array $page) {
$this->setOffset($this->getOffset() + count($page));
return $this;
}
} }