1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-18 12:52:42 +01:00

Support ordering in SearchField

Summary:
Ref T8441. Ref T7715. Automatically generate a modern "Order" control in ApplicationSearch for engines which fully support SearchField.

Notably, this allows the standard "Order" control to automatically support custom field orders. We do this in Maniphest today, but in an ad-hoc way.

Test Plan: Performed order-by queries in Almanac (Services), Pholio, Files, People, Projects, and Paste.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T7715, T8441

Differential Revision: https://secure.phabricator.com/D13193
This commit is contained in:
epriestley 2015-06-08 12:21:48 -07:00
parent 3cdaf52ce9
commit bf87976d25
17 changed files with 229 additions and 154 deletions

View file

@ -2520,6 +2520,7 @@ phutil_register_library_map(array(
'PhabricatorSearchManagementInitWorkflow' => 'applications/search/management/PhabricatorSearchManagementInitWorkflow.php',
'PhabricatorSearchManagementWorkflow' => 'applications/search/management/PhabricatorSearchManagementWorkflow.php',
'PhabricatorSearchOrderController' => 'applications/search/controller/PhabricatorSearchOrderController.php',
'PhabricatorSearchOrderField' => 'applications/search/field/PhabricatorSearchOrderField.php',
'PhabricatorSearchOwnersField' => 'applications/search/field/PhabricatorSearchOwnersField.php',
'PhabricatorSearchPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorSearchPreferencesSettingsPanel.php',
'PhabricatorSearchProjectsField' => 'applications/search/field/PhabricatorSearchProjectsField.php',
@ -6024,6 +6025,7 @@ phutil_register_library_map(array(
'PhabricatorSearchManagementInitWorkflow' => 'PhabricatorSearchManagementWorkflow',
'PhabricatorSearchManagementWorkflow' => 'PhabricatorManagementWorkflow',
'PhabricatorSearchOrderController' => 'PhabricatorSearchBaseController',
'PhabricatorSearchOrderField' => 'PhabricatorSearchField',
'PhabricatorSearchOwnersField' => 'PhabricatorSearchTokenizerField',
'PhabricatorSearchPreferencesSettingsPanel' => 'PhabricatorSettingsPanel',
'PhabricatorSearchProjectsField' => 'PhabricatorSearchTokenizerField',

View file

@ -60,47 +60,35 @@ final class AlmanacServiceQuery
}
protected function loadPage() {
$table = new AlmanacService();
$conn_r = $table->establishConnection('r');
$data = queryfx_all(
$conn_r,
'SELECT service.* FROM %T service %Q %Q %Q %Q',
$table->getTableName(),
$this->buildJoinClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $table->loadAllFromArray($data);
return $this->loadStandardPage(new AlmanacService());
}
protected function buildJoinClause(AphrontDatabaseConnection $conn_r) {
$joins = array();
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->devicePHIDs !== null) {
$joins[] = qsprintf(
$conn_r,
$conn,
'JOIN %T binding ON service.phid = binding.servicePHID',
id(new AlmanacBinding())->getTableName());
}
return implode(' ', $joins);
return $joins;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'service.id IN (%Ld)',
$this->ids);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'service.phid IN (%Ls)',
$this->phids);
}
@ -112,49 +100,47 @@ final class AlmanacServiceQuery
}
$where[] = qsprintf(
$conn_r,
$conn,
'service.nameIndex IN (%Ls)',
$hashes);
}
if ($this->serviceClasses !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'service.serviceClass IN (%Ls)',
$this->serviceClasses);
}
if ($this->devicePHIDs !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'binding.devicePHID IN (%Ls)',
$this->devicePHIDs);
}
if ($this->locked !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'service.isLocked = %d',
(int)$this->locked);
}
if ($this->namePrefix !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'service.name LIKE %>',
$this->namePrefix);
}
if ($this->nameSuffix !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'service.name LIKE %<',
$this->nameSuffix);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
return $where;
}
protected function willFilterPage(array $services) {
@ -192,10 +178,14 @@ final class AlmanacServiceQuery
return parent::didFilterPage($services);
}
public function getPrimaryTableAlias() {
return 'service';
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => 'service',
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',
'type' => 'string',
'unique' => true,

View file

@ -11,30 +11,26 @@ final class AlmanacServiceSearchEngine
return 'PhabricatorAlmanacApplication';
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$this->saveQueryOrder($saved, $request);
return $saved;
public function newQuery() {
return new AlmanacServiceQuery();
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new AlmanacServiceQuery());
public function newResultObject() {
// NOTE: We need to attach a service type in order to generate custom
// field definitions.
return AlmanacService::initializeNewService()
->attachServiceType(new AlmanacCustomServiceType());
}
$this->setQueryOrder($query, $saved);
protected function buildQueryFromParameters(array $map) {
$query = $this->newQuery();
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved) {
$this->appendOrderFieldsToForm(
$form,
$saved,
new AlmanacServiceQuery());
protected function buildCustomSearchFields() {
return array();
}
protected function getURI($path) {
@ -62,12 +58,6 @@ final class AlmanacServiceSearchEngine
return parent::buildSavedQueryFromBuiltin($query_key);
}
protected function getRequiredHandlePHIDsForResultList(
array $services,
PhabricatorSavedQuery $query) {
return array();
}
protected function renderResultList(
array $services,
PhabricatorSavedQuery $query,

View file

@ -117,6 +117,10 @@ final class PhabricatorFileQuery
return $this;
}
public function newResultObject() {
return new PhabricatorFile();
}
protected function loadPage() {
$files = $this->loadStandardPage(new PhabricatorFile());

View file

@ -11,8 +11,8 @@ final class PhabricatorFileSearchEngine
return 'PhabricatorFilesApplication';
}
public function newResultObject() {
return new PhabricatorFile();
public function newQuery() {
return new PhabricatorFileQuery();
}
protected function buildCustomSearchFields() {
@ -46,7 +46,7 @@ final class PhabricatorFileSearchEngine
}
public function buildQueryFromParameters(array $map) {
$query = id(new PhabricatorFileQuery());
$query = $this->newQuery();
if ($map['authorPHIDs']) {
$query->withAuthorPHIDs($map['authorPHIDs']);

View file

@ -193,7 +193,7 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery {
return $this;
}
protected function newResultObject() {
public function newResultObject() {
return new ManiphestTask();
}

View file

@ -67,7 +67,7 @@ final class PhabricatorPasteQuery
return $this;
}
protected function newResultObject() {
public function newResultObject() {
return new PhabricatorPaste();
}

View file

@ -11,13 +11,13 @@ final class PhabricatorPasteSearchEngine
return 'PhabricatorPasteApplication';
}
public function newResultObject() {
return new PhabricatorPaste();
public function newQuery() {
return id(new PhabricatorPasteQuery())
->needContent(true);
}
protected function buildQueryFromParameters(array $map) {
$query = id(new PhabricatorPasteQuery())
->needContent(true);
$query = $this->newQuery();
if ($map['authorPHIDs']) {
$query->withAuthorPHIDs($map['authorPHIDs']);

View file

@ -113,19 +113,13 @@ final class PhabricatorPeopleQuery
return $this;
}
protected function loadPage() {
$table = new PhabricatorUser();
$conn_r = $table->establishConnection('r');
public function newResultObject() {
return new PhabricatorUser();
}
$data = queryfx_all(
$conn_r,
'SELECT * FROM %T user %Q %Q %Q %Q %Q',
$table->getTableName(),
$this->buildJoinsClause($conn_r),
$this->buildWhereClause($conn_r),
$this->buildGroupClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
protected function loadPage() {
$table = new PhabricatorUser();
$data = $this->loadStandardPageRows($table);
if ($this->needPrimaryEmail) {
$table->putInSet(new LiskDAOSet());
@ -225,23 +219,21 @@ final class PhabricatorPeopleQuery
return $users;
}
protected function buildGroupClause(AphrontDatabaseConnection $conn) {
protected function shouldGroupQueryResultRows() {
if ($this->nameTokens) {
return qsprintf(
$conn,
'GROUP BY user.id');
} else {
return $this->buildApplicationSearchGroupClause($conn);
return true;
}
return parent::shouldGroupQueryResultRows();
}
private function buildJoinsClause($conn_r) {
$joins = array();
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
$joins = parent::buildJoinClauseParts($conn);
if ($this->emails) {
$email_table = new PhabricatorUserEmail();
$joins[] = qsprintf(
$conn_r,
$conn,
'JOIN %T email ON email.userPHID = user.PHID',
$email_table->getTableName());
}
@ -250,7 +242,7 @@ final class PhabricatorPeopleQuery
foreach ($this->nameTokens as $key => $token) {
$token_table = 'token_'.$key;
$joins[] = qsprintf(
$conn_r,
$conn,
'JOIN %T %T ON %T.userID = user.id AND %T.token LIKE %>',
PhabricatorUser::NAMETOKEN_TABLE,
$token_table,
@ -260,110 +252,105 @@ final class PhabricatorPeopleQuery
}
}
$joins[] = $this->buildApplicationSearchJoinClause($conn_r);
$joins = implode(' ', $joins);
return $joins;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
$where = parent::buildWhereClauseParts($conn);
if ($this->usernames !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'user.userName IN (%Ls)',
$this->usernames);
}
if ($this->emails !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'email.address IN (%Ls)',
$this->emails);
}
if ($this->realnames !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'user.realName IN (%Ls)',
$this->realnames);
}
if ($this->phids !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'user.phid IN (%Ls)',
$this->phids);
}
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'user.id IN (%Ld)',
$this->ids);
}
if ($this->dateCreatedAfter) {
$where[] = qsprintf(
$conn_r,
$conn,
'user.dateCreated >= %d',
$this->dateCreatedAfter);
}
if ($this->dateCreatedBefore) {
$where[] = qsprintf(
$conn_r,
$conn,
'user.dateCreated <= %d',
$this->dateCreatedBefore);
}
if ($this->isAdmin !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'user.isAdmin = %d',
(int)$this->isAdmin);
}
if ($this->isDisabled !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'user.isDisabled = %d',
(int)$this->isDisabled);
}
if ($this->isApproved !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'user.isApproved = %d',
(int)$this->isApproved);
}
if ($this->isSystemAgent !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'user.isSystemAgent = %d',
(int)$this->isSystemAgent);
}
if ($this->isMailingList !== null) {
$where[] = qsprintf(
$conn_r,
$conn,
'user.isMailingList = %d',
(int)$this->isMailingList);
}
if (strlen($this->nameLike)) {
$where[] = qsprintf(
$conn_r,
$conn,
'user.username LIKE %~ OR user.realname LIKE %~',
$this->nameLike,
$this->nameLike);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
return $where;
}
protected function getPrimaryTableAlias() {

View file

@ -11,8 +11,10 @@ final class PhabricatorPeopleSearchEngine
return 'PhabricatorPeopleApplication';
}
public function newResultObject() {
return new PhabricatorUser();
public function newQuery() {
return id(new PhabricatorPeopleQuery())
->needPrimaryEmail(true)
->needProfileImage(true);
}
protected function buildCustomSearchFields() {
@ -77,9 +79,7 @@ final class PhabricatorPeopleSearchEngine
}
public function buildQueryFromParameters(array $map) {
$query = id(new PhabricatorPeopleQuery())
->needPrimaryEmail(true)
->needProfileImage(true);
$query = $this->newQuery();
$viewer = $this->requireViewer();

View file

@ -53,6 +53,10 @@ final class PholioMockQuery
return $this;
}
public function newResultObject() {
return new PholioMock();
}
protected function loadPage() {
$mocks = $this->loadStandardPage(new PholioMock());

View file

@ -10,11 +10,14 @@ final class PholioMockSearchEngine extends PhabricatorApplicationSearchEngine {
return 'PhabricatorPholioApplication';
}
public function newResultObject() {
return new PholioMock();
public function newQuery() {
return id(new PholioMockQuery())
->needCoverFiles(true)
->needImages(true)
->needTokenCounts(true);
}
public function buildCustomSearchFields() {
protected function buildCustomSearchFields() {
return array(
id(new PhabricatorSearchUsersField())
->setKey('authorPHIDs')
@ -30,10 +33,7 @@ final class PholioMockSearchEngine extends PhabricatorApplicationSearchEngine {
}
public function buildQueryFromParameters(array $map) {
$query = id(new PholioMockQuery())
->needCoverFiles(true)
->needImages(true)
->needTokenCounts(true);
$query = $this->newQuery();
if ($map['authorPHIDs']) {
$query->withAuthorPHIDs($map['authorPHIDs']);

View file

@ -95,12 +95,25 @@ final class PhabricatorProjectQuery
return $this;
}
public function newResultObject() {
return new PhabricatorProject();
}
protected function getDefaultOrderVector() {
return array('name');
}
public function getOrderableColumns() {
public function getBuiltinOrders() {
return array(
'name' => array(
'vector' => array('name'),
'name' => pht('Name'),
),
) + parent::getBuiltinOrders();
}
public function getOrderableColumns() {
return parent::getOrderableColumns() + array(
'name' => array(
'table' => $this->getPrimaryTableAlias(),
'column' => 'name',

View file

@ -11,8 +11,9 @@ final class PhabricatorProjectSearchEngine
return 'PhabricatorProjectApplication';
}
public function newResultObject() {
return new PhabricatorProject();
public function newQuery() {
return id(new PhabricatorProjectQuery())
->needImages(true);
}
protected function buildCustomSearchFields() {
@ -41,8 +42,7 @@ final class PhabricatorProjectSearchEngine
public function buildQueryFromParameters(array $map) {
$query = id(new PhabricatorProjectQuery())
->needImages(true);
$query = $this->newQuery();
if (strlen($map['name'])) {
$tokens = PhabricatorTypeaheadDatasource::tokenizeString($map['name']);

View file

@ -27,6 +27,19 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject {
const CONTEXT_PANEL = 'panel';
public function newResultObject() {
// 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() {
return null;
}
@ -98,15 +111,15 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject {
$fields = $this->buildSearchFields();
$viewer = $this->requireViewer();
$parameters = array();
$map = array();
foreach ($fields as $field) {
$field->setViewer($viewer);
$field->readValueFromSavedQuery($saved);
$value = $field->getValueForQuery($field->getValue());
$parameters[$field->getKey()] = $value;
$map[$field->getKey()] = $value;
}
$query = $this->buildQueryFromParameters($parameters);
$query = $this->buildQueryFromParameters($map);
$object = $this->newResultObject();
if (!$object) {
@ -114,25 +127,25 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject {
}
if ($object instanceof PhabricatorSubscribableInterface) {
if (!empty($parameters['subscriberPHIDs'])) {
if (!empty($map['subscriberPHIDs'])) {
$query->withEdgeLogicPHIDs(
PhabricatorObjectHasSubscriberEdgeType::EDGECONST,
PhabricatorQueryConstraint::OPERATOR_OR,
$parameters['subscriberPHIDs']);
$map['subscriberPHIDs']);
}
}
if ($object instanceof PhabricatorProjectInterface) {
if (!empty($parameters['projectPHIDs'])) {
if (!empty($map['projectPHIDs'])) {
$query->withEdgeLogicConstraints(
PhabricatorProjectObjectHasProjectEdgeType::EDGECONST,
$parameters['projectPHIDs']);
$map['projectPHIDs']);
}
}
if ($object instanceof PhabricatorSpacesInterface) {
if (!empty($parameters['spacePHIDs'])) {
$query->withSpacePHIDs($parameters['spacePHIDs']);
if (!empty($map['spacePHIDs'])) {
$query->withSpacePHIDs($map['spacePHIDs']);
}
}
@ -140,6 +153,8 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject {
$this->applyCustomFieldsToQuery($query, $saved);
}
$this->setQueryOrder($query, $saved);
return $query;
}
@ -185,30 +200,28 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject {
}
$object = $this->newResultObject();
if (!$object) {
return $fields;
}
if ($object) {
if ($object instanceof PhabricatorSubscribableInterface) {
$fields[] = id(new PhabricatorSearchSubscribersField())
->setLabel(pht('Subscribers'))
->setKey('subscriberPHIDs')
->setAliases(array('subscriber', 'subscribers'));
}
if ($object instanceof PhabricatorSubscribableInterface) {
$fields[] = id(new PhabricatorSearchSubscribersField())
->setLabel(pht('Subscribers'))
->setKey('subscriberPHIDs')
->setAliases(array('subscriber', 'subscribers'));
}
if ($object instanceof PhabricatorProjectInterface) {
$fields[] = id(new PhabricatorSearchProjectsField())
->setKey('projectPHIDs')
->setAliases(array('project', 'projects'))
->setLabel(pht('Projects'));
}
if ($object instanceof PhabricatorProjectInterface) {
$fields[] = id(new PhabricatorSearchProjectsField())
->setKey('projectPHIDs')
->setAliases(array('project', 'projects'))
->setLabel(pht('Projects'));
}
if ($object instanceof PhabricatorSpacesInterface) {
if (PhabricatorSpacesNamespaceQuery::getSpacesExist()) {
$fields[] = id(new PhabricatorSearchSpacesField())
->setKey('spacePHIDs')
->setAliases(array('space', 'spaces'))
->setLabel(pht('Spaces'));
if ($object instanceof PhabricatorSpacesInterface) {
if (PhabricatorSpacesNamespaceQuery::getSpacesExist()) {
$fields[] = id(new PhabricatorSearchSpacesField())
->setKey('spacePHIDs')
->setAliases(array('space', 'spaces'))
->setLabel(pht('Spaces'));
}
}
}
@ -216,6 +229,17 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject {
$fields[] = $custom_field;
}
$query = $this->newQuery();
if ($query) {
$orders = $query->getBuiltinOrders();
$orders = ipull($orders, 'name');
$fields[] = id(new PhabricatorSearchOrderField())
->setLabel(pht('Order'))
->setKey('order')
->setOptions($orders);
}
$field_map = array();
foreach ($fields as $field) {
$key = $field->getKey();
@ -890,6 +914,7 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject {
$order = $saved->getParameter('order');
$builtin = $query->getBuiltinOrders();
if (strlen($order) && isset($builtin[$order])) {
$query->setOrder($order);
} else {

View file

@ -0,0 +1,30 @@
<?php
final class PhabricatorSearchOrderField
extends PhabricatorSearchField {
private $options;
public function setOptions(array $options) {
$this->options = $options;
return $this;
}
public function getOptions() {
return $this->options;
}
protected function getDefaultValue() {
return null;
}
protected function getValueFromRequest(AphrontRequest $request, $key) {
return $request->getStr($key);
}
protected function newControl() {
return id(new AphrontFormSelectControl())
->setOptions($this->getOptions());
}
}

View file

@ -200,7 +200,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
return null;
}
protected function newResultObject() {
public function newResultObject() {
return null;
}
@ -844,6 +844,9 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
'column' => 'indexValue',
'type' => $index->getIndexValueType(),
'null' => 'tail',
'customfield' => true,
'customfield.index.table' => $index->getTableName(),
'customfield.index.key' => $digest,
);
}
}
@ -1229,6 +1232,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
}
}
// TODO: Get rid of this.
foreach ($this->applicationSearchOrders as $key => $order) {
$table = $order['table'];
$index = $order['index'];
@ -1247,6 +1251,32 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
$index);
}
$phid_column = $this->getApplicationSearchObjectPHIDColumn();
$orderable = $this->getOrderableColumns();
$vector = $this->getOrderVector();
foreach ($vector as $order) {
$spec = $orderable[$order->getOrderKey()];
if (empty($spec['customfield'])) {
continue;
}
$table = $spec['customfield.index.table'];
$alias = $spec['table'];
$key = $spec['customfield.index.key'];
$joins[] = qsprintf(
$conn_r,
'LEFT JOIN %T %T ON %T.objectPHID = %Q
AND %T.indexKey = %s',
$table,
$alias,
$alias,
$phid_column,
$alias,
$key);
}
return implode(' ', $joins);
}