diff --git a/src/applications/feed/query/PhabricatorFeedTransactionQuery.php b/src/applications/feed/query/PhabricatorFeedTransactionQuery.php index 00da566532..e7ab4cc55d 100644 --- a/src/applications/feed/query/PhabricatorFeedTransactionQuery.php +++ b/src/applications/feed/query/PhabricatorFeedTransactionQuery.php @@ -1,178 +1,189 @@ phids = $phids; return $this; } + public function withAuthorPHIDs(array $phids) { + $this->authorPHIDs = $phids; + return $this; + } + public function withDateCreatedBetween($min, $max) { $this->createdMin = $min; $this->createdMax = $max; return $this; } protected function loadPage() { $queries = $this->newTransactionQueries(); $xactions = array(); if ($this->shouldLimitResults()) { $limit = $this->getRawResultLimit(); if (!$limit) { $limit = null; } } else { $limit = null; } // We're doing a bit of manual work to get paging working, because this // query aggregates the results of a large number of subqueries. // Overall, we're ordering transactions by "". Ordering // by PHID is not very meaningful, but we don't need the ordering to be // especially meaningful, just consistent. Using PHIDs is easy and does // everything we need it to technically. // To actually configure paging, if we have an external cursor, we load // the internal cursor first. Then we pass it to each subquery and the // subqueries pretend they just loaded a page where it was the last object. // This configures their queries properly and we can aggregate a cohesive // set of results by combining all the queries. $cursor = $this->getExternalCursorString(); if ($cursor !== null) { $cursor_object = $this->newInternalCursorFromExternalCursor($cursor); } else { $cursor_object = null; } $is_reversed = $this->getIsQueryOrderReversed(); $created_min = $this->createdMin; $created_max = $this->createdMax; $xaction_phids = $this->phids; + $author_phids = $this->authorPHIDs; foreach ($queries as $query) { $query->withDateCreatedBetween($created_min, $created_max); if ($xaction_phids !== null) { $query->withPHIDs($xaction_phids); } + if ($author_phids !== null) { + $query->withAuthorPHIDs($author_phids); + } + if ($limit !== null) { $query->setLimit($limit); } if ($cursor_object !== null) { $query ->setAggregatePagingCursor($cursor_object) ->setIsQueryOrderReversed($is_reversed); } $query->setOrder('global'); $query_xactions = $query->execute(); foreach ($query_xactions as $query_xaction) { $xactions[] = $query_xaction; } $xactions = msortv($xactions, 'newGlobalSortVector'); if ($is_reversed) { $xactions = array_reverse($xactions); } if ($limit !== null) { $xactions = array_slice($xactions, 0, $limit); // If we've found enough transactions to fill up the entire requested // page size, we can narrow the search window: transactions after the // last transaction we've found so far can't possibly be part of the // result set. if (count($xactions) === $limit) { $last_date = last($xactions)->getDateCreated(); if ($is_reversed) { if ($created_max === null) { $created_max = $last_date; } else { $created_max = min($created_max, $last_date); } } else { if ($created_min === null) { $created_min = $last_date; } else { $created_min = max($created_min, $last_date); } } } } } return $xactions; } public function getQueryApplicationClass() { return 'PhabricatorFeedApplication'; } private function newTransactionQueries() { $viewer = $this->getViewer(); $queries = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorApplicationTransactionQuery') ->execute(); $type_map = array(); // If we're querying for specific transaction PHIDs, we only need to // consider queries which may load transactions with subtypes present // in the list. // For example, if we're loading Maniphest Task transaction PHIDs, we know // we only have to look at Maniphest Task transactions, since other types // of objects will never have the right transaction PHIDs. $xaction_phids = $this->phids; if ($xaction_phids) { foreach ($xaction_phids as $xaction_phid) { $type_map[phid_get_subtype($xaction_phid)] = true; } } $results = array(); foreach ($queries as $query) { if ($type_map) { $type = $query->getTemplateApplicationTransaction() ->getApplicationTransactionType(); if (!isset($type_map[$type])) { continue; } } $results[] = id(clone $query) ->setViewer($viewer) ->setParentQuery($this); } return $results; } protected function newExternalCursorStringForResult($object) { return (string)$object->getPHID(); } protected function applyExternalCursorConstraintsToQuery( PhabricatorCursorPagedPolicyAwareQuery $subquery, $cursor) { $subquery->withPHIDs(array($cursor)); } } diff --git a/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php b/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php index bc0d27c70c..5c73818e4d 100644 --- a/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php +++ b/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php @@ -1,113 +1,145 @@ setLabel(pht('Authors')) + ->setKey('authorPHIDs') + ->setAliases(array('author', 'authors')), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created After')) + ->setKey('createdStart'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created Before')) + ->setKey('createdEnd'), + ); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); + if ($map['authorPHIDs']) { + $query->withAuthorPHIDs($map['authorPHIDs']); + } + + $created_min = $map['createdStart']; + $created_max = $map['createdEnd']; + + if ($created_min && $created_max) { + if ($created_min > $created_max) { + throw new PhabricatorSearchConstraintException( + pht( + 'The specified "Created Before" date is earlier in time than the '. + 'specified "Created After" date, so this query can never match '. + 'any results.')); + } + } + + if ($created_min || $created_max) { + $query->withDateCreatedBetween($created_min, $created_max); + } + return $query; } protected function getURI($path) { return '/feed/transactions/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'all' => pht('All Transactions'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery() ->setQueryKey($query_key); switch ($query_key) { case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } protected function renderResultList( array $objects, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($objects, 'PhabricatorApplicationTransaction'); $viewer = $this->requireViewer(); $handle_phids = array(); foreach ($objects as $object) { $author_phid = $object->getAuthorPHID(); if ($author_phid !== null) { $handle_phids[] = $author_phid; } $object_phid = $object->getObjectPHID(); if ($object_phid !== null) { $handle_phids[] = $object_phid; } } $handles = $viewer->loadHandles($handle_phids); $rows = array(); foreach ($objects as $object) { $author_phid = $object->getAuthorPHID(); $object_phid = $object->getObjectPHID(); try { $title = $object->getTitle(); } catch (Exception $ex) { $title = null; } $rows[] = array( $handles[$author_phid]->renderLink(), $handles[$object_phid]->renderLink(), AphrontTableView::renderSingleDisplayLine($title), phabricator_datetime($object->getDateCreated(), $viewer), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( - pht('Actor'), + pht('Author'), pht('Object'), pht('Transaction'), pht('Date'), )) ->setColumnClasses( array( null, null, 'wide', 'right', )); return id(new PhabricatorApplicationSearchResultView()) ->setTable($table); } }