diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php index 1cdb71994d..8a875202f1 100644 --- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php +++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php @@ -1,505 +1,376 @@ isBoardView = $is_board_view; return $this; } public function getIsBoardView() { return $this->isBoardView; } public function setBaseURI($base_uri) { $this->baseURI = $base_uri; return $this; } public function getBaseURI() { return $this->baseURI; } public function setShowBatchControls($show_batch_controls) { $this->showBatchControls = $show_batch_controls; return $this; } public function getResultTypeDescription() { return pht('Tasks'); } public function getApplicationClassName() { return 'PhabricatorManiphestApplication'; } public function newQuery() { return id(new ManiphestTaskQuery()) ->needProjectPHIDs(true); } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); + public function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchOwnersField()) + ->setLabel(pht('Assigned To')) + ->setKey('assignedPHIDs') + ->setAliases(array('assigned')), + id(new PhabricatorSearchUsersField()) + ->setLabel(pht('Authors')) + ->setKey('authorPHIDs') + ->setAliases(array('author', 'authors')), + id(new PhabricatorSearchDatasourceField()) + ->setLabel(pht('Statuses')) + ->setKey('statuses') + ->setAliases(array('status')) + ->setDatasource(new ManiphestTaskStatusFunctionDatasource()), + id(new PhabricatorSearchDatasourceField()) + ->setLabel(pht('Priorities')) + ->setKey('priorities') + ->setAliases(array('priority')) + ->setDatasource(new ManiphestTaskPriorityDatasource()), + id(new PhabricatorSearchTextField()) + ->setLabel(pht('Contains Words')) + ->setKey('fulltext'), + id(new PhabricatorSearchThreeStateField()) + ->setLabel(pht('Blocking')) + ->setKey('blocking') + ->setOptions( + pht('(Show All)'), + pht('Show Only Tasks Blocking Other Tasks'), + pht('Hide Tasks Blocking Other Tasks')), + id(new PhabricatorSearchThreeStateField()) + ->setLabel(pht('Blocked')) + ->setKey('blocked') + ->setOptions( + pht('(Show All)'), + pht('Show Only Task Blocked By Other Tasks'), + pht('Hide Tasks Blocked By Other Tasks')), + id(new PhabricatorSearchSelectField()) + ->setLabel(pht('Group By')) + ->setKey('group') + ->setOptions($this->getGroupOptions()), + id(new PhabricatorSearchStringListField()) + ->setLabel(pht('Task IDs')) + ->setKey('ids'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created After')) + ->setKey('createdStart'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created Before')) + ->setKey('createdEnd'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Updated After')) + ->setKey('modifiedStart'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Updated Before')) + ->setKey('modifiedEnd'), + id(new PhabricatorSearchTextField()) + ->setLabel(pht('Page Size')) + ->setKey('limit'), + ); + } - $saved->setParameter( + public function getDefaultFieldOrder() { + return array( 'assignedPHIDs', - $this->readUsersFromRequest($request, 'assigned')); - - $saved->setParameter( + 'projectPHIDs', 'authorPHIDs', - $this->readUsersFromRequest($request, 'authors')); - - $saved->setParameter( 'subscriberPHIDs', - $this->readSubscribersFromRequest($request, 'subscribers')); - - $saved->setParameter( 'statuses', - $this->readListFromRequest($request, 'statuses')); - - $saved->setParameter( 'priorities', - $this->readListFromRequest($request, 'priorities')); - - $saved->setParameter( + 'fulltext', 'blocking', - $this->readBoolFromRequest($request, 'blocking')); - $saved->setParameter( 'blocked', - $this->readBoolFromRequest($request, 'blocked')); - - $saved->setParameter('group', $request->getStr('group')); - $saved->setParameter('order', $request->getStr('order')); - - $ids = $request->getStrList('ids'); - foreach ($ids as $key => $id) { - $id = trim($id, ' Tt'); - if (!$id || !is_numeric($id)) { - unset($ids[$key]); - } else { - $ids[$key] = $id; - } - } - $saved->setParameter('ids', $ids); - - $saved->setParameter('fulltext', $request->getStr('fulltext')); - - $saved->setParameter( - 'projects', - $this->readProjectsFromRequest($request, 'projects')); + 'group', + 'order', + 'ids', + '...', + 'createdStart', + 'createdEnd', + 'modifiedStart', + 'modifiedEnd', + 'limit', + ); + } - $saved->setParameter('createdStart', $request->getStr('createdStart')); - $saved->setParameter('createdEnd', $request->getStr('createdEnd')); - $saved->setParameter('modifiedStart', $request->getStr('modifiedStart')); - $saved->setParameter('modifiedEnd', $request->getStr('modifiedEnd')); + public function getHiddenFields() { + $keys = array(); - $limit = $request->getInt('limit'); - if ($limit > 0) { - $saved->setParameter('limit', $limit); + if ($this->getIsBoardView()) { + $keys[] = 'group'; + $keys[] = 'order'; + $keys[] = 'limit'; } - $this->readCustomFieldsFromRequest($request, $saved); - - return $saved; + return $keys; } - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = $this->newQuery(); + public function buildQueryFromParameters(array $map) { + $query = id(new ManiphestTaskQuery()) + ->needProjectPHIDs(true); - $viewer = $this->requireViewer(); + if ($map['assignedPHIDs']) { + $query->withOwners($map['assignedPHIDs']); + } - $datasource = id(new PhabricatorPeopleUserFunctionDatasource()) - ->setViewer($viewer); + if ($map['authorPHIDs']) { + $query->withAuthors($map['authorPHIDs']); + } - $author_phids = $saved->getParameter('authorPHIDs', array()); - $author_phids = $datasource->evaluateTokens($author_phids); - if ($author_phids) { - $query->withAuthors($author_phids); + if ($map['statuses']) { + $query->withStatuses($map['statuses']); } - $datasource = id(new PhabricatorMetaMTAMailableFunctionDatasource()) - ->setViewer($viewer); - $subscriber_phids = $saved->getParameter('subscriberPHIDs', array()); - $subscriber_phids = $datasource->evaluateTokens($subscriber_phids); - if ($subscriber_phids) { - $query->withSubscribers($subscriber_phids); + if ($map['priorities']) { + $query->withPriorities($map['priorities']); } - $datasource = id(new PhabricatorPeopleOwnerDatasource()) - ->setViewer($this->requireViewer()); + if ($map['createdStart']) { + $query->withDateCreatedAfter($map['createdStart']); + } - $assigned_phids = $this->readAssignedPHIDs($saved); - $assigned_phids = $datasource->evaluateTokens($assigned_phids); - if ($assigned_phids) { - $query->withOwners($assigned_phids); + if ($map['createdEnd']) { + $query->withDateCreatedBefore($map['createdEnd']); } - $datasource = id(new ManiphestTaskStatusFunctionDatasource()) - ->setViewer($this->requireViewer()); - $statuses = $saved->getParameter('statuses', array()); - $statuses = $datasource->evaluateTokens($statuses); - if ($statuses) { - $query->withStatuses($statuses); + if ($map['modifiedStart']) { + $query->withDateModifiedAfter($map['modifiedStart']); } - $priorities = $saved->getParameter('priorities', array()); - if ($priorities) { - $query->withPriorities($priorities); + if ($map['modifiedEnd']) { + $query->withDateModifiedBefore($map['modifiedEnd']); } + if ($map['blocking'] !== null) { + $query->withBlockingTasks($map['blocking']); + } - $query->withBlockingTasks($saved->getParameter('blocking')); - $query->withBlockedTasks($saved->getParameter('blocked')); + if ($map['blocked'] !== null) { + $query->withBlockedTasks($map['blocked']); + } - // TODO: This is glue that will be obsolete soon. - $order = $saved->getParameter('order'); - $builtin = $query->getBuiltinOrderAliasMap(); - if (strlen($order) && isset($builtin[$order])) { - $query->setOrder($order); - } else { - $query->setOrder(head_key($builtin)); + if (strlen($map['fulltext'])) { + $query->withFullTextSearch($map['fulltext']); } - $group = $saved->getParameter('group'); + $group = idx($map, 'group'); $group = idx($this->getGroupValues(), $group); if ($group) { $query->setGroupBy($group); } else { $query->setGroupBy(head($this->getGroupValues())); } - $ids = $saved->getParameter('ids'); - if ($ids) { - $query->withIDs($ids); - } - - $fulltext = $saved->getParameter('fulltext'); - if (strlen($fulltext)) { - $query->withFullTextSearch($fulltext); - } - - $projects = $this->readProjectTokens($saved); - $adjusted = id(clone $saved)->setParameter('projects', $projects); - $this->setQueryProjects($query, $adjusted); - - $start = $this->parseDateTime($saved->getParameter('createdStart')); - $end = $this->parseDateTime($saved->getParameter('createdEnd')); - - if ($start) { - $query->withDateCreatedAfter($start); - } - - if ($end) { - $query->withDateCreatedBefore($end); - } - - $mod_start = $this->parseDateTime($saved->getParameter('modifiedStart')); - $mod_end = $this->parseDateTime($saved->getParameter('modifiedEnd')); - - if ($mod_start) { - $query->withDateModifiedAfter($mod_start); - } + if ($map['ids']) { + $ids = $map['ids']; + foreach ($ids as $key => $id) { + $id = trim($id, ' Tt'); + if (!$id || !is_numeric($id)) { + unset($ids[$key]); + } else { + $ids[$key] = $id; + } + } - if ($mod_end) { - $query->withDateModifiedBefore($mod_end); + if ($ids) { + $query->withIDs($ids); + } } - $this->applyCustomFieldsToQuery($query, $saved); - return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved) { - - $assigned_phids = $this->readAssignedPHIDs($saved); - - $author_phids = $saved->getParameter('authorPHIDs', array()); - $projects = $this->readProjectTokens($saved); - - $subscriber_phids = $saved->getParameter('subscriberPHIDs', array()); - - $statuses = $saved->getParameter('statuses', array()); - $priorities = $saved->getParameter('priorities', array()); - - $blocking_control = id(new AphrontFormSelectControl()) - ->setLabel(pht('Blocking')) - ->setName('blocking') - ->setValue($this->getBoolFromQuery($saved, 'blocking')) - ->setOptions(array( - '' => pht('Show All Tasks'), - 'true' => pht('Show Tasks Blocking Other Tasks'), - 'false' => pht('Show Tasks Not Blocking Other Tasks'), - )); - - $blocked_control = id(new AphrontFormSelectControl()) - ->setLabel(pht('Blocked')) - ->setName('blocked') - ->setValue($this->getBoolFromQuery($saved, 'blocked')) - ->setOptions(array( - '' => pht('Show All Tasks'), - 'true' => pht('Show Tasks Blocked By Other Tasks'), - 'false' => pht('Show Tasks Not Blocked By Other Tasks'), - )); - - $ids = $saved->getParameter('ids', array()); - - $all_orders = ipull($this->newQuery()->getBuiltinOrders(), 'name'); - - $form - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorPeopleOwnerDatasource()) - ->setName('assigned') - ->setLabel(pht('Assigned To')) - ->setValue($assigned_phids)) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorProjectLogicalDatasource()) - ->setName('projects') - ->setLabel(pht('Projects')) - ->setValue($projects)) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorPeopleUserFunctionDatasource()) - ->setName('authors') - ->setLabel(pht('Authors')) - ->setValue($author_phids)) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorMetaMTAMailableFunctionDatasource()) - ->setName('subscribers') - ->setLabel(pht('Subscribers')) - ->setValue($subscriber_phids)) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new ManiphestTaskStatusFunctionDatasource()) - ->setLabel(pht('Statuses')) - ->setName('statuses') - ->setValue($statuses)) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new ManiphestTaskPriorityDatasource()) - ->setLabel(pht('Priorities')) - ->setName('priorities') - ->setValue($priorities)) - ->appendChild( - id(new AphrontFormTextControl()) - ->setName('fulltext') - ->setLabel(pht('Contains Words')) - ->setValue($saved->getParameter('fulltext'))) - ->appendChild($blocking_control) - ->appendChild($blocked_control); - - if (!$this->getIsBoardView()) { - $form - ->appendChild( - id(new AphrontFormSelectControl()) - ->setName('group') - ->setLabel(pht('Group By')) - ->setValue($saved->getParameter('group')) - ->setOptions($this->getGroupOptions())) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setName('order') - ->setLabel(pht('Order By')) - ->setValue($saved->getParameter('order')) - ->setOptions($all_orders)); - } - - $form - ->appendChild( - id(new AphrontFormTextControl()) - ->setName('ids') - ->setLabel(pht('Task IDs')) - ->setValue(implode(', ', $ids))); - - $this->appendCustomFieldsToForm($form, $saved); - - $this->buildDateRange( - $form, - $saved, - 'createdStart', - pht('Created After'), - 'createdEnd', - pht('Created Before')); - - $this->buildDateRange( - $form, - $saved, - 'modifiedStart', - pht('Updated After'), - 'modifiedEnd', - pht('Updated Before')); - - if (!$this->getIsBoardView()) { - $form - ->appendChild( - id(new AphrontFormTextControl()) - ->setName('limit') - ->setLabel(pht('Page Size')) - ->setValue($saved->getParameter('limit', 100))); - } - } - protected function getURI($path) { if ($this->baseURI) { return $this->baseURI.$path; } return '/maniphest/'.$path; } protected function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { $names['assigned'] = pht('Assigned'); $names['authored'] = pht('Authored'); $names['subscribed'] = pht('Subscribed'); } $names['open'] = pht('Open Tasks'); $names['all'] = pht('All Tasks'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); $viewer_phid = $this->requireViewer()->getPHID(); switch ($query_key) { case 'all': return $query; case 'assigned': return $query ->setParameter('assignedPHIDs', array($viewer_phid)) ->setParameter( 'statuses', ManiphestTaskStatus::getOpenStatusConstants()); case 'subscribed': return $query ->setParameter('subscriberPHIDs', array($viewer_phid)) ->setParameter( 'statuses', ManiphestTaskStatus::getOpenStatusConstants()); case 'open': return $query ->setParameter( 'statuses', ManiphestTaskStatus::getOpenStatusConstants()); case 'authored': return $query ->setParameter('authorPHIDs', array($viewer_phid)) ->setParameter('order', 'created') ->setParameter('group', 'none'); } return parent::buildSavedQueryFromBuiltin($query_key); } private function getGroupOptions() { return array( 'priority' => pht('Priority'), 'assigned' => pht('Assigned'), 'status' => pht('Status'), 'project' => pht('Project'), 'none' => pht('None'), ); } private function getGroupValues() { return array( 'priority' => ManiphestTaskQuery::GROUP_PRIORITY, 'assigned' => ManiphestTaskQuery::GROUP_OWNER, 'status' => ManiphestTaskQuery::GROUP_STATUS, 'project' => ManiphestTaskQuery::GROUP_PROJECT, 'none' => ManiphestTaskQuery::GROUP_NONE, ); } protected function renderResultList( array $tasks, PhabricatorSavedQuery $saved, array $handles) { $viewer = $this->requireViewer(); if ($this->isPanelContext()) { $can_edit_priority = false; $can_bulk_edit = false; } else { $can_edit_priority = PhabricatorPolicyFilter::hasCapability( $viewer, $this->getApplication(), ManiphestEditPriorityCapability::CAPABILITY); $can_bulk_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $this->getApplication(), ManiphestBulkEditCapability::CAPABILITY); } return id(new ManiphestTaskResultListView()) ->setUser($viewer) ->setTasks($tasks) ->setSavedQuery($saved) ->setCanEditPriority($can_edit_priority) ->setCanBatchEdit($can_bulk_edit) ->setShowBatchControls($this->showBatchControls); } - private function readAssignedPHIDs(PhabricatorSavedQuery $saved) { - $assigned_phids = $saved->getParameter('assignedPHIDs', array()); + protected function willUseSavedQuery(PhabricatorSavedQuery $saved) { - // This may be present in old saved queries from before parameterized - // typeaheads, and is retained for compatibility. We could remove it by - // migrating old saved queries. + // The 'withUnassigned' parameter may be present in old saved queries from + // before parameterized typeaheads, and is retained for compatibility. We + // could remove it by migrating old saved queries. + $assigned_phids = $saved->getParameter('assignedPHIDs', array()); if ($saved->getParameter('withUnassigned')) { $assigned_phids[] = PhabricatorPeopleNoOwnerDatasource::FUNCTION_TOKEN; } + $saved->setParameter('assignedPHIDs', $assigned_phids); - return $assigned_phids; - } + // The 'projects' and other parameters may be present in old saved queries + // from before parameterized typeaheads. + $project_phids = $saved->getParameter('projectPHIDs', array()); - private function readProjectTokens(PhabricatorSavedQuery $saved) { - $projects = $saved->getParameter('projects', array()); + $old = $saved->getParameter('projects', array()); + foreach ($old as $phid) { + $project_phids[] = $phid; + } $all = $saved->getParameter('allProjectPHIDs', array()); foreach ($all as $phid) { - $projects[] = $phid; + $project_phids[] = $phid; } $any = $saved->getParameter('anyProjectPHIDs', array()); foreach ($any as $phid) { - $projects[] = 'any('.$phid.')'; + $project_phids[] = 'any('.$phid.')'; } $not = $saved->getParameter('excludeProjectPHIDs', array()); foreach ($not as $phid) { - $projects[] = 'not('.$phid.')'; + $project_phids[] = 'not('.$phid.')'; } $users = $saved->getParameter('userProjectPHIDs', array()); foreach ($users as $phid) { - $projects[] = 'projects('.$phid.')'; + $project_phids[] = 'projects('.$phid.')'; } $no = $saved->getParameter('withNoProject'); if ($no) { - $projects[] = 'null()'; + $project_phids[] = 'null()'; } - return $projects; + $saved->setParameter('projectPHIDs', $project_phids); } } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index 6204a37c35..716b1e5c2a 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -1,1158 +1,1126 @@ newQuery(); if ($query) { $object = $query->newResultObject(); if ($object) { return $object; } } return null; } public function newQuery() { return null; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } protected function requireViewer() { if (!$this->viewer) { throw new PhutilInvalidStateException('setViewer'); } return $this->viewer; } public function setContext($context) { $this->context = $context; return $this; } public function isPanelContext() { return ($this->context == self::CONTEXT_PANEL); } public function canUseInPanelContext() { return true; } public function saveQuery(PhabricatorSavedQuery $query) { $query->setEngineClassName(get_class($this)); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); try { $query->save(); } catch (AphrontDuplicateKeyQueryException $ex) { // Ignore, this is just a repeated search. } unset($unguarded); } /** * Create a saved query object from the request. * * @param AphrontRequest The search request. * @return PhabricatorSavedQuery */ 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; } /** * Executes the saved query. * * @param PhabricatorSavedQuery The saved query to operate on. * @return The result of the query. */ public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { $saved = clone $saved; $this->willUseSavedQuery($saved); $fields = $this->buildSearchFields(); $viewer = $this->requireViewer(); $map = array(); foreach ($fields as $field) { $field->setViewer($viewer); $field->readValueFromSavedQuery($saved); $value = $field->getValueForQuery($field->getValue()); $map[$field->getKey()] = $value; } $query = $this->buildQueryFromParameters($map); $object = $this->newResultObject(); if (!$object) { return $query; } if ($object instanceof PhabricatorSubscribableInterface) { if (!empty($map['subscriberPHIDs'])) { $query->withEdgeLogicPHIDs( PhabricatorObjectHasSubscriberEdgeType::EDGECONST, PhabricatorQueryConstraint::OPERATOR_OR, $map['subscriberPHIDs']); } } if ($object instanceof PhabricatorProjectInterface) { if (!empty($map['projectPHIDs'])) { $query->withEdgeLogicConstraints( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $map['projectPHIDs']); } } if ($object instanceof PhabricatorSpacesInterface) { if (!empty($map['spacePHIDs'])) { $query->withSpacePHIDs($map['spacePHIDs']); } } if ($object instanceof PhabricatorCustomFieldInterface) { $this->applyCustomFieldsToQuery($query, $saved); } $order = $saved->getParameter('order'); $builtin = $query->getBuiltinOrderAliasMap(); 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)); } return $query; } /** * 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; } protected function buildQueryFromParameters(array $parameters) { throw new PhutilMethodNotImplementedException(); } /** * Builds the search form using the request. * * @param AphrontFormView Form to populate. * @param PhabricatorSavedQuery The query from which to build the form. * @return void */ public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $saved = clone $saved; $this->willUseSavedQuery($saved); $fields = $this->buildSearchFields(); + $fields = $this->adjustFieldsForDisplay($fields); $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; } $object = $this->newResultObject(); if ($object) { 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 PhabricatorSpacesInterface) { if (PhabricatorSpacesNamespaceQuery::getSpacesExist()) { $fields[] = id(new PhabricatorSearchSpacesField()) ->setKey('spacePHIDs') ->setAliases(array('space', 'spaces')) ->setLabel(pht('Spaces')); } } } foreach ($this->buildCustomFieldSearchFields() as $custom_field) { $fields[] = $custom_field; } $query = $this->newQuery(); if ($query) { $orders = $query->getBuiltinOrders(); $orders = ipull($orders, 'name'); $fields[] = id(new PhabricatorSearchOrderField()) - ->setLabel(pht('Order')) + ->setLabel(pht('Order By')) ->setKey('order') ->setOptions($orders); } $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; } - return $this->adjustFieldsForDisplay($field_map); + return $field_map; } 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); - return $head + $body + $tail; + $result = $head + $body + $tail; + + foreach ($this->getHiddenFields() as $hidden_key) { + unset($result[$hidden_key]); + } + + return $result; } protected function buildCustomSearchFields() { throw new PhutilMethodNotImplementedException(); } /** * 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 Default ordering for field keys. */ protected function getDefaultFieldOrder() { return array(); } + /** + * Return a list of field keys which should be hidden from the viewer. + * + * @return list Fields to hide. + */ + protected function getHiddenFields() { + return array(); + } + public function getErrors() { return $this->errors; } public function addError($error) { $this->errors[] = $error; return $this; } /** * Return an application URI corresponding to the results page of a query. * Normally, this is something like `/application/query/QUERYKEY/`. * * @param string The query key to build a URI for. * @return string URI where the query can be executed. * @task uri */ public function getQueryResultsPageURI($query_key) { return $this->getURI('query/'.$query_key.'/'); } /** * 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 */ 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); /** * 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(); public function newSavedQuery() { return id(new PhabricatorSavedQuery()) ->setEngineClassName(get_class($this)); } public function addNavigationItems(PHUIListView $menu) { $viewer = $this->requireViewer(); $menu->newLabel(pht('Queries')); $named_queries = $this->loadEnabledNamedQueries(); foreach ($named_queries as $query) { $key = $query->getQueryKey(); $uri = $this->getQueryResultsPageURI($key); $menu->newLink($query->getQueryName(), $uri, 'query/'.$key); } if ($viewer->isLoggedIn()) { $manage_uri = $this->getQueryManagementURI(); $menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit'); } $menu->newLabel(pht('Search')); $advanced_uri = $this->getQueryResultsPageURI('advanced'); $menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced'); return $this; } public function loadAllNamedQueries() { $viewer = $this->requireViewer(); $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 = $this->getBuiltinQueries($viewer); $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]); } } unset($builtin[$key]); } $named_queries = msort($named_queries, 'getSortKey'); return $named_queries + $builtin; } 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; } 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); } } /* -( 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; } abstract public function getApplicationClassName(); /* -( Constructing Engines )----------------------------------------------- */ /** * Load all available application search engines. * * @return list All available engines. * @task construct */ public static function getAllEngines() { $engines = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); return $engines; } /** * 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); } /* -( Builtin Queries )---------------------------------------------------- */ /** * @task builtin */ public function getBuiltinQueries() { $names = $this->getBuiltinQueryNames(); $queries = array(); $sequence = 0; foreach ($names as $key => $name) { $queries[$key] = id(new PhabricatorNamedQuery()) ->setUserPHID($this->requireViewer()->getPHID()) ->setEngineClassName(get_class($this)) ->setQueryName($name) ->setQueryKey($key) ->setSequence((1 << 24) + $sequence++) ->setIsBuiltin(true); } return $queries; } /** * @task builtin */ public function getBuiltinQuery($query_key) { if (!$this->isBuiltinQuery($query_key)) { throw new Exception(pht("'%s' is not a builtin!", $query_key)); } return idx($this->getBuiltinQueries(), $query_key); } /** * @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) { throw new Exception(pht("Builtin '%s' is not supported!", $query_key)); } /* -( 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. * @param list Other permitted PHID types. * @return list List of user PHIDs and selector functions. * @task read */ protected function readUsersFromRequest( AphrontRequest $request, $key, array $allow_types = array()) { $list = $this->readListFromRequest($request, $key); $phids = array(); $names = array(); $allow_types = array_fuse($allow_types); $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; foreach ($list as $item) { $type = phid_get_type($item); if ($type == $user_type) { $phids[] = $item; } else if (isset($allow_types[$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 { $names[] = $item; } } } 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; } /** * 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 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 List of object PHIDs. * @task read */ protected function readSubscribersFromRequest( AphrontRequest $request, $key) { return $this->readUsersFromRequest( $request, $key, array( PhabricatorProjectProjectPHIDType::TYPECONST, )); } /** * 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 Optional, list of permitted PHID types. * @return list List of object PHIDs. * * @task read */ protected function readPHIDsFromRequest( AphrontRequest $request, $key, array $allow_types = array()) { $list = $this->readListFromRequest($request, $key); $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; } /** * 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. * * @param AphrontRequest Request to read strings from. * @param string Key to read in the request. * @return list 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; } protected function readDateFromRequest( AphrontRequest $request, $key) { $value = AphrontFormDateControlValue::newFromRequest($request, $key); if ($value->isEmpty()) { return null; } return $value->getDictionary(); } 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'; } /* -( Dates )-------------------------------------------------------------- */ /** * @task dates */ protected function parseDateTime($date_time) { if (!strlen($date_time)) { return null; } return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer()); } /** * @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)); } /* -( Paging and Executing Queries )--------------------------------------- */ public function getPageSize(PhabricatorSavedQuery $saved) { - return $saved->getParameter('limit', 100); + $limit = (int)$saved->getParameter('limit'); + + if ($limit > 0) { + return $limit; + } + + return 100; } public function shouldUseOffsetPaging() { return false; } public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) { if ($this->shouldUseOffsetPaging()) { $pager = new AphrontPagerView(); } 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 )---------------------------------------------------------- */ public function setRequest(AphrontRequest $request) { $this->request = $request; return $this; } public function getRequest() { return $this->request; } public function renderResults( array $objects, PhabricatorSavedQuery $query) { $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); } protected function getRequiredHandlePHIDsForResultList( array $objects, PhabricatorSavedQuery $query) { return array(); } protected function renderResultList( array $objects, PhabricatorSavedQuery $query, array $handles) { throw new Exception(pht('Not supported here yet!')); } /* -( 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() { $object = $this->newResultObject(); if ($object instanceof PhabricatorCustomFieldInterface) { return $object; } 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); $fields->setViewer($this->requireViewer()); } else { $fields = null; } $this->customFields = $fields; } return $this->customFields; } - /** - * Moves data from the request into a saved query. - * - * @param AphrontRequest Request to read. - * @param PhabricatorSavedQuery Query to write to. - * @return void - * @task appsearch - */ - protected function readCustomFieldsFromRequest( - AphrontRequest $request, - PhabricatorSavedQuery $saved) { - - $list = $this->getCustomFieldList(); - if (!$list) { - return; - } - - foreach ($list->getFields() as $field) { - $key = $this->getKeyForCustomField($field); - $value = $field->readApplicationSearchValueFromRequest( - $this, - $request); - $saved->setParameter($key, $value); - } - } - - /** * 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) { - $key = $this->getKeyForCustomField($field); $value = $field->applyApplicationSearchConstraintToQuery( $this, $query, - $saved->getParameter($key)); + $saved->getParameter('custom:'.$field->getFieldIndex())); } } - /** - * Get a unique key identifying a field. - * - * @param PhabricatorCustomField Field to identify. - * @return string Unique identifier, suitable for use as an input name. - */ - public function getKeyForCustomField(PhabricatorCustomField $field) { - return 'custom:'.$field->getFieldIndex(); - } - private function buildCustomFieldSearchFields() { $list = $this->getCustomFieldList(); if (!$list) { return array(); } $fields = array(); foreach ($list->getFields() as $field) { $fields[] = id(new PhabricatorSearchCustomFieldProxyField()) ->setSearchEngine($this) ->setCustomField($field); } - return $fields; - } - - // TODO: Remove. - protected function appendCustomFieldsToForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved) { - - $list = $this->getCustomFieldList(); - if (!$list) { - return; - } - foreach ($list->getFields() as $field) { - $key = $this->getKeyForCustomField($field); - $value = $saved->getParameter($key); - $field->appendToApplicationSearchForm($this, $form, $value); - } + return $fields; } }