diff --git a/src/applications/feed/query/PhabricatorFeedTransactionQuery.php b/src/applications/feed/query/PhabricatorFeedTransactionQuery.php index ba25f0842d..d0a9f53e35 100644 --- a/src/applications/feed/query/PhabricatorFeedTransactionQuery.php +++ b/src/applications/feed/query/PhabricatorFeedTransactionQuery.php @@ -1,207 +1,215 @@ phids = $phids; return $this; } public function withAuthorPHIDs(array $phids) { $this->authorPHIDs = $phids; return $this; } public function withObjectTypes(array $types) { $this->objectTypes = $types; return $this; } public function withDateCreatedBetween($min, $max) { $this->createdMin = $min; $this->createdMax = $max; return $this; } + public function newResultObject() { + // Return an arbitrary valid transaction object. The actual query may + // return objects of any subclass of "ApplicationTransaction" when it is + // executed, but we need to pick something concrete here to make some + // integrations work (like automatic handling of PHIDs in data export). + return new PhabricatorUserTransaction(); + } + 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; } } $object_types = $this->objectTypes; if ($object_types) { $object_types = array_fuse($object_types); } $results = array(); foreach ($queries as $query) { $query_type = $query->getTemplateApplicationTransaction() ->getApplicationTransactionType(); if ($type_map) { if (!isset($type_map[$query_type])) { continue; } } if ($object_types) { if (!isset($object_types[$query_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 af40b6ff6b..0cbbcd23b1 100644 --- a/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php +++ b/src/applications/feed/query/PhabricatorFeedTransactionSearchEngine.php @@ -1,154 +1,230 @@ setLabel(pht('Authors')) ->setKey('authorPHIDs') ->setAliases(array('author', 'authors')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Object Types')) ->setKey('objectTypes') ->setAliases(array('objectType')) ->setDatasource(new PhabricatorTransactionsObjectTypeDatasource()), 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']); } if ($map['objectTypes']) { $query->withObjectTypes($map['objectTypes']); } $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('Author'), pht('Object'), pht('Transaction'), pht('Date'), )) ->setColumnClasses( array( null, null, 'wide', 'right', )); return id(new PhabricatorApplicationSearchResultView()) ->setTable($table); } + protected function newExportFields() { + $fields = array( + id(new PhabricatorPHIDExportField()) + ->setKey('authorPHID') + ->setLabel(pht('Author PHID')), + id(new PhabricatorStringExportField()) + ->setKey('author') + ->setLabel(pht('Author')), + id(new PhabricatorStringExportField()) + ->setKey('objectType') + ->setLabel(pht('Object Type')), + id(new PhabricatorPHIDExportField()) + ->setKey('objectPHID') + ->setLabel(pht('Object PHID')), + id(new PhabricatorStringExportField()) + ->setKey('objectName') + ->setLabel(pht('Object Name')), + id(new PhabricatorStringExportField()) + ->setKey('description') + ->setLabel(pht('Description')), + ); + + return $fields; + } + + protected function newExportData(array $xactions) { + $viewer = $this->requireViewer(); + + $phids = array(); + foreach ($xactions as $xaction) { + $phids[] = $xaction->getAuthorPHID(); + $phids[] = $xaction->getObjectPHID(); + } + $handles = $viewer->loadHandles($phids); + + $export = array(); + foreach ($xactions as $xaction) { + $xaction_phid = $xaction->getPHID(); + + $author_phid = $xaction->getAuthorPHID(); + if ($author_phid) { + $author_name = $handles[$author_phid]->getName(); + } else { + $author_name = null; + } + + $object_phid = $xaction->getObjectPHID(); + if ($object_phid) { + $object_name = $handles[$object_phid]->getName(); + } else { + $object_name = null; + } + + $old_target = $xaction->getRenderingTarget(); + try { + $description = $xaction + ->setRenderingTarget(PhabricatorApplicationTransaction::TARGET_TEXT) + ->getTitle(); + } catch (Exception $ex) { + $description = null; + } + $xaction->setRenderingTarget($old_target); + + $export[] = array( + 'authorPHID' => $author_phid, + 'author' => $author_name, + 'objectType' => phid_get_subtype($xaction_phid), + 'objectPHID' => $object_phid, + 'objectName' => $object_name, + 'description' => $description, + ); + } + + return $export; + } + }