diff --git a/src/applications/differential/xaction/DifferentialRevisionStatusTransaction.php b/src/applications/differential/xaction/DifferentialRevisionStatusTransaction.php index c08eb9d187..615ce38bcf 100644 --- a/src/applications/differential/xaction/DifferentialRevisionStatusTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionStatusTransaction.php @@ -1,73 +1,84 @@ getModernRevisionStatus(); } public function applyInternalEffects($object, $value) { $object->setModernRevisionStatus($value); } public function getTitle() { $status = $this->newStatusObject(); if ($status->isAccepted()) { return pht('This revision is now accepted and ready to land.'); } if ($status->isNeedsRevision()) { return pht('This revision now requires changes to proceed.'); } if ($status->isNeedsReview()) { return pht('This revision now requires review to proceed.'); } return null; } public function getTitleForFeed() { $status = $this->newStatusObject(); if ($status->isAccepted()) { return pht( '%s is now accepted and ready to land.', $this->renderObject()); } if ($status->isNeedsRevision()) { return pht( '%s now requires changes to proceed.', $this->renderObject()); } if ($status->isNeedsReview()) { return pht( '%s now requires review to proceed.', $this->renderObject()); } return null; } public function getIcon() { $status = $this->newStatusObject(); return $status->getTimelineIcon(); } public function getColor() { $status = $this->newStatusObject(); return $status->getTimelineColor(); } private function newStatusObject() { $new = $this->getNewValue(); return DifferentialRevisionStatus::newForStatus($new); } + public function getTransactionTypeForConduit($xaction) { + return 'status'; + } + + public function getFieldValuesForConduit($object, $data) { + return array( + 'old' => $object->getOldValue(), + 'new' => $object->getNewValue(), + ); + } + } diff --git a/src/applications/differential/xaction/DifferentialRevisionTitleTransaction.php b/src/applications/differential/xaction/DifferentialRevisionTitleTransaction.php index 9b763c53ca..812464b26d 100644 --- a/src/applications/differential/xaction/DifferentialRevisionTitleTransaction.php +++ b/src/applications/differential/xaction/DifferentialRevisionTitleTransaction.php @@ -1,58 +1,69 @@ getTitle(); } public function applyInternalEffects($object, $value) { $object->setTitle($value); } public function getTitle() { return pht( '%s retitled this revision from %s to %s.', $this->renderAuthor(), $this->renderOldValue(), $this->renderNewValue()); } public function getTitleForFeed() { return pht( '%s retitled %s from %s to %s.', $this->renderAuthor(), $this->renderObject(), $this->renderOldValue(), $this->renderNewValue()); } public function validateTransactions($object, array $xactions) { $errors = array(); if ($this->isEmptyTextTransaction($object->getTitle(), $xactions)) { $errors[] = $this->newRequiredError( pht('Revisions must have a title.')); } $max_length = $object->getColumnMaximumByteLength('title'); foreach ($xactions as $xaction) { $new_value = $xaction->getNewValue(); $new_length = strlen($new_value); if ($new_length > $max_length) { $errors[] = $this->newInvalidError( pht( 'Revision title is too long: the maximum length of a '. 'revision title is 255 bytes.'), $xaction); } } return $errors; } + public function getTransactionTypeForConduit($xaction) { + return 'title'; + } + + public function getFieldValuesForConduit($object, $data) { + return array( + 'old' => $object->getOldValue(), + 'new' => $object->getNewValue(), + ); + } + } diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php index 0f655199fa..b3065b0d9c 100644 --- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php +++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php @@ -1,152 +1,195 @@ 'phid|string', ) + $this->getPagerParamTypes(); } protected function defineReturnType() { return 'list'; } protected function defineErrorTypes() { return array(); } protected function execute(ConduitAPIRequest $request) { $viewer = $request->getUser(); $pager = $this->newPager($request); $object_name = $request->getValue('objectIdentifier', null); if (!strlen($object_name)) { throw new Exception( pht( 'When calling "transaction.search", you must provide an object to '. 'retrieve transactions for.')); } $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withNames(array($object_name)) ->executeOne(); if (!$object) { throw new Exception( pht( 'No object "%s" exists.', $object_name)); } if (!($object instanceof PhabricatorApplicationTransactionInterface)) { throw new Exception( pht( 'Object "%s" does not implement "%s", so transactions can not '. 'be loaded for it.')); } $xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject( $object); $xactions = $xaction_query ->withObjectPHIDs(array($object->getPHID())) ->setViewer($viewer) ->executeWithCursorPager($pager); if ($xactions) { $template = head($xactions)->getApplicationTransactionCommentObject(); $query = new PhabricatorApplicationTransactionTemplatedCommentQuery(); $comment_map = $query ->setViewer($viewer) ->setTemplate($template) ->withTransactionPHIDs(mpull($xactions, 'getPHID')) ->execute(); $comment_map = msort($comment_map, 'getCommentVersion'); $comment_map = array_reverse($comment_map); $comment_map = mgroup($comment_map, 'getTransactionPHID'); } else { $comment_map = array(); } + $modular_classes = array(); + $modular_objects = array(); + $modular_xactions = array(); + foreach ($xactions as $xaction) { + if (!$xaction instanceof PhabricatorModularTransaction) { + continue; + } + + $modular_template = $xaction->getModularType(); + $modular_class = get_class($modular_template); + if (!isset($modular_objects[$modular_class])) { + try { + $modular_object = newv($modular_class, array()); + $modular_objects[$modular_class] = $modular_object; + } catch (Exception $ex) { + continue; + } + } + + $modular_classes[$xaction->getPHID()] = $modular_class; + $modular_xactions[$modular_class][] = $xaction; + } + + $modular_data_map = array(); + foreach ($modular_objects as $class => $modular_type) { + $modular_data_map[$class] = $modular_type + ->setViewer($viewer) + ->loadTransactionTypeConduitData($modular_xactions[$class]); + } + $data = array(); foreach ($xactions as $xaction) { $comments = idx($comment_map, $xaction->getPHID()); $comment_data = array(); if ($comments) { $removed = head($comments)->getIsDeleted(); foreach ($comments as $comment) { if ($removed) { // If the most recent version of the comment has been removed, // don't show the history. This is for consistency with the web // UI, which also prevents users from retrieving the content of // removed comments. $content = array( 'raw' => '', ); } else { $content = array( 'raw' => (string)$comment->getContent(), ); } $comment_data[] = array( 'id' => (int)$comment->getID(), 'phid' => (string)$comment->getPHID(), 'version' => (int)$comment->getCommentVersion(), 'authorPHID' => (string)$comment->getAuthorPHID(), 'dateCreated' => (int)$comment->getDateCreated(), 'dateModified' => (int)$comment->getDateModified(), 'removed' => (bool)$comment->getIsDeleted(), 'content' => $content, ); } } $fields = array(); + $type = null; + + if (isset($modular_classes[$xaction->getPHID()])) { + $modular_class = $modular_classes[$xaction->getPHID()]; + $modular_object = $modular_objects[$modular_class]; + $modular_data = $modular_data_map[$modular_class]; + + $type = $modular_object->getTransactionTypeForConduit($xaction); + $fields = $modular_object->getFieldValuesForConduit( + $xaction, + $modular_data); + } if (!$fields) { $fields = (object)$fields; } $data[] = array( 'id' => (int)$xaction->getID(), 'phid' => (string)$xaction->getPHID(), + 'type' => $type, 'authorPHID' => (string)$xaction->getAuthorPHID(), 'objectPHID' => (string)$xaction->getObjectPHID(), 'dateCreated' => (int)$xaction->getDateCreated(), 'dateModified' => (int)$xaction->getDateModified(), 'comments' => $comment_data, 'fields' => $fields, ); } $results = array( 'data' => $data, ); return $this->addPagerResults($results, $pager); } } diff --git a/src/applications/transactions/storage/PhabricatorModularTransactionType.php b/src/applications/transactions/storage/PhabricatorModularTransactionType.php index 128b5c7c19..119bfedd32 100644 --- a/src/applications/transactions/storage/PhabricatorModularTransactionType.php +++ b/src/applications/transactions/storage/PhabricatorModularTransactionType.php @@ -1,335 +1,347 @@ getPhobjectClassConstant('TRANSACTIONTYPE'); } public function generateOldValue($object) { throw new PhutilMethodNotImplementedException(); } public function generateNewValue($object, $value) { return $value; } public function validateTransactions($object, array $xactions) { return array(); } public function willApplyTransactions($object, array $xactions) { return; } public function applyInternalEffects($object, $value) { return; } public function applyExternalEffects($object, $value) { return; } public function getTransactionHasEffect($object, $old, $new) { return ($old !== $new); } public function extractFilePHIDs($object, $value) { return array(); } public function shouldHide() { return false; } public function getIcon() { return null; } public function getTitle() { return null; } public function getTitleForFeed() { return null; } public function getActionName() { return null; } public function getActionStrength() { return null; } public function getColor() { return null; } public function hasChangeDetailView() { return false; } public function newChangeDetailView() { return null; } public function getMailDiffSectionHeader() { return pht('EDIT DETAILS'); } public function newRemarkupChanges() { return array(); } public function mergeTransactions( $object, PhabricatorApplicationTransaction $u, PhabricatorApplicationTransaction $v) { return null; } final public function setStorage( PhabricatorApplicationTransaction $xaction) { $this->storage = $xaction; return $this; } private function getStorage() { return $this->storage; } final public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } final protected function getViewer() { return $this->viewer; } final public function getActor() { return $this->getEditor()->getActor(); } final public function getActingAsPHID() { return $this->getEditor()->getActingAsPHID(); } final public function setEditor( PhabricatorApplicationTransactionEditor $editor) { $this->editor = $editor; return $this; } final protected function getEditor() { if (!$this->editor) { throw new PhutilInvalidStateException('setEditor'); } return $this->editor; } final protected function getAuthorPHID() { return $this->getStorage()->getAuthorPHID(); } final protected function getObjectPHID() { return $this->getStorage()->getObjectPHID(); } final protected function getObject() { return $this->getStorage()->getObject(); } final protected function getOldValue() { return $this->getStorage()->getOldValue(); } final protected function getNewValue() { return $this->getStorage()->getNewValue(); } final protected function renderAuthor() { $author_phid = $this->getAuthorPHID(); return $this->getStorage()->renderHandleLink($author_phid); } final protected function renderObject() { $object_phid = $this->getObjectPHID(); return $this->getStorage()->renderHandleLink($object_phid); } final protected function renderHandle($phid) { $viewer = $this->getViewer(); $display = $viewer->renderHandle($phid); if ($this->isTextMode()) { $display->setAsText(true); } return $display; } final protected function renderOldHandle() { return $this->renderHandle($this->getOldValue()); } final protected function renderNewHandle() { return $this->renderHandle($this->getNewValue()); } final protected function renderHandleList(array $phids) { $viewer = $this->getViewer(); $display = $viewer->renderHandleList($phids) ->setAsInline(true); if ($this->isTextMode()) { $display->setAsText(true); } return $display; } final protected function renderValue($value) { if ($this->isTextMode()) { return sprintf('"%s"', $value); } return phutil_tag( 'span', array( 'class' => 'phui-timeline-value', ), $value); } final protected function renderValueList(array $values) { $result = array(); foreach ($values as $value) { $result[] = $this->renderValue($value); } if ($this->isTextMode()) { return implode(', ', $result); } return phutil_implode_html(', ', $result); } final protected function renderOldValue() { return $this->renderValue($this->getOldValue()); } final protected function renderNewValue() { return $this->renderValue($this->getNewValue()); } final protected function renderDate($epoch) { $viewer = $this->getViewer(); // We accept either epoch timestamps or dictionaries describing a // PhutilCalendarDateTime. if (is_array($epoch)) { $datetime = PhutilCalendarAbsoluteDateTime::newFromDictionary($epoch) ->setViewerTimezone($viewer->getTimezoneIdentifier()); $all_day = $datetime->getIsAllDay(); $epoch = $datetime->getEpoch(); } else { $all_day = false; } if ($all_day) { $display = phabricator_date($epoch, $viewer); } else if ($this->isRenderingTargetExternal()) { // When rendering to text, we explicitly render the offset from UTC to // provide context to the date: the mail may be generating with the // server's settings, or the user may later refer back to it after // changing timezones. $display = phabricator_datetimezone($epoch, $viewer); } else { $display = phabricator_datetime($epoch, $viewer); } return $this->renderValue($display); } final protected function renderOldDate() { return $this->renderDate($this->getOldValue()); } final protected function renderNewDate() { return $this->renderDate($this->getNewValue()); } final protected function newError($title, $message, $xaction = null) { return new PhabricatorApplicationTransactionValidationError( $this->getTransactionTypeConstant(), $title, $message, $xaction); } final protected function newRequiredError($message, $xaction = null) { return $this->newError(pht('Required'), $message, $xaction) ->setIsMissingFieldError(true); } final protected function newInvalidError($message, $xaction = null) { return $this->newError(pht('Invalid'), $message, $xaction); } final protected function isNewObject() { return $this->getEditor()->getIsNewObject(); } final protected function isEmptyTextTransaction($value, array $xactions) { foreach ($xactions as $xaction) { $value = $xaction->getNewValue(); } return !strlen($value); } /** * When rendering to external targets (Email/Asana/etc), we need to include * more information that users can't obtain later. */ final protected function isRenderingTargetExternal() { // Right now, this is our best proxy for this: return $this->isTextMode(); // "TARGET_TEXT" means "EMail" and "TARGET_HTML" means "Web". } final protected function isTextMode() { $target = $this->getStorage()->getRenderingTarget(); return ($target == PhabricatorApplicationTransaction::TARGET_TEXT); } final protected function newRemarkupChange() { return id(new PhabricatorTransactionRemarkupChange()) ->setTransaction($this->getStorage()); } final protected function isCreateTransaction() { return $this->getStorage()->getIsCreateTransaction(); } final protected function getPHIDList(array $old, array $new) { $editor = $this->getEditor(); return $editor->getPHIDList($old, $new); } public function getMetadataValue($key, $default = null) { return $this->getStorage()->getMetadataValue($key, $default); } + public function loadTransactionTypeConduitData(array $xactions) { + return null; + } + + public function getTransactionTypeForConduit($xaction) { + return null; + } + + public function getFieldValuesForConduit($xaction, $data) { + return array(); + } + }