2013-04-14 15:53:20 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Represents an abstract search engine for an application. It supports
|
|
|
|
* creating and storing saved queries.
|
|
|
|
*
|
2014-05-07 23:56:30 +02:00
|
|
|
* @task construct Constructing Engines
|
2014-05-08 18:18:02 +02:00
|
|
|
* @task app Applications
|
2014-05-07 23:56:30 +02:00
|
|
|
* @task builtin Builtin Queries
|
|
|
|
* @task uri Query URIs
|
|
|
|
* @task dates Date Filters
|
2015-04-13 00:16:55 +02:00
|
|
|
* @task order Result Ordering
|
2014-05-07 23:56:30 +02:00
|
|
|
* @task read Reading Utilities
|
2014-05-08 17:24:47 +02:00
|
|
|
* @task exec Paging and Executing Queries
|
|
|
|
* @task render Rendering Results
|
2013-04-14 15:53:20 +02:00
|
|
|
*/
|
2015-06-05 20:21:08 +02:00
|
|
|
abstract class PhabricatorApplicationSearchEngine extends Phobject {
|
2013-04-14 15:53:20 +02:00
|
|
|
|
2014-05-08 18:18:02 +02:00
|
|
|
private $application;
|
2013-05-27 22:42:18 +02:00
|
|
|
private $viewer;
|
2013-05-31 02:32:12 +02:00
|
|
|
private $errors = array();
|
Integrate ApplicationSearch with CustomField
Summary:
Ref T2625. Ref T3794. Ref T418. Ref T1703.
This is a more general version of D5278. It expands CustomField support to include real integration with ApplicationSearch.
Broadly, custom fields may elect to:
- build indicies when objects are updated;
- populate ApplicationSearch forms with new controls;
- read inputs entered into those controls out of the request; and
- apply constraints to search queries.
Some utility/helper stuff is provided to make this easier. This part could be cleaner, but seems reasonable for a first cut. In particular, the Query and SearchEngine must manually call all the hooks right now instead of everything happening magically. I think that's fine for the moment; they're pretty easy to get right.
Test Plan:
I added a new searchable "Company" field to People:
{F58229}
This also cleaned up the disable/reorder view a little bit:
{F58230}
As it did before, this field appears on the edit screen:
{F58231}
However, because it has `search`, it also appears on the search screen:
{F58232}
When queried, it returns the expected results:
{F58233}
And the actually good bit of all this is that the query can take advantage of indexes:
mysql> explain SELECT * FROM `user` user JOIN `user_customfieldstringindex` `appsearch_0` ON `appsearch_0`.objectPHID = user.phid AND `appsearch_0`.indexKey = 'mk3Ndy476ge6' AND `appsearch_0`.indexValue IN ('phacility') ORDER BY user.id DESC LIMIT 101;
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| 1 | SIMPLE | appsearch_0 | ref | key_join,key_find | key_find | 232 | const,const | 1 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | user | eq_ref | phid | phid | 194 | phabricator2_user.appsearch_0.objectPHID | 1 | |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
2 rows in set (0.00 sec)
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T418, T1703, T2625, T3794
Differential Revision: https://secure.phabricator.com/D6992
2013-09-16 22:44:34 +02:00
|
|
|
private $customFields = false;
|
2014-05-08 19:08:37 +02:00
|
|
|
private $request;
|
2014-06-25 00:59:06 +02:00
|
|
|
private $context;
|
Make mobile navigation work properly by default in more cases
Summary:
Fixes T5752. This obsoletes a bunch of old patterns and I'll follow up on those with a big "go do a bunch of mechanical code changes" task. Major goals are:
- Don't load named queries multiple times on search pages.
- Don't require extra code to get standard navigation right on mobile.
- Reduce the amount of boilerplate in ListControllers.
- Reduce the amount of boilerplate around navigation/menus in all controllers.
Specifically, here's what this does:
- The StandardPage is now a smarter/more structured object with `setNavigation()` and `setCrumbs()` methods. More rendering decisions are delayed until the last possible moment.
- It uses this to automatically add crumb actions to the application menu.
- It uses this to automatically reuse one SearchEngine instead of running queries multiple times.
- The new preferred way to build responses is `$this->newPage()` (like `$this->newDialog()`), which has structured methods for adding stuff (`setTitle()`, etc).
- SearchEngine exposes a new convenience method so you don't have to do all the controller delegation stuff.
- Building menus is generally simpler.
Test Plan:
- Tested paste list, view, edit, comment, raw controllers for functionality, mobile menu, crumbs, navigation menu.
- Edited saved queries.
- Tested Differential, Maniphest (no changes).
- Verified the paste pages don't run any duplicate NamedQuery queries.
Reviewers: chad
Reviewed By: chad
Maniphest Tasks: T5752
Differential Revision: https://secure.phabricator.com/D14382
2015-11-02 21:06:28 +01:00
|
|
|
private $controller;
|
|
|
|
private $namedQueries;
|
2014-06-25 00:59:06 +02:00
|
|
|
|
|
|
|
const CONTEXT_LIST = 'list';
|
|
|
|
const CONTEXT_PANEL = 'panel';
|
2013-05-27 22:42:18 +02:00
|
|
|
|
Make mobile navigation work properly by default in more cases
Summary:
Fixes T5752. This obsoletes a bunch of old patterns and I'll follow up on those with a big "go do a bunch of mechanical code changes" task. Major goals are:
- Don't load named queries multiple times on search pages.
- Don't require extra code to get standard navigation right on mobile.
- Reduce the amount of boilerplate in ListControllers.
- Reduce the amount of boilerplate around navigation/menus in all controllers.
Specifically, here's what this does:
- The StandardPage is now a smarter/more structured object with `setNavigation()` and `setCrumbs()` methods. More rendering decisions are delayed until the last possible moment.
- It uses this to automatically add crumb actions to the application menu.
- It uses this to automatically reuse one SearchEngine instead of running queries multiple times.
- The new preferred way to build responses is `$this->newPage()` (like `$this->newDialog()`), which has structured methods for adding stuff (`setTitle()`, etc).
- SearchEngine exposes a new convenience method so you don't have to do all the controller delegation stuff.
- Building menus is generally simpler.
Test Plan:
- Tested paste list, view, edit, comment, raw controllers for functionality, mobile menu, crumbs, navigation menu.
- Edited saved queries.
- Tested Differential, Maniphest (no changes).
- Verified the paste pages don't run any duplicate NamedQuery queries.
Reviewers: chad
Reviewed By: chad
Maniphest Tasks: T5752
Differential Revision: https://secure.phabricator.com/D14382
2015-11-02 21:06:28 +01:00
|
|
|
public function setController(PhabricatorController $controller) {
|
|
|
|
$this->controller = $controller;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getController() {
|
|
|
|
return $this->controller;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function buildResponse() {
|
|
|
|
$controller = $this->getController();
|
|
|
|
$request = $controller->getRequest();
|
|
|
|
|
|
|
|
$search = id(new PhabricatorApplicationSearchController())
|
|
|
|
->setQueryKey($request->getURIData('queryKey'))
|
|
|
|
->setSearchEngine($this);
|
|
|
|
|
|
|
|
return $controller->delegateToController($search);
|
|
|
|
}
|
|
|
|
|
2015-06-05 20:21:45 +02:00
|
|
|
public function newResultObject() {
|
2015-06-08 21:21:48 +02:00
|
|
|
// We may be able to get this automatically if newQuery() is implemented.
|
|
|
|
$query = $this->newQuery();
|
|
|
|
if ($query) {
|
|
|
|
$object = $query->newResultObject();
|
|
|
|
if ($object) {
|
|
|
|
return $object;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function newQuery() {
|
2015-06-05 20:21:45 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2013-05-27 22:42:18 +02:00
|
|
|
public function setViewer(PhabricatorUser $viewer) {
|
|
|
|
$this->viewer = $viewer;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function requireViewer() {
|
|
|
|
if (!$this->viewer) {
|
2015-05-13 23:53:52 +02:00
|
|
|
throw new PhutilInvalidStateException('setViewer');
|
2013-05-27 22:42:18 +02:00
|
|
|
}
|
|
|
|
return $this->viewer;
|
|
|
|
}
|
|
|
|
|
2014-06-25 00:59:06 +02:00
|
|
|
public function setContext($context) {
|
|
|
|
$this->context = $context;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function isPanelContext() {
|
|
|
|
return ($this->context == self::CONTEXT_PANEL);
|
|
|
|
}
|
|
|
|
|
2015-02-11 22:43:59 +01:00
|
|
|
public function canUseInPanelContext() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2014-05-20 20:42:05 +02:00
|
|
|
public function saveQuery(PhabricatorSavedQuery $query) {
|
|
|
|
$query->setEngineClassName(get_class($this));
|
|
|
|
|
|
|
|
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
|
|
|
try {
|
|
|
|
$query->save();
|
2014-08-05 23:51:21 +02:00
|
|
|
} catch (AphrontDuplicateKeyQueryException $ex) {
|
2014-05-20 20:42:05 +02:00
|
|
|
// Ignore, this is just a repeated search.
|
|
|
|
}
|
|
|
|
unset($unguarded);
|
|
|
|
}
|
|
|
|
|
2013-04-14 15:53:20 +02:00
|
|
|
/**
|
|
|
|
* Create a saved query object from the request.
|
|
|
|
*
|
|
|
|
* @param AphrontRequest The search request.
|
|
|
|
* @return PhabricatorSavedQuery
|
|
|
|
*/
|
2015-06-05 20:21:08 +02:00
|
|
|
public function buildSavedQueryFromRequest(AphrontRequest $request) {
|
|
|
|
$fields = $this->buildSearchFields();
|
|
|
|
$viewer = $this->requireViewer();
|
|
|
|
|
|
|
|
$saved = new PhabricatorSavedQuery();
|
|
|
|
foreach ($fields as $field) {
|
|
|
|
$field->setViewer($viewer);
|
|
|
|
|
|
|
|
$value = $field->readValueFromRequest($request);
|
|
|
|
$saved->setParameter($field->getKey(), $value);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $saved;
|
|
|
|
}
|
2013-04-14 15:53:20 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Executes the saved query.
|
|
|
|
*
|
|
|
|
* @param PhabricatorSavedQuery The saved query to operate on.
|
|
|
|
* @return The result of the query.
|
|
|
|
*/
|
2015-06-05 20:21:22 +02:00
|
|
|
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
|
2015-06-08 21:22:47 +02:00
|
|
|
$saved = clone $saved;
|
|
|
|
$this->willUseSavedQuery($saved);
|
|
|
|
|
2015-06-05 20:21:22 +02:00
|
|
|
$fields = $this->buildSearchFields();
|
|
|
|
$viewer = $this->requireViewer();
|
|
|
|
|
2015-06-08 21:21:48 +02:00
|
|
|
$map = array();
|
2015-06-05 20:21:22 +02:00
|
|
|
foreach ($fields as $field) {
|
|
|
|
$field->setViewer($viewer);
|
|
|
|
$field->readValueFromSavedQuery($saved);
|
|
|
|
$value = $field->getValueForQuery($field->getValue());
|
2015-06-08 21:21:48 +02:00
|
|
|
$map[$field->getKey()] = $value;
|
2015-06-05 20:21:22 +02:00
|
|
|
}
|
|
|
|
|
2015-06-08 21:21:48 +02:00
|
|
|
$query = $this->buildQueryFromParameters($map);
|
2015-06-05 20:21:45 +02:00
|
|
|
|
|
|
|
$object = $this->newResultObject();
|
|
|
|
if (!$object) {
|
|
|
|
return $query;
|
|
|
|
}
|
|
|
|
|
2015-06-08 21:20:53 +02:00
|
|
|
if ($object instanceof PhabricatorSubscribableInterface) {
|
2015-06-08 21:21:48 +02:00
|
|
|
if (!empty($map['subscriberPHIDs'])) {
|
2015-06-08 21:20:53 +02:00
|
|
|
$query->withEdgeLogicPHIDs(
|
|
|
|
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
|
|
|
|
PhabricatorQueryConstraint::OPERATOR_OR,
|
2015-06-08 21:21:48 +02:00
|
|
|
$map['subscriberPHIDs']);
|
2015-06-08 21:20:53 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-07 16:31:28 +02:00
|
|
|
if ($object instanceof PhabricatorProjectInterface) {
|
2015-06-08 21:21:48 +02:00
|
|
|
if (!empty($map['projectPHIDs'])) {
|
2015-06-07 16:31:28 +02:00
|
|
|
$query->withEdgeLogicConstraints(
|
|
|
|
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
|
2015-06-08 21:21:48 +02:00
|
|
|
$map['projectPHIDs']);
|
2015-06-07 16:31:28 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-05 20:21:45 +02:00
|
|
|
if ($object instanceof PhabricatorSpacesInterface) {
|
2015-06-08 21:21:48 +02:00
|
|
|
if (!empty($map['spacePHIDs'])) {
|
|
|
|
$query->withSpacePHIDs($map['spacePHIDs']);
|
2015-06-11 19:13:47 +02:00
|
|
|
} else {
|
|
|
|
// If the user doesn't search for objects in specific spaces, we
|
|
|
|
// default to "all active spaces you have permission to view".
|
|
|
|
$query->withSpaceIsArchived(false);
|
2015-06-05 20:21:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-08 21:20:16 +02:00
|
|
|
if ($object instanceof PhabricatorCustomFieldInterface) {
|
|
|
|
$this->applyCustomFieldsToQuery($query, $saved);
|
|
|
|
}
|
|
|
|
|
2015-06-08 21:22:47 +02:00
|
|
|
$order = $saved->getParameter('order');
|
2015-06-08 21:23:13 +02:00
|
|
|
$builtin = $query->getBuiltinOrderAliasMap();
|
2015-06-08 21:22:47 +02:00
|
|
|
if (strlen($order) && isset($builtin[$order])) {
|
|
|
|
$query->setOrder($order);
|
|
|
|
} else {
|
|
|
|
// If the order is invalid or not available, we choose the first
|
|
|
|
// builtin order. This isn't always the default order for the query,
|
|
|
|
// but is the first value in the "Order" dropdown, and makes the query
|
|
|
|
// behavior more consistent with the UI. In queries where the two
|
|
|
|
// orders differ, this order is the preferred order for humans.
|
|
|
|
$query->setOrder(head_key($builtin));
|
|
|
|
}
|
2015-06-08 21:21:48 +02:00
|
|
|
|
2015-06-05 20:21:45 +02:00
|
|
|
return $query;
|
2015-06-05 20:21:22 +02:00
|
|
|
}
|
|
|
|
|
2015-06-08 21:22:47 +02:00
|
|
|
/**
|
|
|
|
* Hook for subclasses to adjust saved queries prior to use.
|
|
|
|
*
|
|
|
|
* If an application changes how queries are saved, it can implement this
|
|
|
|
* hook to keep old queries working the way users expect, by reading,
|
|
|
|
* adjusting, and overwriting parameters.
|
|
|
|
*
|
|
|
|
* @param PhabricatorSavedQuery Saved query which will be executed.
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
protected function willUseSavedQuery(PhabricatorSavedQuery $saved) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-06-05 20:21:22 +02:00
|
|
|
protected function buildQueryFromParameters(array $parameters) {
|
|
|
|
throw new PhutilMethodNotImplementedException();
|
|
|
|
}
|
2013-04-14 15:53:20 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Builds the search form using the request.
|
|
|
|
*
|
2013-05-27 22:43:47 +02:00
|
|
|
* @param AphrontFormView Form to populate.
|
2013-04-14 15:53:20 +02:00
|
|
|
* @param PhabricatorSavedQuery The query from which to build the form.
|
|
|
|
* @return void
|
|
|
|
*/
|
2015-06-05 20:21:08 +02:00
|
|
|
public function buildSearchForm(
|
2013-05-27 22:43:47 +02:00
|
|
|
AphrontFormView $form,
|
2015-06-05 20:21:08 +02:00
|
|
|
PhabricatorSavedQuery $saved) {
|
|
|
|
|
2015-06-08 21:22:47 +02:00
|
|
|
$saved = clone $saved;
|
|
|
|
$this->willUseSavedQuery($saved);
|
|
|
|
|
2015-06-05 20:21:08 +02:00
|
|
|
$fields = $this->buildSearchFields();
|
2015-06-09 22:17:31 +02:00
|
|
|
$fields = $this->adjustFieldsForDisplay($fields);
|
2015-06-05 20:21:08 +02:00
|
|
|
$viewer = $this->requireViewer();
|
|
|
|
|
|
|
|
foreach ($fields as $field) {
|
|
|
|
$field->setViewer($viewer);
|
|
|
|
$field->readValueFromSavedQuery($saved);
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($fields as $field) {
|
|
|
|
foreach ($field->getErrors() as $error) {
|
|
|
|
$this->addError(last($error));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($fields as $field) {
|
|
|
|
$field->appendToForm($form);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function buildSearchFields() {
|
|
|
|
$fields = array();
|
|
|
|
|
|
|
|
foreach ($this->buildCustomSearchFields() as $field) {
|
|
|
|
$fields[] = $field;
|
|
|
|
}
|
|
|
|
|
2015-06-05 20:21:45 +02:00
|
|
|
$object = $this->newResultObject();
|
2015-06-08 21:21:48 +02:00
|
|
|
if ($object) {
|
|
|
|
if ($object instanceof PhabricatorSubscribableInterface) {
|
|
|
|
$fields[] = id(new PhabricatorSearchSubscribersField())
|
|
|
|
->setLabel(pht('Subscribers'))
|
|
|
|
->setKey('subscriberPHIDs')
|
|
|
|
->setAliases(array('subscriber', 'subscribers'));
|
|
|
|
}
|
2015-06-08 21:20:53 +02:00
|
|
|
|
2015-06-08 21:21:48 +02:00
|
|
|
if ($object instanceof PhabricatorProjectInterface) {
|
2015-07-06 14:52:04 +02:00
|
|
|
$fields[] = id(new PhabricatorProjectSearchField())
|
2015-06-08 21:21:48 +02:00
|
|
|
->setKey('projectPHIDs')
|
|
|
|
->setAliases(array('project', 'projects'))
|
|
|
|
->setLabel(pht('Projects'));
|
|
|
|
}
|
2015-06-07 16:31:28 +02:00
|
|
|
|
2015-06-08 21:21:48 +02:00
|
|
|
if ($object instanceof PhabricatorSpacesInterface) {
|
|
|
|
if (PhabricatorSpacesNamespaceQuery::getSpacesExist()) {
|
2015-07-06 14:52:04 +02:00
|
|
|
$fields[] = id(new PhabricatorSpacesSearchField())
|
2015-06-08 21:21:48 +02:00
|
|
|
->setKey('spacePHIDs')
|
|
|
|
->setAliases(array('space', 'spaces'))
|
|
|
|
->setLabel(pht('Spaces'));
|
|
|
|
}
|
2015-06-05 20:21:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-08 21:20:16 +02:00
|
|
|
foreach ($this->buildCustomFieldSearchFields() as $custom_field) {
|
|
|
|
$fields[] = $custom_field;
|
|
|
|
}
|
|
|
|
|
2015-06-08 21:21:48 +02:00
|
|
|
$query = $this->newQuery();
|
2015-06-22 22:27:37 +02:00
|
|
|
if ($query && $this->shouldShowOrderField()) {
|
2015-06-08 21:21:48 +02:00
|
|
|
$orders = $query->getBuiltinOrders();
|
|
|
|
$orders = ipull($orders, 'name');
|
|
|
|
|
|
|
|
$fields[] = id(new PhabricatorSearchOrderField())
|
2015-06-09 22:17:31 +02:00
|
|
|
->setLabel(pht('Order By'))
|
2015-06-08 21:21:48 +02:00
|
|
|
->setKey('order')
|
2015-06-09 22:49:01 +02:00
|
|
|
->setOrderAliases($query->getBuiltinOrderAliasMap())
|
2015-06-08 21:21:48 +02:00
|
|
|
->setOptions($orders);
|
|
|
|
}
|
|
|
|
|
2015-06-08 21:20:35 +02:00
|
|
|
$field_map = array();
|
|
|
|
foreach ($fields as $field) {
|
|
|
|
$key = $field->getKey();
|
|
|
|
if (isset($field_map[$key])) {
|
|
|
|
throw new Exception(
|
|
|
|
pht(
|
|
|
|
'Two fields in this SearchEngine use the same key ("%s"), but '.
|
|
|
|
'each field must use a unique key.',
|
|
|
|
$key));
|
|
|
|
}
|
|
|
|
$field_map[$key] = $field;
|
|
|
|
}
|
|
|
|
|
2015-06-09 22:17:31 +02:00
|
|
|
return $field_map;
|
2015-06-08 21:20:35 +02:00
|
|
|
}
|
|
|
|
|
2015-06-22 22:27:37 +02:00
|
|
|
protected function shouldShowOrderField() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-06-08 21:20:35 +02:00
|
|
|
private function adjustFieldsForDisplay(array $field_map) {
|
|
|
|
$order = $this->getDefaultFieldOrder();
|
|
|
|
|
|
|
|
$head_keys = array();
|
|
|
|
$tail_keys = array();
|
|
|
|
$seen_tail = false;
|
|
|
|
foreach ($order as $order_key) {
|
|
|
|
if ($order_key === '...') {
|
|
|
|
$seen_tail = true;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$seen_tail) {
|
|
|
|
$head_keys[] = $order_key;
|
|
|
|
} else {
|
|
|
|
$tail_keys[] = $order_key;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$head = array_select_keys($field_map, $head_keys);
|
|
|
|
$body = array_diff_key($field_map, array_fuse($tail_keys));
|
|
|
|
$tail = array_select_keys($field_map, $tail_keys);
|
|
|
|
|
2015-06-09 22:17:31 +02:00
|
|
|
$result = $head + $body + $tail;
|
|
|
|
|
|
|
|
foreach ($this->getHiddenFields() as $hidden_key) {
|
|
|
|
unset($result[$hidden_key]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $result;
|
2015-06-05 20:21:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
protected function buildCustomSearchFields() {
|
|
|
|
throw new PhutilMethodNotImplementedException();
|
|
|
|
}
|
2013-04-14 15:53:20 +02:00
|
|
|
|
2015-06-08 21:20:35 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Define the default display order for fields by returning a list of
|
|
|
|
* field keys.
|
|
|
|
*
|
|
|
|
* You can use the special key `...` to mean "all unspecified fields go
|
|
|
|
* here". This lets you easily put important fields at the top of the form,
|
|
|
|
* standard fields in the middle of the form, and less important fields at
|
|
|
|
* the bottom.
|
|
|
|
*
|
|
|
|
* For example, you might return a list like this:
|
|
|
|
*
|
|
|
|
* return array(
|
|
|
|
* 'authorPHIDs',
|
|
|
|
* 'reviewerPHIDs',
|
|
|
|
* '...',
|
|
|
|
* 'createdAfter',
|
|
|
|
* 'createdBefore',
|
|
|
|
* );
|
|
|
|
*
|
|
|
|
* Any unspecified fields (including custom fields and fields added
|
|
|
|
* automatically by infrastruture) will be put in the middle.
|
|
|
|
*
|
|
|
|
* @return list<string> Default ordering for field keys.
|
|
|
|
*/
|
|
|
|
protected function getDefaultFieldOrder() {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
2015-06-09 22:17:31 +02:00
|
|
|
/**
|
|
|
|
* Return a list of field keys which should be hidden from the viewer.
|
|
|
|
*
|
|
|
|
* @return list<string> Fields to hide.
|
|
|
|
*/
|
|
|
|
protected function getHiddenFields() {
|
|
|
|
return array();
|
|
|
|
}
|
2013-04-14 15:53:20 +02:00
|
|
|
|
2013-05-31 02:32:12 +02:00
|
|
|
public function getErrors() {
|
|
|
|
return $this->errors;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function addError($error) {
|
|
|
|
$this->errors[] = $error;
|
|
|
|
return $this;
|
|
|
|
}
|
2013-05-27 22:41:39 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Return an application URI corresponding to the results page of a query.
|
|
|
|
* Normally, this is something like `/application/query/QUERYKEY/`.
|
|
|
|
*
|
2013-05-27 22:43:02 +02:00
|
|
|
* @param string The query key to build a URI for.
|
|
|
|
* @return string URI where the query can be executed.
|
2013-05-27 22:42:44 +02:00
|
|
|
* @task uri
|
2013-05-27 22:41:39 +02:00
|
|
|
*/
|
2013-05-30 23:09:02 +02:00
|
|
|
public function getQueryResultsPageURI($query_key) {
|
|
|
|
return $this->getURI('query/'.$query_key.'/');
|
|
|
|
}
|
2013-05-27 22:42:18 +02:00
|
|
|
|
|
|
|
|
2013-05-27 22:42:44 +02:00
|
|
|
/**
|
|
|
|
* Return an application URI for query management. This is used when, e.g.,
|
|
|
|
* a query deletion operation is cancelled.
|
|
|
|
*
|
|
|
|
* @return string URI where queries can be managed.
|
|
|
|
* @task uri
|
|
|
|
*/
|
2013-05-30 23:09:02 +02:00
|
|
|
public function getQueryManagementURI() {
|
|
|
|
return $this->getURI('query/edit/');
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return the URI to a path within the application. Used to construct default
|
|
|
|
* URIs for management and results.
|
|
|
|
*
|
|
|
|
* @return string URI to path.
|
|
|
|
* @task uri
|
|
|
|
*/
|
|
|
|
abstract protected function getURI($path);
|
2013-05-27 22:42:44 +02:00
|
|
|
|
|
|
|
|
2014-06-12 22:22:20 +02:00
|
|
|
/**
|
|
|
|
* Return a human readable description of the type of objects this query
|
|
|
|
* searches for.
|
|
|
|
*
|
|
|
|
* For example, "Tasks" or "Commits".
|
|
|
|
*
|
|
|
|
* @return string Human-readable description of what this engine is used to
|
|
|
|
* find.
|
|
|
|
*/
|
|
|
|
abstract public function getResultTypeDescription();
|
|
|
|
|
|
|
|
|
2013-05-27 22:42:18 +02:00
|
|
|
public function newSavedQuery() {
|
|
|
|
return id(new PhabricatorSavedQuery())
|
|
|
|
->setEngineClassName(get_class($this));
|
|
|
|
}
|
|
|
|
|
2013-12-26 21:30:36 +01:00
|
|
|
public function addNavigationItems(PHUIListView $menu) {
|
2013-05-30 23:09:02 +02:00
|
|
|
$viewer = $this->requireViewer();
|
|
|
|
|
2013-12-26 21:30:36 +01:00
|
|
|
$menu->newLabel(pht('Queries'));
|
2013-05-30 23:09:02 +02:00
|
|
|
|
2013-06-05 14:28:25 +02:00
|
|
|
$named_queries = $this->loadEnabledNamedQueries();
|
2013-05-30 23:09:02 +02:00
|
|
|
|
|
|
|
foreach ($named_queries as $query) {
|
|
|
|
$key = $query->getQueryKey();
|
|
|
|
$uri = $this->getQueryResultsPageURI($key);
|
2013-12-26 21:30:36 +01:00
|
|
|
$menu->newLink($query->getQueryName(), $uri, 'query/'.$key);
|
2013-05-30 23:09:02 +02:00
|
|
|
}
|
|
|
|
|
2013-05-31 19:51:20 +02:00
|
|
|
if ($viewer->isLoggedIn()) {
|
|
|
|
$manage_uri = $this->getQueryManagementURI();
|
2013-12-26 21:30:36 +01:00
|
|
|
$menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit');
|
2013-05-31 19:51:20 +02:00
|
|
|
}
|
2013-05-30 23:09:02 +02:00
|
|
|
|
2013-05-31 19:51:05 +02:00
|
|
|
$menu->newLabel(pht('Search'));
|
2013-05-30 23:09:02 +02:00
|
|
|
$advanced_uri = $this->getQueryResultsPageURI('advanced');
|
2013-12-26 21:30:36 +01:00
|
|
|
$menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced');
|
2013-05-30 23:09:02 +02:00
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2013-06-05 14:28:25 +02:00
|
|
|
public function loadAllNamedQueries() {
|
|
|
|
$viewer = $this->requireViewer();
|
|
|
|
$builtin = $this->getBuiltinQueries($viewer);
|
|
|
|
|
Make mobile navigation work properly by default in more cases
Summary:
Fixes T5752. This obsoletes a bunch of old patterns and I'll follow up on those with a big "go do a bunch of mechanical code changes" task. Major goals are:
- Don't load named queries multiple times on search pages.
- Don't require extra code to get standard navigation right on mobile.
- Reduce the amount of boilerplate in ListControllers.
- Reduce the amount of boilerplate around navigation/menus in all controllers.
Specifically, here's what this does:
- The StandardPage is now a smarter/more structured object with `setNavigation()` and `setCrumbs()` methods. More rendering decisions are delayed until the last possible moment.
- It uses this to automatically add crumb actions to the application menu.
- It uses this to automatically reuse one SearchEngine instead of running queries multiple times.
- The new preferred way to build responses is `$this->newPage()` (like `$this->newDialog()`), which has structured methods for adding stuff (`setTitle()`, etc).
- SearchEngine exposes a new convenience method so you don't have to do all the controller delegation stuff.
- Building menus is generally simpler.
Test Plan:
- Tested paste list, view, edit, comment, raw controllers for functionality, mobile menu, crumbs, navigation menu.
- Edited saved queries.
- Tested Differential, Maniphest (no changes).
- Verified the paste pages don't run any duplicate NamedQuery queries.
Reviewers: chad
Reviewed By: chad
Maniphest Tasks: T5752
Differential Revision: https://secure.phabricator.com/D14382
2015-11-02 21:06:28 +01:00
|
|
|
if ($this->namedQueries === null) {
|
|
|
|
$named_queries = id(new PhabricatorNamedQueryQuery())
|
|
|
|
->setViewer($viewer)
|
|
|
|
->withUserPHIDs(array($viewer->getPHID()))
|
|
|
|
->withEngineClassNames(array(get_class($this)))
|
|
|
|
->execute();
|
|
|
|
$named_queries = mpull($named_queries, null, 'getQueryKey');
|
|
|
|
|
|
|
|
$builtin = mpull($builtin, null, 'getQueryKey');
|
|
|
|
|
|
|
|
foreach ($named_queries as $key => $named_query) {
|
|
|
|
if ($named_query->getIsBuiltin()) {
|
|
|
|
if (isset($builtin[$key])) {
|
|
|
|
$named_queries[$key]->setQueryName($builtin[$key]->getQueryName());
|
|
|
|
unset($builtin[$key]);
|
|
|
|
} else {
|
|
|
|
unset($named_queries[$key]);
|
|
|
|
}
|
2013-06-05 14:28:25 +02:00
|
|
|
}
|
Make mobile navigation work properly by default in more cases
Summary:
Fixes T5752. This obsoletes a bunch of old patterns and I'll follow up on those with a big "go do a bunch of mechanical code changes" task. Major goals are:
- Don't load named queries multiple times on search pages.
- Don't require extra code to get standard navigation right on mobile.
- Reduce the amount of boilerplate in ListControllers.
- Reduce the amount of boilerplate around navigation/menus in all controllers.
Specifically, here's what this does:
- The StandardPage is now a smarter/more structured object with `setNavigation()` and `setCrumbs()` methods. More rendering decisions are delayed until the last possible moment.
- It uses this to automatically add crumb actions to the application menu.
- It uses this to automatically reuse one SearchEngine instead of running queries multiple times.
- The new preferred way to build responses is `$this->newPage()` (like `$this->newDialog()`), which has structured methods for adding stuff (`setTitle()`, etc).
- SearchEngine exposes a new convenience method so you don't have to do all the controller delegation stuff.
- Building menus is generally simpler.
Test Plan:
- Tested paste list, view, edit, comment, raw controllers for functionality, mobile menu, crumbs, navigation menu.
- Edited saved queries.
- Tested Differential, Maniphest (no changes).
- Verified the paste pages don't run any duplicate NamedQuery queries.
Reviewers: chad
Reviewed By: chad
Maniphest Tasks: T5752
Differential Revision: https://secure.phabricator.com/D14382
2015-11-02 21:06:28 +01:00
|
|
|
|
|
|
|
unset($builtin[$key]);
|
2013-06-05 14:28:25 +02:00
|
|
|
}
|
|
|
|
|
Make mobile navigation work properly by default in more cases
Summary:
Fixes T5752. This obsoletes a bunch of old patterns and I'll follow up on those with a big "go do a bunch of mechanical code changes" task. Major goals are:
- Don't load named queries multiple times on search pages.
- Don't require extra code to get standard navigation right on mobile.
- Reduce the amount of boilerplate in ListControllers.
- Reduce the amount of boilerplate around navigation/menus in all controllers.
Specifically, here's what this does:
- The StandardPage is now a smarter/more structured object with `setNavigation()` and `setCrumbs()` methods. More rendering decisions are delayed until the last possible moment.
- It uses this to automatically add crumb actions to the application menu.
- It uses this to automatically reuse one SearchEngine instead of running queries multiple times.
- The new preferred way to build responses is `$this->newPage()` (like `$this->newDialog()`), which has structured methods for adding stuff (`setTitle()`, etc).
- SearchEngine exposes a new convenience method so you don't have to do all the controller delegation stuff.
- Building menus is generally simpler.
Test Plan:
- Tested paste list, view, edit, comment, raw controllers for functionality, mobile menu, crumbs, navigation menu.
- Edited saved queries.
- Tested Differential, Maniphest (no changes).
- Verified the paste pages don't run any duplicate NamedQuery queries.
Reviewers: chad
Reviewed By: chad
Maniphest Tasks: T5752
Differential Revision: https://secure.phabricator.com/D14382
2015-11-02 21:06:28 +01:00
|
|
|
$named_queries = msort($named_queries, 'getSortKey');
|
|
|
|
$this->namedQueries = $named_queries;
|
2013-06-05 14:28:25 +02:00
|
|
|
}
|
|
|
|
|
Make mobile navigation work properly by default in more cases
Summary:
Fixes T5752. This obsoletes a bunch of old patterns and I'll follow up on those with a big "go do a bunch of mechanical code changes" task. Major goals are:
- Don't load named queries multiple times on search pages.
- Don't require extra code to get standard navigation right on mobile.
- Reduce the amount of boilerplate in ListControllers.
- Reduce the amount of boilerplate around navigation/menus in all controllers.
Specifically, here's what this does:
- The StandardPage is now a smarter/more structured object with `setNavigation()` and `setCrumbs()` methods. More rendering decisions are delayed until the last possible moment.
- It uses this to automatically add crumb actions to the application menu.
- It uses this to automatically reuse one SearchEngine instead of running queries multiple times.
- The new preferred way to build responses is `$this->newPage()` (like `$this->newDialog()`), which has structured methods for adding stuff (`setTitle()`, etc).
- SearchEngine exposes a new convenience method so you don't have to do all the controller delegation stuff.
- Building menus is generally simpler.
Test Plan:
- Tested paste list, view, edit, comment, raw controllers for functionality, mobile menu, crumbs, navigation menu.
- Edited saved queries.
- Tested Differential, Maniphest (no changes).
- Verified the paste pages don't run any duplicate NamedQuery queries.
Reviewers: chad
Reviewed By: chad
Maniphest Tasks: T5752
Differential Revision: https://secure.phabricator.com/D14382
2015-11-02 21:06:28 +01:00
|
|
|
return $this->namedQueries + $builtin;
|
2013-06-05 14:28:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function loadEnabledNamedQueries() {
|
|
|
|
$named_queries = $this->loadAllNamedQueries();
|
|
|
|
foreach ($named_queries as $key => $named_query) {
|
|
|
|
if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) {
|
|
|
|
unset($named_queries[$key]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $named_queries;
|
|
|
|
}
|
|
|
|
|
2015-04-18 20:18:27 +02:00
|
|
|
protected function setQueryProjects(
|
|
|
|
PhabricatorCursorPagedPolicyAwareQuery $query,
|
|
|
|
PhabricatorSavedQuery $saved) {
|
|
|
|
|
|
|
|
$datasource = id(new PhabricatorProjectLogicalDatasource())
|
|
|
|
->setViewer($this->requireViewer());
|
|
|
|
|
|
|
|
$projects = $saved->getParameter('projects', array());
|
|
|
|
$constraints = $datasource->evaluateTokens($projects);
|
|
|
|
|
|
|
|
if ($constraints) {
|
|
|
|
$query->withEdgeLogicConstraints(
|
|
|
|
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
|
|
|
|
$constraints);
|
|
|
|
}
|
2015-07-06 14:53:41 +02:00
|
|
|
|
|
|
|
return $this;
|
2015-04-18 20:18:27 +02:00
|
|
|
}
|
|
|
|
|
2013-05-30 23:09:02 +02:00
|
|
|
|
2014-05-08 18:18:02 +02:00
|
|
|
/* -( Applications )------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
protected function getApplicationURI($path = '') {
|
|
|
|
return $this->getApplication()->getApplicationURI($path);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function getApplication() {
|
|
|
|
if (!$this->application) {
|
|
|
|
$class = $this->getApplicationClassName();
|
|
|
|
|
|
|
|
$this->application = id(new PhabricatorApplicationQuery())
|
|
|
|
->setViewer($this->requireViewer())
|
|
|
|
->withClasses(array($class))
|
|
|
|
->withInstalled(true)
|
|
|
|
->executeOne();
|
|
|
|
|
|
|
|
if (!$this->application) {
|
|
|
|
throw new Exception(
|
|
|
|
pht(
|
|
|
|
'Application "%s" is not installed!',
|
|
|
|
$class));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->application;
|
|
|
|
}
|
|
|
|
|
2015-02-05 00:47:48 +01:00
|
|
|
abstract public function getApplicationClassName();
|
2014-05-08 18:18:02 +02:00
|
|
|
|
|
|
|
|
2014-05-07 23:56:30 +02:00
|
|
|
/* -( Constructing Engines )----------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load all available application search engines.
|
|
|
|
*
|
|
|
|
* @return list<PhabricatorApplicationSearchEngine> All available engines.
|
|
|
|
* @task construct
|
|
|
|
*/
|
|
|
|
public static function getAllEngines() {
|
2015-07-07 14:34:30 +02:00
|
|
|
return id(new PhutilClassMapQuery())
|
2014-05-07 23:56:30 +02:00
|
|
|
->setAncestorClass(__CLASS__)
|
2015-07-07 14:34:30 +02:00
|
|
|
->execute();
|
2014-05-07 23:56:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get an engine by class name, if it exists.
|
|
|
|
*
|
|
|
|
* @return PhabricatorApplicationSearchEngine|null Engine, or null if it does
|
|
|
|
* not exist.
|
|
|
|
* @task construct
|
|
|
|
*/
|
|
|
|
public static function getEngineByClassName($class_name) {
|
|
|
|
return idx(self::getAllEngines(), $class_name);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-05-27 22:42:18 +02:00
|
|
|
/* -( Builtin Queries )---------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task builtin
|
|
|
|
*/
|
|
|
|
public function getBuiltinQueries() {
|
|
|
|
$names = $this->getBuiltinQueryNames();
|
|
|
|
|
|
|
|
$queries = array();
|
2013-06-06 01:22:27 +02:00
|
|
|
$sequence = 0;
|
2013-05-27 22:42:18 +02:00
|
|
|
foreach ($names as $key => $name) {
|
|
|
|
$queries[$key] = id(new PhabricatorNamedQuery())
|
2013-06-06 01:22:27 +02:00
|
|
|
->setUserPHID($this->requireViewer()->getPHID())
|
|
|
|
->setEngineClassName(get_class($this))
|
2013-05-27 22:42:18 +02:00
|
|
|
->setQueryName($name)
|
|
|
|
->setQueryKey($key)
|
2013-06-06 01:22:27 +02:00
|
|
|
->setSequence((1 << 24) + $sequence++)
|
|
|
|
->setIsBuiltin(true);
|
2013-05-27 22:42:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return $queries;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-05-29 23:00:52 +02:00
|
|
|
/**
|
|
|
|
* @task builtin
|
|
|
|
*/
|
|
|
|
public function getBuiltinQuery($query_key) {
|
|
|
|
if (!$this->isBuiltinQuery($query_key)) {
|
2015-05-22 09:27:56 +02:00
|
|
|
throw new Exception(pht("'%s' is not a builtin!", $query_key));
|
2013-05-29 23:00:52 +02:00
|
|
|
}
|
|
|
|
return idx($this->getBuiltinQueries(), $query_key);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-05-27 22:42:18 +02:00
|
|
|
/**
|
|
|
|
* @task builtin
|
|
|
|
*/
|
|
|
|
protected function getBuiltinQueryNames() {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task builtin
|
|
|
|
*/
|
|
|
|
public function isBuiltinQuery($query_key) {
|
|
|
|
$builtins = $this->getBuiltinQueries();
|
|
|
|
return isset($builtins[$query_key]);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task builtin
|
|
|
|
*/
|
|
|
|
public function buildSavedQueryFromBuiltin($query_key) {
|
2015-05-22 09:27:56 +02:00
|
|
|
throw new Exception(pht("Builtin '%s' is not supported!", $query_key));
|
2013-05-27 22:42:18 +02:00
|
|
|
}
|
2013-05-31 02:32:12 +02:00
|
|
|
|
|
|
|
|
Allow construction of ApplicationSearch queries with GET
Summary:
Ref T3775 (discussion here). Ref T2625.
T3775 presents two problems:
# Existing tools which linked to `/differential/active/epriestley/` (that is, put a username in the URL) can't generate search links now.
# Humans can't edit the URL anymore, either.
I think (1) is an actual issue, and this fixes it. I think (2) is pretty fluff, and this doesn't really try to fix it, although it probably improves it.
The fix for (1) is:
- Provide a helper to read a parameter containing either a list of user PHIDs or a list of usernames, so `/?users[]=PHID-USER-xyz` (from a tokenizer) and `/?users=alincoln,htaft` (from an external program) are equivalent inputs.
- Rename all the form parameters to be more digestable (`authorPHIDs` -> `authors`). Almost all of them were in this form already anyway. This just gives us `?users=alincoln` instead of `userPHIDs=alincoln`.
- Inside ApplicationSearch, if a request has no query associated with it but does have query parameters, build a query from the request instead of issuing the user's default query. Basically, this means that `/differential/` runs the default query, while `/differential/?users=x` runs a custom query.
Test Plan: {F56612}
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2625, T3775
Differential Revision: https://secure.phabricator.com/D6840
2013-08-29 20:52:29 +02:00
|
|
|
/* -( Reading Utilities )--------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Read a list of user PHIDs from a request in a flexible way. This method
|
|
|
|
* supports either of these forms:
|
|
|
|
*
|
|
|
|
* users[]=alincoln&users[]=htaft
|
|
|
|
* users=alincoln,htaft
|
|
|
|
*
|
|
|
|
* Additionally, users can be specified either by PHID or by name.
|
|
|
|
*
|
|
|
|
* The main goal of this flexibility is to allow external programs to generate
|
|
|
|
* links to pages (like "alincoln's open revisions") without needing to make
|
|
|
|
* API calls.
|
|
|
|
*
|
|
|
|
* @param AphrontRequest Request to read user PHIDs from.
|
|
|
|
* @param string Key to read in the request.
|
2013-10-05 05:41:50 +02:00
|
|
|
* @param list<const> Other permitted PHID types.
|
2015-04-19 16:14:28 +02:00
|
|
|
* @return list<phid> List of user PHIDs and selector functions.
|
Allow construction of ApplicationSearch queries with GET
Summary:
Ref T3775 (discussion here). Ref T2625.
T3775 presents two problems:
# Existing tools which linked to `/differential/active/epriestley/` (that is, put a username in the URL) can't generate search links now.
# Humans can't edit the URL anymore, either.
I think (1) is an actual issue, and this fixes it. I think (2) is pretty fluff, and this doesn't really try to fix it, although it probably improves it.
The fix for (1) is:
- Provide a helper to read a parameter containing either a list of user PHIDs or a list of usernames, so `/?users[]=PHID-USER-xyz` (from a tokenizer) and `/?users=alincoln,htaft` (from an external program) are equivalent inputs.
- Rename all the form parameters to be more digestable (`authorPHIDs` -> `authors`). Almost all of them were in this form already anyway. This just gives us `?users=alincoln` instead of `userPHIDs=alincoln`.
- Inside ApplicationSearch, if a request has no query associated with it but does have query parameters, build a query from the request instead of issuing the user's default query. Basically, this means that `/differential/` runs the default query, while `/differential/?users=x` runs a custom query.
Test Plan: {F56612}
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2625, T3775
Differential Revision: https://secure.phabricator.com/D6840
2013-08-29 20:52:29 +02:00
|
|
|
* @task read
|
|
|
|
*/
|
2013-10-05 05:41:50 +02:00
|
|
|
protected function readUsersFromRequest(
|
|
|
|
AphrontRequest $request,
|
|
|
|
$key,
|
|
|
|
array $allow_types = array()) {
|
2013-12-26 19:40:22 +01:00
|
|
|
|
|
|
|
$list = $this->readListFromRequest($request, $key);
|
Allow construction of ApplicationSearch queries with GET
Summary:
Ref T3775 (discussion here). Ref T2625.
T3775 presents two problems:
# Existing tools which linked to `/differential/active/epriestley/` (that is, put a username in the URL) can't generate search links now.
# Humans can't edit the URL anymore, either.
I think (1) is an actual issue, and this fixes it. I think (2) is pretty fluff, and this doesn't really try to fix it, although it probably improves it.
The fix for (1) is:
- Provide a helper to read a parameter containing either a list of user PHIDs or a list of usernames, so `/?users[]=PHID-USER-xyz` (from a tokenizer) and `/?users=alincoln,htaft` (from an external program) are equivalent inputs.
- Rename all the form parameters to be more digestable (`authorPHIDs` -> `authors`). Almost all of them were in this form already anyway. This just gives us `?users=alincoln` instead of `userPHIDs=alincoln`.
- Inside ApplicationSearch, if a request has no query associated with it but does have query parameters, build a query from the request instead of issuing the user's default query. Basically, this means that `/differential/` runs the default query, while `/differential/?users=x` runs a custom query.
Test Plan: {F56612}
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2625, T3775
Differential Revision: https://secure.phabricator.com/D6840
2013-08-29 20:52:29 +02:00
|
|
|
|
|
|
|
$phids = array();
|
|
|
|
$names = array();
|
2013-10-05 05:41:50 +02:00
|
|
|
$allow_types = array_fuse($allow_types);
|
2015-04-19 16:14:28 +02:00
|
|
|
$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;
|
Allow construction of ApplicationSearch queries with GET
Summary:
Ref T3775 (discussion here). Ref T2625.
T3775 presents two problems:
# Existing tools which linked to `/differential/active/epriestley/` (that is, put a username in the URL) can't generate search links now.
# Humans can't edit the URL anymore, either.
I think (1) is an actual issue, and this fixes it. I think (2) is pretty fluff, and this doesn't really try to fix it, although it probably improves it.
The fix for (1) is:
- Provide a helper to read a parameter containing either a list of user PHIDs or a list of usernames, so `/?users[]=PHID-USER-xyz` (from a tokenizer) and `/?users=alincoln,htaft` (from an external program) are equivalent inputs.
- Rename all the form parameters to be more digestable (`authorPHIDs` -> `authors`). Almost all of them were in this form already anyway. This just gives us `?users=alincoln` instead of `userPHIDs=alincoln`.
- Inside ApplicationSearch, if a request has no query associated with it but does have query parameters, build a query from the request instead of issuing the user's default query. Basically, this means that `/differential/` runs the default query, while `/differential/?users=x` runs a custom query.
Test Plan: {F56612}
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2625, T3775
Differential Revision: https://secure.phabricator.com/D6840
2013-08-29 20:52:29 +02:00
|
|
|
foreach ($list as $item) {
|
2013-10-05 05:41:50 +02:00
|
|
|
$type = phid_get_type($item);
|
|
|
|
if ($type == $user_type) {
|
|
|
|
$phids[] = $item;
|
|
|
|
} else if (isset($allow_types[$type])) {
|
Allow construction of ApplicationSearch queries with GET
Summary:
Ref T3775 (discussion here). Ref T2625.
T3775 presents two problems:
# Existing tools which linked to `/differential/active/epriestley/` (that is, put a username in the URL) can't generate search links now.
# Humans can't edit the URL anymore, either.
I think (1) is an actual issue, and this fixes it. I think (2) is pretty fluff, and this doesn't really try to fix it, although it probably improves it.
The fix for (1) is:
- Provide a helper to read a parameter containing either a list of user PHIDs or a list of usernames, so `/?users[]=PHID-USER-xyz` (from a tokenizer) and `/?users=alincoln,htaft` (from an external program) are equivalent inputs.
- Rename all the form parameters to be more digestable (`authorPHIDs` -> `authors`). Almost all of them were in this form already anyway. This just gives us `?users=alincoln` instead of `userPHIDs=alincoln`.
- Inside ApplicationSearch, if a request has no query associated with it but does have query parameters, build a query from the request instead of issuing the user's default query. Basically, this means that `/differential/` runs the default query, while `/differential/?users=x` runs a custom query.
Test Plan: {F56612}
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2625, T3775
Differential Revision: https://secure.phabricator.com/D6840
2013-08-29 20:52:29 +02:00
|
|
|
$phids[] = $item;
|
|
|
|
} else {
|
2015-04-17 00:30:41 +02:00
|
|
|
if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
|
|
|
|
// If this is a function, pass it through unchanged; we'll evaluate
|
|
|
|
// it later.
|
|
|
|
$phids[] = $item;
|
|
|
|
} else {
|
|
|
|
$names[] = $item;
|
|
|
|
}
|
Allow construction of ApplicationSearch queries with GET
Summary:
Ref T3775 (discussion here). Ref T2625.
T3775 presents two problems:
# Existing tools which linked to `/differential/active/epriestley/` (that is, put a username in the URL) can't generate search links now.
# Humans can't edit the URL anymore, either.
I think (1) is an actual issue, and this fixes it. I think (2) is pretty fluff, and this doesn't really try to fix it, although it probably improves it.
The fix for (1) is:
- Provide a helper to read a parameter containing either a list of user PHIDs or a list of usernames, so `/?users[]=PHID-USER-xyz` (from a tokenizer) and `/?users=alincoln,htaft` (from an external program) are equivalent inputs.
- Rename all the form parameters to be more digestable (`authorPHIDs` -> `authors`). Almost all of them were in this form already anyway. This just gives us `?users=alincoln` instead of `userPHIDs=alincoln`.
- Inside ApplicationSearch, if a request has no query associated with it but does have query parameters, build a query from the request instead of issuing the user's default query. Basically, this means that `/differential/` runs the default query, while `/differential/?users=x` runs a custom query.
Test Plan: {F56612}
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T2625, T3775
Differential Revision: https://secure.phabricator.com/D6840
2013-08-29 20:52:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($names) {
|
|
|
|
$users = id(new PhabricatorPeopleQuery())
|
|
|
|
->setViewer($this->requireViewer())
|
|
|
|
->withUsernames($names)
|
|
|
|
->execute();
|
|
|
|
foreach ($users as $user) {
|
|
|
|
$phids[] = $user->getPHID();
|
|
|
|
}
|
|
|
|
$phids = array_unique($phids);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $phids;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-04-19 16:14:28 +02:00
|
|
|
/**
|
|
|
|
* Read a list of project PHIDs from a request in a flexible way.
|
|
|
|
*
|
|
|
|
* @param AphrontRequest Request to read user PHIDs from.
|
|
|
|
* @param string Key to read in the request.
|
|
|
|
* @return list<phid> List of projet PHIDs and selector functions.
|
|
|
|
* @task read
|
|
|
|
*/
|
|
|
|
protected function readProjectsFromRequest(AphrontRequest $request, $key) {
|
|
|
|
$list = $this->readListFromRequest($request, $key);
|
|
|
|
|
|
|
|
$phids = array();
|
|
|
|
$slugs = array();
|
|
|
|
$project_type = PhabricatorProjectProjectPHIDType::TYPECONST;
|
|
|
|
foreach ($list as $item) {
|
|
|
|
$type = phid_get_type($item);
|
|
|
|
if ($type == $project_type) {
|
|
|
|
$phids[] = $item;
|
|
|
|
} else {
|
|
|
|
if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) {
|
|
|
|
// If this is a function, pass it through unchanged; we'll evaluate
|
|
|
|
// it later.
|
|
|
|
$phids[] = $item;
|
|
|
|
} else {
|
|
|
|
$slugs[] = $item;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($slugs) {
|
|
|
|
$projects = id(new PhabricatorProjectQuery())
|
|
|
|
->setViewer($this->requireViewer())
|
|
|
|
->withSlugs($slugs)
|
|
|
|
->execute();
|
|
|
|
foreach ($projects as $project) {
|
|
|
|
$phids[] = $project->getPHID();
|
|
|
|
}
|
|
|
|
$phids = array_unique($phids);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $phids;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Read a list of subscribers from a request in a flexible way.
|
|
|
|
*
|
|
|
|
* @param AphrontRequest Request to read PHIDs from.
|
|
|
|
* @param string Key to read in the request.
|
|
|
|
* @return list<phid> List of object PHIDs.
|
|
|
|
* @task read
|
|
|
|
*/
|
|
|
|
protected function readSubscribersFromRequest(
|
|
|
|
AphrontRequest $request,
|
|
|
|
$key) {
|
|
|
|
return $this->readUsersFromRequest(
|
|
|
|
$request,
|
|
|
|
$key,
|
|
|
|
array(
|
|
|
|
PhabricatorProjectProjectPHIDType::TYPECONST,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-12-05 20:59:33 +01:00
|
|
|
/**
|
|
|
|
* Read a list of generic PHIDs from a request in a flexible way. Like
|
|
|
|
* @{method:readUsersFromRequest}, this method supports either array or
|
|
|
|
* comma-delimited forms. Objects can be specified either by PHID or by
|
|
|
|
* object name.
|
|
|
|
*
|
|
|
|
* @param AphrontRequest Request to read PHIDs from.
|
|
|
|
* @param string Key to read in the request.
|
|
|
|
* @param list<const> Optional, list of permitted PHID types.
|
|
|
|
* @return list<phid> List of object PHIDs.
|
|
|
|
*
|
|
|
|
* @task read
|
|
|
|
*/
|
|
|
|
protected function readPHIDsFromRequest(
|
|
|
|
AphrontRequest $request,
|
|
|
|
$key,
|
|
|
|
array $allow_types = array()) {
|
|
|
|
|
2013-12-26 19:40:22 +01:00
|
|
|
$list = $this->readListFromRequest($request, $key);
|
2013-12-05 20:59:33 +01:00
|
|
|
|
|
|
|
$objects = id(new PhabricatorObjectQuery())
|
|
|
|
->setViewer($this->requireViewer())
|
|
|
|
->withNames($list)
|
|
|
|
->execute();
|
|
|
|
$list = mpull($objects, 'getPHID');
|
|
|
|
|
|
|
|
if (!$list) {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
|
|
|
// If only certain PHID types are allowed, filter out all the others.
|
|
|
|
if ($allow_types) {
|
|
|
|
$allow_types = array_fuse($allow_types);
|
|
|
|
foreach ($list as $key => $phid) {
|
|
|
|
if (empty($allow_types[phid_get_type($phid)])) {
|
|
|
|
unset($list[$key]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $list;
|
|
|
|
}
|
|
|
|
|
2013-12-26 19:40:22 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Read a list of items from the request, in either array format or string
|
|
|
|
* format:
|
|
|
|
*
|
|
|
|
* list[]=item1&list[]=item2
|
|
|
|
* list=item1,item2
|
|
|
|
*
|
|
|
|
* This provides flexibility when constructing URIs, especially from external
|
|
|
|
* sources.
|
|
|
|
*
|
2014-02-03 21:52:19 +01:00
|
|
|
* @param AphrontRequest Request to read strings from.
|
2013-12-26 19:40:22 +01:00
|
|
|
* @param string Key to read in the request.
|
|
|
|
* @return list<string> List of values.
|
|
|
|
*/
|
|
|
|
protected function readListFromRequest(
|
|
|
|
AphrontRequest $request,
|
|
|
|
$key) {
|
|
|
|
$list = $request->getArr($key, null);
|
|
|
|
if ($list === null) {
|
|
|
|
$list = $request->getStrList($key);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$list) {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $list;
|
|
|
|
}
|
|
|
|
|
2014-02-06 19:10:18 +01:00
|
|
|
protected function readDateFromRequest(
|
|
|
|
AphrontRequest $request,
|
|
|
|
$key) {
|
|
|
|
|
2015-05-04 19:08:49 +02:00
|
|
|
$value = AphrontFormDateControlValue::newFromRequest($request, $key);
|
|
|
|
|
|
|
|
if ($value->isEmpty()) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $value->getDictionary();
|
2014-02-06 19:10:18 +01:00
|
|
|
}
|
|
|
|
|
2013-10-07 02:10:29 +02:00
|
|
|
protected function readBoolFromRequest(
|
|
|
|
AphrontRequest $request,
|
|
|
|
$key) {
|
|
|
|
if (!strlen($request->getStr($key))) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return $request->getBool($key);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) {
|
|
|
|
$value = $query->getParameter($key);
|
|
|
|
if ($value === null) {
|
|
|
|
return $value;
|
|
|
|
}
|
|
|
|
return $value ? 'true' : 'false';
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-05-31 02:32:12 +02:00
|
|
|
/* -( Dates )-------------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task dates
|
|
|
|
*/
|
|
|
|
protected function parseDateTime($date_time) {
|
|
|
|
if (!strlen($date_time)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2013-06-03 21:58:11 +02:00
|
|
|
return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer());
|
2013-05-31 02:32:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @task dates
|
|
|
|
*/
|
|
|
|
protected function buildDateRange(
|
|
|
|
AphrontFormView $form,
|
|
|
|
PhabricatorSavedQuery $saved_query,
|
|
|
|
$start_key,
|
|
|
|
$start_name,
|
|
|
|
$end_key,
|
|
|
|
$end_name) {
|
|
|
|
|
|
|
|
$start_str = $saved_query->getParameter($start_key);
|
|
|
|
$start = null;
|
|
|
|
if (strlen($start_str)) {
|
|
|
|
$start = $this->parseDateTime($start_str);
|
|
|
|
if (!$start) {
|
|
|
|
$this->addError(
|
|
|
|
pht(
|
|
|
|
'"%s" date can not be parsed.',
|
|
|
|
$start_name));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$end_str = $saved_query->getParameter($end_key);
|
|
|
|
$end = null;
|
|
|
|
if (strlen($end_str)) {
|
|
|
|
$end = $this->parseDateTime($end_str);
|
|
|
|
if (!$end) {
|
|
|
|
$this->addError(
|
|
|
|
pht(
|
|
|
|
'"%s" date can not be parsed.',
|
|
|
|
$end_name));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($start && $end && ($start >= $end)) {
|
|
|
|
$this->addError(
|
|
|
|
pht(
|
|
|
|
'"%s" must be a date before "%s".',
|
|
|
|
$start_name,
|
|
|
|
$end_name));
|
|
|
|
}
|
|
|
|
|
|
|
|
$form
|
|
|
|
->appendChild(
|
|
|
|
id(new PHUIFormFreeformDateControl())
|
|
|
|
->setName($start_key)
|
|
|
|
->setLabel($start_name)
|
|
|
|
->setValue($start_str))
|
|
|
|
->appendChild(
|
|
|
|
id(new AphrontFormTextControl())
|
|
|
|
->setName($end_key)
|
|
|
|
->setLabel($end_name)
|
|
|
|
->setValue($end_str));
|
|
|
|
}
|
2013-07-01 21:36:34 +02:00
|
|
|
|
|
|
|
|
2014-05-08 17:24:47 +02:00
|
|
|
/* -( Paging and Executing Queries )--------------------------------------- */
|
2015-04-16 16:43:13 +02:00
|
|
|
|
|
|
|
|
2013-07-01 21:36:34 +02:00
|
|
|
public function getPageSize(PhabricatorSavedQuery $saved) {
|
2015-06-09 22:17:31 +02:00
|
|
|
$limit = (int)$saved->getParameter('limit');
|
2015-04-16 16:43:13 +02:00
|
|
|
|
2015-06-09 22:17:31 +02:00
|
|
|
if ($limit > 0) {
|
|
|
|
return $limit;
|
2015-04-16 16:43:13 +02:00
|
|
|
}
|
|
|
|
|
2015-06-09 22:17:31 +02:00
|
|
|
return 100;
|
2013-07-01 21:36:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2014-05-08 17:24:47 +02:00
|
|
|
public function shouldUseOffsetPaging() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) {
|
|
|
|
if ($this->shouldUseOffsetPaging()) {
|
2015-06-02 23:34:04 +02:00
|
|
|
$pager = new PHUIPagerView();
|
2014-05-08 17:24:47 +02:00
|
|
|
} else {
|
|
|
|
$pager = new AphrontCursorPagerView();
|
|
|
|
}
|
|
|
|
|
|
|
|
$page_size = $this->getPageSize($saved);
|
|
|
|
if (is_finite($page_size)) {
|
|
|
|
$pager->setPageSize($page_size);
|
|
|
|
} else {
|
|
|
|
// Consider an INF pagesize to mean a large finite pagesize.
|
|
|
|
|
|
|
|
// TODO: It would be nice to handle this more gracefully, but math
|
|
|
|
// with INF seems to vary across PHP versions, systems, and runtimes.
|
|
|
|
$pager->setPageSize(0xFFFF);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $pager;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public function executeQuery(
|
|
|
|
PhabricatorPolicyAwareQuery $query,
|
|
|
|
AphrontView $pager) {
|
|
|
|
|
|
|
|
$query->setViewer($this->requireViewer());
|
|
|
|
|
|
|
|
if ($this->shouldUseOffsetPaging()) {
|
|
|
|
$objects = $query->executeWithOffsetPager($pager);
|
|
|
|
} else {
|
|
|
|
$objects = $query->executeWithCursorPager($pager);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $objects;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* -( Rendering )---------------------------------------------------------- */
|
|
|
|
|
|
|
|
|
2014-05-08 19:08:37 +02:00
|
|
|
public function setRequest(AphrontRequest $request) {
|
|
|
|
$this->request = $request;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getRequest() {
|
|
|
|
return $this->request;
|
|
|
|
}
|
|
|
|
|
2014-05-08 17:24:47 +02:00
|
|
|
public function renderResults(
|
|
|
|
array $objects,
|
|
|
|
PhabricatorSavedQuery $query) {
|
2014-05-08 17:40:59 +02:00
|
|
|
|
|
|
|
$phids = $this->getRequiredHandlePHIDsForResultList($objects, $query);
|
|
|
|
|
|
|
|
if ($phids) {
|
|
|
|
$handles = id(new PhabricatorHandleQuery())
|
|
|
|
->setViewer($this->requireViewer())
|
|
|
|
->witHPHIDs($phids)
|
|
|
|
->execute();
|
|
|
|
} else {
|
|
|
|
$handles = array();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->renderResultList($objects, $query, $handles);
|
|
|
|
}
|
|
|
|
|
2014-05-08 17:49:32 +02:00
|
|
|
protected function getRequiredHandlePHIDsForResultList(
|
2014-05-08 17:40:59 +02:00
|
|
|
array $objects,
|
|
|
|
PhabricatorSavedQuery $query) {
|
|
|
|
return array();
|
|
|
|
}
|
|
|
|
|
2015-07-08 17:10:58 +02:00
|
|
|
abstract protected function renderResultList(
|
2014-05-08 17:40:59 +02:00
|
|
|
array $objects,
|
|
|
|
PhabricatorSavedQuery $query,
|
2015-07-08 17:10:58 +02:00
|
|
|
array $handles);
|
2014-05-08 17:24:47 +02:00
|
|
|
|
|
|
|
|
Integrate ApplicationSearch with CustomField
Summary:
Ref T2625. Ref T3794. Ref T418. Ref T1703.
This is a more general version of D5278. It expands CustomField support to include real integration with ApplicationSearch.
Broadly, custom fields may elect to:
- build indicies when objects are updated;
- populate ApplicationSearch forms with new controls;
- read inputs entered into those controls out of the request; and
- apply constraints to search queries.
Some utility/helper stuff is provided to make this easier. This part could be cleaner, but seems reasonable for a first cut. In particular, the Query and SearchEngine must manually call all the hooks right now instead of everything happening magically. I think that's fine for the moment; they're pretty easy to get right.
Test Plan:
I added a new searchable "Company" field to People:
{F58229}
This also cleaned up the disable/reorder view a little bit:
{F58230}
As it did before, this field appears on the edit screen:
{F58231}
However, because it has `search`, it also appears on the search screen:
{F58232}
When queried, it returns the expected results:
{F58233}
And the actually good bit of all this is that the query can take advantage of indexes:
mysql> explain SELECT * FROM `user` user JOIN `user_customfieldstringindex` `appsearch_0` ON `appsearch_0`.objectPHID = user.phid AND `appsearch_0`.indexKey = 'mk3Ndy476ge6' AND `appsearch_0`.indexValue IN ('phacility') ORDER BY user.id DESC LIMIT 101;
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| 1 | SIMPLE | appsearch_0 | ref | key_join,key_find | key_find | 232 | const,const | 1 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | user | eq_ref | phid | phid | 194 | phabricator2_user.appsearch_0.objectPHID | 1 | |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
2 rows in set (0.00 sec)
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T418, T1703, T2625, T3794
Differential Revision: https://secure.phabricator.com/D6992
2013-09-16 22:44:34 +02:00
|
|
|
/* -( Application Search )------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieve an object to use to define custom fields for this search.
|
|
|
|
*
|
|
|
|
* To integrate with custom fields, subclasses should override this method
|
|
|
|
* and return an instance of the application object which implements
|
|
|
|
* @{interface:PhabricatorCustomFieldInterface}.
|
|
|
|
*
|
|
|
|
* @return PhabricatorCustomFieldInterface|null Object with custom fields.
|
|
|
|
* @task appsearch
|
|
|
|
*/
|
|
|
|
public function getCustomFieldObject() {
|
2015-06-05 20:21:45 +02:00
|
|
|
$object = $this->newResultObject();
|
|
|
|
if ($object instanceof PhabricatorCustomFieldInterface) {
|
|
|
|
return $object;
|
|
|
|
}
|
Integrate ApplicationSearch with CustomField
Summary:
Ref T2625. Ref T3794. Ref T418. Ref T1703.
This is a more general version of D5278. It expands CustomField support to include real integration with ApplicationSearch.
Broadly, custom fields may elect to:
- build indicies when objects are updated;
- populate ApplicationSearch forms with new controls;
- read inputs entered into those controls out of the request; and
- apply constraints to search queries.
Some utility/helper stuff is provided to make this easier. This part could be cleaner, but seems reasonable for a first cut. In particular, the Query and SearchEngine must manually call all the hooks right now instead of everything happening magically. I think that's fine for the moment; they're pretty easy to get right.
Test Plan:
I added a new searchable "Company" field to People:
{F58229}
This also cleaned up the disable/reorder view a little bit:
{F58230}
As it did before, this field appears on the edit screen:
{F58231}
However, because it has `search`, it also appears on the search screen:
{F58232}
When queried, it returns the expected results:
{F58233}
And the actually good bit of all this is that the query can take advantage of indexes:
mysql> explain SELECT * FROM `user` user JOIN `user_customfieldstringindex` `appsearch_0` ON `appsearch_0`.objectPHID = user.phid AND `appsearch_0`.indexKey = 'mk3Ndy476ge6' AND `appsearch_0`.indexValue IN ('phacility') ORDER BY user.id DESC LIMIT 101;
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| 1 | SIMPLE | appsearch_0 | ref | key_join,key_find | key_find | 232 | const,const | 1 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | user | eq_ref | phid | phid | 194 | phabricator2_user.appsearch_0.objectPHID | 1 | |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
2 rows in set (0.00 sec)
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T418, T1703, T2625, T3794
Differential Revision: https://secure.phabricator.com/D6992
2013-09-16 22:44:34 +02:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the custom fields for this search.
|
|
|
|
*
|
|
|
|
* @return PhabricatorCustomFieldList|null Custom fields, if this search
|
|
|
|
* supports custom fields.
|
|
|
|
* @task appsearch
|
|
|
|
*/
|
|
|
|
public function getCustomFieldList() {
|
|
|
|
if ($this->customFields === false) {
|
|
|
|
$object = $this->getCustomFieldObject();
|
|
|
|
if ($object) {
|
|
|
|
$fields = PhabricatorCustomField::getObjectFields(
|
|
|
|
$object,
|
|
|
|
PhabricatorCustomField::ROLE_APPLICATIONSEARCH);
|
2014-03-25 22:02:18 +01:00
|
|
|
$fields->setViewer($this->requireViewer());
|
Integrate ApplicationSearch with CustomField
Summary:
Ref T2625. Ref T3794. Ref T418. Ref T1703.
This is a more general version of D5278. It expands CustomField support to include real integration with ApplicationSearch.
Broadly, custom fields may elect to:
- build indicies when objects are updated;
- populate ApplicationSearch forms with new controls;
- read inputs entered into those controls out of the request; and
- apply constraints to search queries.
Some utility/helper stuff is provided to make this easier. This part could be cleaner, but seems reasonable for a first cut. In particular, the Query and SearchEngine must manually call all the hooks right now instead of everything happening magically. I think that's fine for the moment; they're pretty easy to get right.
Test Plan:
I added a new searchable "Company" field to People:
{F58229}
This also cleaned up the disable/reorder view a little bit:
{F58230}
As it did before, this field appears on the edit screen:
{F58231}
However, because it has `search`, it also appears on the search screen:
{F58232}
When queried, it returns the expected results:
{F58233}
And the actually good bit of all this is that the query can take advantage of indexes:
mysql> explain SELECT * FROM `user` user JOIN `user_customfieldstringindex` `appsearch_0` ON `appsearch_0`.objectPHID = user.phid AND `appsearch_0`.indexKey = 'mk3Ndy476ge6' AND `appsearch_0`.indexValue IN ('phacility') ORDER BY user.id DESC LIMIT 101;
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| 1 | SIMPLE | appsearch_0 | ref | key_join,key_find | key_find | 232 | const,const | 1 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | user | eq_ref | phid | phid | 194 | phabricator2_user.appsearch_0.objectPHID | 1 | |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
2 rows in set (0.00 sec)
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T418, T1703, T2625, T3794
Differential Revision: https://secure.phabricator.com/D6992
2013-09-16 22:44:34 +02:00
|
|
|
} else {
|
|
|
|
$fields = null;
|
|
|
|
}
|
|
|
|
$this->customFields = $fields;
|
|
|
|
}
|
|
|
|
return $this->customFields;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Applies data from a saved query to an executable query.
|
|
|
|
*
|
|
|
|
* @param PhabricatorCursorPagedPolicyAwareQuery Query to constrain.
|
|
|
|
* @param PhabricatorSavedQuery Saved query to read.
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
protected function applyCustomFieldsToQuery(
|
|
|
|
PhabricatorCursorPagedPolicyAwareQuery $query,
|
|
|
|
PhabricatorSavedQuery $saved) {
|
|
|
|
|
|
|
|
$list = $this->getCustomFieldList();
|
|
|
|
if (!$list) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($list->getFields() as $field) {
|
|
|
|
$value = $field->applyApplicationSearchConstraintToQuery(
|
|
|
|
$this,
|
|
|
|
$query,
|
2015-06-09 22:17:31 +02:00
|
|
|
$saved->getParameter('custom:'.$field->getFieldIndex()));
|
Integrate ApplicationSearch with CustomField
Summary:
Ref T2625. Ref T3794. Ref T418. Ref T1703.
This is a more general version of D5278. It expands CustomField support to include real integration with ApplicationSearch.
Broadly, custom fields may elect to:
- build indicies when objects are updated;
- populate ApplicationSearch forms with new controls;
- read inputs entered into those controls out of the request; and
- apply constraints to search queries.
Some utility/helper stuff is provided to make this easier. This part could be cleaner, but seems reasonable for a first cut. In particular, the Query and SearchEngine must manually call all the hooks right now instead of everything happening magically. I think that's fine for the moment; they're pretty easy to get right.
Test Plan:
I added a new searchable "Company" field to People:
{F58229}
This also cleaned up the disable/reorder view a little bit:
{F58230}
As it did before, this field appears on the edit screen:
{F58231}
However, because it has `search`, it also appears on the search screen:
{F58232}
When queried, it returns the expected results:
{F58233}
And the actually good bit of all this is that the query can take advantage of indexes:
mysql> explain SELECT * FROM `user` user JOIN `user_customfieldstringindex` `appsearch_0` ON `appsearch_0`.objectPHID = user.phid AND `appsearch_0`.indexKey = 'mk3Ndy476ge6' AND `appsearch_0`.indexValue IN ('phacility') ORDER BY user.id DESC LIMIT 101;
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| 1 | SIMPLE | appsearch_0 | ref | key_join,key_find | key_find | 232 | const,const | 1 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | user | eq_ref | phid | phid | 194 | phabricator2_user.appsearch_0.objectPHID | 1 | |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
2 rows in set (0.00 sec)
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T418, T1703, T2625, T3794
Differential Revision: https://secure.phabricator.com/D6992
2013-09-16 22:44:34 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-08 21:20:35 +02:00
|
|
|
private function buildCustomFieldSearchFields() {
|
2014-08-02 10:22:16 +02:00
|
|
|
$list = $this->getCustomFieldList();
|
|
|
|
if (!$list) {
|
2015-06-08 21:20:16 +02:00
|
|
|
return array();
|
Integrate ApplicationSearch with CustomField
Summary:
Ref T2625. Ref T3794. Ref T418. Ref T1703.
This is a more general version of D5278. It expands CustomField support to include real integration with ApplicationSearch.
Broadly, custom fields may elect to:
- build indicies when objects are updated;
- populate ApplicationSearch forms with new controls;
- read inputs entered into those controls out of the request; and
- apply constraints to search queries.
Some utility/helper stuff is provided to make this easier. This part could be cleaner, but seems reasonable for a first cut. In particular, the Query and SearchEngine must manually call all the hooks right now instead of everything happening magically. I think that's fine for the moment; they're pretty easy to get right.
Test Plan:
I added a new searchable "Company" field to People:
{F58229}
This also cleaned up the disable/reorder view a little bit:
{F58230}
As it did before, this field appears on the edit screen:
{F58231}
However, because it has `search`, it also appears on the search screen:
{F58232}
When queried, it returns the expected results:
{F58233}
And the actually good bit of all this is that the query can take advantage of indexes:
mysql> explain SELECT * FROM `user` user JOIN `user_customfieldstringindex` `appsearch_0` ON `appsearch_0`.objectPHID = user.phid AND `appsearch_0`.indexKey = 'mk3Ndy476ge6' AND `appsearch_0`.indexValue IN ('phacility') ORDER BY user.id DESC LIMIT 101;
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| 1 | SIMPLE | appsearch_0 | ref | key_join,key_find | key_find | 232 | const,const | 1 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | user | eq_ref | phid | phid | 194 | phabricator2_user.appsearch_0.objectPHID | 1 | |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
2 rows in set (0.00 sec)
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T418, T1703, T2625, T3794
Differential Revision: https://secure.phabricator.com/D6992
2013-09-16 22:44:34 +02:00
|
|
|
}
|
|
|
|
|
2015-06-08 21:20:16 +02:00
|
|
|
$fields = array();
|
Integrate ApplicationSearch with CustomField
Summary:
Ref T2625. Ref T3794. Ref T418. Ref T1703.
This is a more general version of D5278. It expands CustomField support to include real integration with ApplicationSearch.
Broadly, custom fields may elect to:
- build indicies when objects are updated;
- populate ApplicationSearch forms with new controls;
- read inputs entered into those controls out of the request; and
- apply constraints to search queries.
Some utility/helper stuff is provided to make this easier. This part could be cleaner, but seems reasonable for a first cut. In particular, the Query and SearchEngine must manually call all the hooks right now instead of everything happening magically. I think that's fine for the moment; they're pretty easy to get right.
Test Plan:
I added a new searchable "Company" field to People:
{F58229}
This also cleaned up the disable/reorder view a little bit:
{F58230}
As it did before, this field appears on the edit screen:
{F58231}
However, because it has `search`, it also appears on the search screen:
{F58232}
When queried, it returns the expected results:
{F58233}
And the actually good bit of all this is that the query can take advantage of indexes:
mysql> explain SELECT * FROM `user` user JOIN `user_customfieldstringindex` `appsearch_0` ON `appsearch_0`.objectPHID = user.phid AND `appsearch_0`.indexKey = 'mk3Ndy476ge6' AND `appsearch_0`.indexValue IN ('phacility') ORDER BY user.id DESC LIMIT 101;
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| 1 | SIMPLE | appsearch_0 | ref | key_join,key_find | key_find | 232 | const,const | 1 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | user | eq_ref | phid | phid | 194 | phabricator2_user.appsearch_0.objectPHID | 1 | |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
2 rows in set (0.00 sec)
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T418, T1703, T2625, T3794
Differential Revision: https://secure.phabricator.com/D6992
2013-09-16 22:44:34 +02:00
|
|
|
foreach ($list->getFields() as $field) {
|
2015-06-08 21:20:16 +02:00
|
|
|
$fields[] = id(new PhabricatorSearchCustomFieldProxyField())
|
|
|
|
->setSearchEngine($this)
|
|
|
|
->setCustomField($field);
|
Integrate ApplicationSearch with CustomField
Summary:
Ref T2625. Ref T3794. Ref T418. Ref T1703.
This is a more general version of D5278. It expands CustomField support to include real integration with ApplicationSearch.
Broadly, custom fields may elect to:
- build indicies when objects are updated;
- populate ApplicationSearch forms with new controls;
- read inputs entered into those controls out of the request; and
- apply constraints to search queries.
Some utility/helper stuff is provided to make this easier. This part could be cleaner, but seems reasonable for a first cut. In particular, the Query and SearchEngine must manually call all the hooks right now instead of everything happening magically. I think that's fine for the moment; they're pretty easy to get right.
Test Plan:
I added a new searchable "Company" field to People:
{F58229}
This also cleaned up the disable/reorder view a little bit:
{F58230}
As it did before, this field appears on the edit screen:
{F58231}
However, because it has `search`, it also appears on the search screen:
{F58232}
When queried, it returns the expected results:
{F58233}
And the actually good bit of all this is that the query can take advantage of indexes:
mysql> explain SELECT * FROM `user` user JOIN `user_customfieldstringindex` `appsearch_0` ON `appsearch_0`.objectPHID = user.phid AND `appsearch_0`.indexKey = 'mk3Ndy476ge6' AND `appsearch_0`.indexValue IN ('phacility') ORDER BY user.id DESC LIMIT 101;
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| 1 | SIMPLE | appsearch_0 | ref | key_join,key_find | key_find | 232 | const,const | 1 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | user | eq_ref | phid | phid | 194 | phabricator2_user.appsearch_0.objectPHID | 1 | |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
2 rows in set (0.00 sec)
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T418, T1703, T2625, T3794
Differential Revision: https://secure.phabricator.com/D6992
2013-09-16 22:44:34 +02:00
|
|
|
}
|
|
|
|
|
2015-06-09 22:17:31 +02:00
|
|
|
return $fields;
|
Integrate ApplicationSearch with CustomField
Summary:
Ref T2625. Ref T3794. Ref T418. Ref T1703.
This is a more general version of D5278. It expands CustomField support to include real integration with ApplicationSearch.
Broadly, custom fields may elect to:
- build indicies when objects are updated;
- populate ApplicationSearch forms with new controls;
- read inputs entered into those controls out of the request; and
- apply constraints to search queries.
Some utility/helper stuff is provided to make this easier. This part could be cleaner, but seems reasonable for a first cut. In particular, the Query and SearchEngine must manually call all the hooks right now instead of everything happening magically. I think that's fine for the moment; they're pretty easy to get right.
Test Plan:
I added a new searchable "Company" field to People:
{F58229}
This also cleaned up the disable/reorder view a little bit:
{F58230}
As it did before, this field appears on the edit screen:
{F58231}
However, because it has `search`, it also appears on the search screen:
{F58232}
When queried, it returns the expected results:
{F58233}
And the actually good bit of all this is that the query can take advantage of indexes:
mysql> explain SELECT * FROM `user` user JOIN `user_customfieldstringindex` `appsearch_0` ON `appsearch_0`.objectPHID = user.phid AND `appsearch_0`.indexKey = 'mk3Ndy476ge6' AND `appsearch_0`.indexValue IN ('phacility') ORDER BY user.id DESC LIMIT 101;
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
| 1 | SIMPLE | appsearch_0 | ref | key_join,key_find | key_find | 232 | const,const | 1 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | user | eq_ref | phid | phid | 194 | phabricator2_user.appsearch_0.objectPHID | 1 | |
+----+-------------+-------------+--------+-------------------+----------+---------+------------------------------------------+------+----------------------------------------------+
2 rows in set (0.00 sec)
Reviewers: btrahan
Reviewed By: btrahan
CC: aran
Maniphest Tasks: T418, T1703, T2625, T3794
Differential Revision: https://secure.phabricator.com/D6992
2013-09-16 22:44:34 +02:00
|
|
|
}
|
|
|
|
|
2013-04-14 15:53:20 +02:00
|
|
|
}
|