diff --git a/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldSpecification.php b/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldSpecification.php index 82cdda49fa..18ab1d2915 100644 --- a/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldSpecification.php +++ b/src/applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldSpecification.php @@ -1,288 +1,239 @@ getObject(); } // TODO: Remove; obsolete. public function getUser() { return $this->getViewer(); } public function setLabel($val) { $this->label = $val; return $this; } public function getLabel() { return $this->label; } public function setAuxiliaryKey($val) { $this->auxiliaryKey = $val; return $this; } public function getAuxiliaryKey() { return 'std:maniphest:'.$this->auxiliaryKey; } public function setCaption($val) { $this->caption = $val; return $this; } public function getCaption() { return $this->caption; } public function setValue($val) { $this->value = $val; return $this; } public function getValue() { return $this->value; } public function validate() { return true; } public function isRequired() { return false; } public function setType($val) { $this->type = $val; return $this; } public function getType() { return $this->type; } public function renderControl() { return null; } public function renderForDetailView() { return $this->getValue(); } - - /** - * Render a verb to appear in email titles when a transaction involving this - * field occurs. Specifically, Maniphest emails are formatted like this: - * - * [Maniphest] [Verb Here] TNNN: Task title here - * ^^^^^^^^^ - * - * You should optionally return a title-case verb or short phrase like - * "Created", "Retitled", "Closed", "Resolved", "Commented On", - * "Lowered Priority", etc., which describes the transaction. - * - * @param ManiphestTransaction The transaction which needs description. - * @return string|null A short description of the transaction. - */ - public function renderTransactionEmailVerb( - ManiphestTransaction $transaction) { - return null; - } - - - /** - * Render a short description of the transaction, to appear above comments - * in the Maniphest transaction log. The string will be rendered after the - * acting user's name. Examples are: - * - * added a comment - * added alincoln to CC - * claimed this task - * created this task - * closed this task out of spite - * - * You should return a similar string, describing the transaction. - * - * Note the ##$target## parameter -- Maniphest needs to render transaction - * descriptions for different targets, like web and email. This method will - * be called with a ##ManiphestAuxiliaryFieldSpecification::RENDER_TARGET_*## - * constant describing the intended target. - * - * @param ManiphestTransaction The transaction which needs description. - * @param const Constant describing the rendering target (e.g., html or text). - * @return string|null Description of the transaction. - */ - public function renderTransactionDescription( - ManiphestTransaction $transaction, - $target) { - return 'updated a custom field'; - } - public function getRequiredHandlePHIDs() { return array(); } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = array_select_keys( $handles, $this->getRequiredHandlePHIDs()); return $this; } public function getHandle($phid) { if (empty($this->handles[$phid])) { throw new Exception( "Field is requesting a handle ('{$phid}') it did not require."); } return $this->handles[$phid]; } public function getMarkupFields() { return array(); } public function setMarkupEngine(PhabricatorMarkupEngine $engine) { $this->markupEngine = $engine; return $this; } public function getMarkupEngine() { return $this->markupEngine; } /* -( PhabricatorMarkupInterface )----------------------------------------- */ public function getMarkupFieldKey($field) { $hash = PhabricatorHash::digestForIndex($this->getMarkupText($field)); return 'maux:'.$this->getAuxiliaryKey().':'.$hash; } public function newMarkupEngine($field) { return PhabricatorMarkupEngine::newManiphestMarkupEngine(); } public function getMarkupText($field) { return $this->getValue(); } public function didMarkupText( $field, $output, PhutilMarkupEngine $engine) { return phutil_tag( 'div', array( 'class' => 'phabricator-remarkup', ), $output); } public function shouldUseMarkupCache($field) { return true; } /* -( API Compatibility With New Custom Fields )--------------------------- */ public function getFieldKey() { return $this->getAuxiliaryKey(); } public function shouldAppearInEditView() { return true; } public function shouldAppearInPropertyView() { return true; } public function shouldUseStorage() { return true; } public function renderPropertyViewValue() { return $this->renderForDetailView(); } public function renderPropertyViewLabel() { return $this->getLabel(); } public function readValueFromRequest(AphrontRequest $request) { return $this->setValueFromRequest($request); } public static function writeLegacyAuxiliaryUpdates( ManiphestTask $task, array $map) { $table = new ManiphestCustomFieldStorage(); $conn_w = $table->establishConnection('w'); $update = array(); $remove = array(); foreach ($map as $key => $value) { $index = PhabricatorHash::digestForIndex($key); if ($value === null) { $remove[$index] = true; } else { $update[$index] = $value; } } if ($remove) { queryfx( $conn_w, 'DELETE FROM %T WHERE objectPHID = %s AND fieldIndex IN (%Ls)', $table->getTableName(), $task->getPHID(), array_keys($remove)); } if ($update) { $sql = array(); foreach ($update as $index => $val) { $sql[] = qsprintf( $conn_w, '(%s, %s, %s)', $task->getPHID(), $index, $val); } queryfx( $conn_w, 'INSERT INTO %T (objectPHID, fieldIndex, fieldValue) VALUES %Q ON DUPLICATE KEY UPDATE fieldValue = VALUES(fieldValue)', $table->getTableName(), implode(', ', $sql)); } } } diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index b5bf62f196..00034da47f 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -1,487 +1,494 @@ auxiliaryFields = $fields; return $this; } public function setParentMessageID($parent_message_id) { $this->parentMessageID = $parent_message_id; return $this; } public function applyTransactions(ManiphestTask $task, array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $email_cc = $task->getCCPHIDs(); $email_to = array(); $email_to[] = $task->getOwnerPHID(); $pri_changed = $this->isCreate($transactions); $aux_writes = array(); foreach ($transactions as $key => $transaction) { $type = $transaction->getTransactionType(); $new = $transaction->getNewValue(); $email_to[] = $transaction->getAuthorPHID(); $value_is_phid_set = false; switch ($type) { case ManiphestTransactionType::TYPE_NONE: $old = null; break; case ManiphestTransactionType::TYPE_STATUS: $old = $task->getStatus(); break; case ManiphestTransactionType::TYPE_OWNER: $old = $task->getOwnerPHID(); break; case ManiphestTransactionType::TYPE_CCS: $old = $task->getCCPHIDs(); $value_is_phid_set = true; break; case ManiphestTransactionType::TYPE_PRIORITY: $old = $task->getPriority(); break; case ManiphestTransactionType::TYPE_EDGE: $old = $transaction->getOldValue(); break; case ManiphestTransactionType::TYPE_ATTACH: $old = $task->getAttached(); break; case ManiphestTransactionType::TYPE_TITLE: $old = $task->getTitle(); break; case ManiphestTransactionType::TYPE_DESCRIPTION: $old = $task->getDescription(); break; case ManiphestTransactionType::TYPE_PROJECTS: $old = $task->getProjectPHIDs(); $value_is_phid_set = true; break; case ManiphestTransactionType::TYPE_AUXILIARY: $aux_key = $transaction->getMetadataValue('aux:key'); if (!$aux_key) { throw new Exception( "Expected 'aux:key' metadata on TYPE_AUXILIARY transaction."); } // This has already been populated. $old = $transaction->getOldValue(); break; default: throw new Exception('Unknown action type.'); } $old_cmp = $old; $new_cmp = $new; if ($value_is_phid_set) { // Normalize the old and new values if they are PHID sets so we don't // get any no-op transactions where the values differ only by keys, // order, duplicates, etc. if (is_array($old)) { $old = array_filter($old); $old = array_unique($old); sort($old); $old = array_values($old); $old_cmp = $old; } if (is_array($new)) { $new = array_filter($new); $new = array_unique($new); $transaction->setNewValue($new); $new_cmp = $new; sort($new_cmp); $new_cmp = array_values($new_cmp); } } if (($old !== null) && ($old_cmp == $new_cmp)) { if (count($transactions) > 1 && !$transaction->hasComments()) { // If we have at least one other transaction and this one isn't // doing anything and doesn't have any comments, just throw it // away. unset($transactions[$key]); continue; } else { $transaction->setOldValue(null); $transaction->setNewValue(null); $transaction->setTransactionType(ManiphestTransactionType::TYPE_NONE); } } else { switch ($type) { case ManiphestTransactionType::TYPE_NONE: break; case ManiphestTransactionType::TYPE_STATUS: $task->setStatus($new); break; case ManiphestTransactionType::TYPE_OWNER: if ($new) { $handle = id(new PhabricatorHandleQuery()) ->setViewer($this->getActor()) ->withPHIDs(array($new)) ->executeOne(); $task->setOwnerOrdering($handle->getName()); } else { $task->setOwnerOrdering(null); } $task->setOwnerPHID($new); break; case ManiphestTransactionType::TYPE_CCS: $task->setCCPHIDs($new); break; case ManiphestTransactionType::TYPE_PRIORITY: $task->setPriority($new); $pri_changed = true; break; case ManiphestTransactionType::TYPE_ATTACH: $task->setAttached($new); break; case ManiphestTransactionType::TYPE_TITLE: $task->setTitle($new); break; case ManiphestTransactionType::TYPE_DESCRIPTION: $task->setDescription($new); break; case ManiphestTransactionType::TYPE_PROJECTS: $task->setProjectPHIDs($new); break; case ManiphestTransactionType::TYPE_AUXILIARY: $aux_key = $transaction->getMetadataValue('aux:key'); $aux_writes[$aux_key] = $new; break; case ManiphestTransactionType::TYPE_EDGE: // Edge edits are accomplished through PhabricatorEdgeEditor, which // has authority. break; default: throw new Exception('Unknown action type.'); } $transaction->setOldValue($old); $transaction->setNewValue($new); } } if ($pri_changed) { $subpriority = ManiphestTransactionEditor::getNextSubpriority( $task->getPriority(), null); $task->setSubpriority($subpriority); } $task->save(); if ($aux_writes) { ManiphestAuxiliaryFieldSpecification::writeLegacyAuxiliaryUpdates( $task, $aux_writes); } foreach ($transactions as $transaction) { $transaction->setTaskID($task->getID()); $transaction->save(); } $email_to[] = $task->getOwnerPHID(); $email_cc = array_merge( $email_cc, $task->getCCPHIDs()); $mail = $this->sendEmail($task, $transactions, $email_to, $email_cc); $this->publishFeedStory( $task, $transactions, $mail->buildRecipientList()); id(new PhabricatorSearchIndexer()) ->indexDocumentByPHID($task->getPHID()); + + $fields = PhabricatorCustomField::getObjectFields( + $task, + PhabricatorCustomField::ROLE_APPLICATIONSEARCH); + $fields->readFieldsFromStorage($task); + $fields->rebuildIndexes($task); + } protected function getSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix'); } private function sendEmail($task, $transactions, $email_to, $email_cc) { $email_to = array_filter(array_unique($email_to)); $email_cc = array_filter(array_unique($email_cc)); $phids = array(); foreach ($transactions as $transaction) { foreach ($transaction->extractPHIDs() as $phid) { $phids[$phid] = true; } } foreach ($email_to as $phid) { $phids[$phid] = true; } foreach ($email_cc as $phid) { $phids[$phid] = true; } $phids = array_keys($phids); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getActor()) ->withPHIDs($phids) ->execute(); $view = new ManiphestTransactionDetailView(); $view->setTransactionGroup($transactions); $view->setHandles($handles); $view->setAuxiliaryFields($this->auxiliaryFields); list($action, $main_body) = $view->renderForEmail($with_date = false); $is_create = $this->isCreate($transactions); $task_uri = PhabricatorEnv::getProductionURI('/T'.$task->getID()); $reply_handler = $this->buildReplyHandler($task); $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection($main_body); if ($is_create) { $body->addTextSection(pht('TASK DESCRIPTION'), $task->getDescription()); } $body->addTextSection(pht('TASK DETAIL'), $task_uri); $body->addReplySection($reply_handler->getReplyHandlerInstructions()); $thread_id = 'maniphest-task-'.$task->getPHID(); $task_id = $task->getID(); $title = $task->getTitle(); $mailtags = $this->getMailTags($transactions); $template = id(new PhabricatorMetaMTAMail()) ->setSubject("T{$task_id}: {$title}") ->setSubjectPrefix($this->getSubjectPrefix()) ->setVarySubjectPrefix("[{$action}]") ->setFrom($transaction->getAuthorPHID()) ->setParentMessageID($this->parentMessageID) ->addHeader('Thread-Topic', "T{$task_id}: ".$task->getOriginalTitle()) ->setThreadID($thread_id, $is_create) ->setRelatedPHID($task->getPHID()) ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) ->setIsBulk(true) ->setMailTags($mailtags) ->setBody($body->render()); $mails = $reply_handler->multiplexMail( $template, array_select_keys($handles, $email_to), array_select_keys($handles, $email_cc)); foreach ($mails as $mail) { $mail->saveAndSend(); } $template->addTos($email_to); $template->addCCs($email_cc); return $template; } public function buildReplyHandler(ManiphestTask $task) { $handler_object = PhabricatorEnv::newObjectFromConfig( 'metamta.maniphest.reply-handler'); $handler_object->setMailReceiver($task); return $handler_object; } private function publishFeedStory( ManiphestTask $task, array $transactions, array $mailed_phids) { assert_instances_of($transactions, 'ManiphestTransaction'); $actions = array(ManiphestAction::ACTION_UPDATE); $comments = null; foreach ($transactions as $transaction) { if ($transaction->hasComments()) { $comments = $transaction->getComments(); } $type = $transaction->getTransactionType(); switch ($type) { case ManiphestTransactionType::TYPE_OWNER: $actions[] = ManiphestAction::ACTION_ASSIGN; break; case ManiphestTransactionType::TYPE_STATUS: if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) { $actions[] = ManiphestAction::ACTION_CLOSE; } else if ($this->isCreate($transactions)) { $actions[] = ManiphestAction::ACTION_CREATE; } else { $actions[] = ManiphestAction::ACTION_REOPEN; } break; default: $actions[] = $type; break; } } $action_type = ManiphestAction::selectStrongestAction($actions); $owner_phid = $task->getOwnerPHID(); $actor_phid = head($transactions)->getAuthorPHID(); $author_phid = $task->getAuthorPHID(); id(new PhabricatorFeedStoryPublisher()) ->setStoryType('PhabricatorFeedStoryManiphest') ->setStoryData(array( 'taskPHID' => $task->getPHID(), 'transactionIDs' => mpull($transactions, 'getID'), 'ownerPHID' => $owner_phid, 'action' => $action_type, 'comments' => $comments, 'description' => $task->getDescription(), )) ->setStoryTime(time()) ->setStoryAuthorPHID($actor_phid) ->setRelatedPHIDs( array_merge( array_filter( array( $task->getPHID(), $author_phid, $actor_phid, $owner_phid, )), $task->getProjectPHIDs())) ->setPrimaryObjectPHID($task->getPHID()) ->setSubscribedPHIDs( array_merge( array_filter( array( $author_phid, $owner_phid, $actor_phid)), $task->getCCPHIDs())) ->setMailRecipientPHIDs($mailed_phids) ->publish(); } private function isCreate(array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $is_create = false; foreach ($transactions as $transaction) { $type = $transaction->getTransactionType(); if (($type == ManiphestTransactionType::TYPE_STATUS) && ($transaction->getOldValue() === null) && ($transaction->getNewValue() == ManiphestTaskStatus::STATUS_OPEN)) { $is_create = true; } } return $is_create; } private function getMailTags(array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $tags = array(); foreach ($transactions as $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTransactionType::TYPE_STATUS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_STATUS; break; case ManiphestTransactionType::TYPE_OWNER: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OWNER; break; case ManiphestTransactionType::TYPE_CCS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_CC; break; case ManiphestTransactionType::TYPE_PROJECTS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS; break; case ManiphestTransactionType::TYPE_PRIORITY: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PRIORITY; break; case ManiphestTransactionType::TYPE_NONE: // this is a comment which we will check separately below for // content break; default: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OTHER; break; } if ($xaction->hasComments()) { $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_COMMENT; } } return array_unique($tags); } public static function getNextSubpriority($pri, $sub) { if ($sub === null) { $next = id(new ManiphestTask())->loadOneWhere( 'priority = %d ORDER BY subpriority ASC LIMIT 1', $pri); if ($next) { return $next->getSubpriority() - ((double)(2 << 16)); } } else { $next = id(new ManiphestTask())->loadOneWhere( 'priority = %d AND subpriority > %s ORDER BY subpriority ASC LIMIT 1', $pri, $sub); if ($next) { return ($sub + $next->getSubpriority()) / 2; } } return (double)(2 << 32); } public static function addCC( ManiphestTask $task, PhabricatorUser $user) { $current_ccs = $task->getCCPHIDs(); $new_ccs = array_merge($current_ccs, array($user->getPHID())); $transaction = new ManiphestTransaction(); $transaction->setTaskID($task->getID()); $transaction->setAuthorPHID($user->getPHID()); $transaction->setTransactionType(ManiphestTransactionType::TYPE_CCS); $transaction->setNewValue(array_unique($new_ccs)); $transaction->setOldValue($current_ccs); id(new ManiphestTransactionEditor()) ->setActor($user) ->applyTransactions($task, array($transaction)); } public static function removeCC( ManiphestTask $task, PhabricatorUser $user) { $current_ccs = $task->getCCPHIDs(); $new_ccs = array_diff($current_ccs, array($user->getPHID())); $transaction = new ManiphestTransaction(); $transaction->setTaskID($task->getID()); $transaction->setAuthorPHID($user->getPHID()); $transaction->setTransactionType(ManiphestTransactionType::TYPE_CCS); $transaction->setNewValue(array_unique($new_ccs)); $transaction->setOldValue($current_ccs); id(new ManiphestTransactionEditor()) ->setActor($user) ->applyTransactions($task, array($transaction)); } } diff --git a/src/applications/maniphest/field/ManiphestCustomField.php b/src/applications/maniphest/field/ManiphestCustomField.php index 6fd679f0a5..103d4934b3 100644 --- a/src/applications/maniphest/field/ManiphestCustomField.php +++ b/src/applications/maniphest/field/ManiphestCustomField.php @@ -1,32 +1,104 @@ renderEditControl(); + } + + public function validate() { + return true; + } + + /** + * Render a verb to appear in email titles when a transaction involving this + * field occurs. Specifically, Maniphest emails are formatted like this: + * + * [Maniphest] [Verb Here] TNNN: Task title here + * ^^^^^^^^^ + * + * You should optionally return a title-case verb or short phrase like + * "Created", "Retitled", "Closed", "Resolved", "Commented On", + * "Lowered Priority", etc., which describes the transaction. + * + * @param ManiphestTransaction The transaction which needs description. + * @return string|null A short description of the transaction. + */ + public function renderTransactionEmailVerb( + ManiphestTransaction $transaction) { + return null; + } + + + /** + * Render a short description of the transaction, to appear above comments + * in the Maniphest transaction log. The string will be rendered after the + * acting user's name. Examples are: + * + * added a comment + * added alincoln to CC + * claimed this task + * created this task + * closed this task out of spite + * + * You should return a similar string, describing the transaction. + * + * Note the ##$target## parameter -- Maniphest needs to render transaction + * descriptions for different targets, like web and email. This method will + * be called with a ##ManiphestAuxiliaryFieldSpecification::RENDER_TARGET_*## + * constant describing the intended target. + * + * @param ManiphestTransaction The transaction which needs description. + * @param const Constant describing the rendering target (e.g., html or text). + * @return string|null Description of the transaction. + */ + public function renderTransactionDescription( + ManiphestTransaction $transaction, + $target) { + return 'updated a custom field'; + } + + public function getMarkupFields() { + return array(); + } + } diff --git a/src/applications/maniphest/view/ManiphestTransactionDetailView.php b/src/applications/maniphest/view/ManiphestTransactionDetailView.php index f08bcb1f95..b4feeb349c 100644 --- a/src/applications/maniphest/view/ManiphestTransactionDetailView.php +++ b/src/applications/maniphest/view/ManiphestTransactionDetailView.php @@ -1,859 +1,859 @@ auxiliaryFields = mpull($fields, null, 'getAuxiliaryKey'); + $this->auxiliaryFields = mpull($fields, null, 'getFieldKey'); return $this; } public function getAuxiliaryField($key) { return idx($this->auxiliaryFields, $key); } public function setTransactionGroup(array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $this->transactions = $transactions; return $this; } public function setHandles(array $handles) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $this->handles = $handles; return $this; } public function setMarkupEngine(PhabricatorMarkupEngine $engine) { $this->markupEngine = $engine; return $this; } public function setPreview($preview) { $this->preview = $preview; return $this; } public function setRenderSummaryOnly($render_summary_only) { $this->renderSummaryOnly = $render_summary_only; return $this; } public function getRenderSummaryOnly() { return $this->renderSummaryOnly; } public function setRenderFullSummary($render_full_summary) { $this->renderFullSummary = $render_full_summary; return $this; } public function getRenderFullSummary() { return $this->renderFullSummary; } public function setCommentNumber($comment_number) { $this->commentNumber = $comment_number; return $this; } public function setRangeSpecification($range) { $this->rangeSpecification = $range; return $this; } public function getRangeSpecification() { return $this->rangeSpecification; } public function renderForEmail($with_date) { $this->forEmail = true; $transaction = reset($this->transactions); $author = $this->renderHandles(array($transaction->getAuthorPHID())); $action = null; $descs = array(); $comments = null; foreach ($this->transactions as $transaction) { list($verb, $desc, $classes) = $this->describeAction($transaction); if ($desc === null) { continue; } if ($action === null) { $action = $verb; } $desc = $author.' '.$desc.'.'; if ($with_date) { // NOTE: This is going into a (potentially multi-recipient) email so // we can't use a single user's timezone preferences. Use the server's // instead, but make the timezone explicit. $datetime = date('M jS \a\t g:i A T', $transaction->getDateCreated()); $desc = "On {$datetime}, {$desc}"; } $descs[] = $desc; if ($transaction->hasComments()) { $comments = $transaction->getComments(); } } $descs = implode("\n", $descs); if ($comments) { $descs .= "\n".$comments; } foreach ($this->transactions as $transaction) { $supplemental = $this->renderSupplementalInfoForEmail($transaction); if ($supplemental) { $descs .= "\n\n".$supplemental; } } $this->forEmail = false; return array($action, $descs); } public function render() { if (!$this->user) { throw new Exception("Call setUser() before render()!"); } $handles = $this->handles; $transactions = $this->transactions; require_celerity_resource('maniphest-transaction-detail-css'); $comment_transaction = null; foreach ($this->transactions as $transaction) { if ($transaction->hasComments()) { $comment_transaction = $transaction; break; } } $any_transaction = reset($transactions); $author = $this->handles[$any_transaction->getAuthorPHID()]; $more_classes = array(); $descs = array(); foreach ($transactions as $transaction) { list($verb, $desc, $classes) = $this->describeAction($transaction); if ($desc === null) { continue; } $more_classes = array_merge($more_classes, $classes); $full_summary = null; if ($this->getRenderFullSummary()) { $full_summary = $this->renderFullSummary($transaction); } $descs[] = javelin_tag( 'div', array( 'sigil' => 'maniphest-transaction-description', ), array( $author->renderLink(), ' ', $desc, '.', $full_summary, )); } if ($this->getRenderSummaryOnly()) { return phutil_implode_html("\n", $descs); } if ($comment_transaction && $comment_transaction->hasComments()) { $comment_block = $this->markupEngine->getOutput( $comment_transaction, ManiphestTransaction::MARKUP_FIELD_BODY); $comment_block = phutil_tag( 'div', array('class' => 'maniphest-transaction-comments phabricator-remarkup'), $comment_block); } else { $comment_block = null; } $source_transaction = nonempty($comment_transaction, $any_transaction); $xaction_view = id(new PhabricatorTransactionView()) ->setUser($this->user) ->setImageURI($author->getImageURI()) ->setContentSource($source_transaction->getContentSource()) ->setActions($descs); foreach ($more_classes as $class) { $xaction_view->addClass($class); } if ($this->preview) { $xaction_view->setIsPreview($this->preview); } else { $xaction_view->setEpoch($any_transaction->getDateCreated()); if ($this->commentNumber) { $anchor_name = 'comment-'.$this->commentNumber; $anchor_text = 'T'.$any_transaction->getTaskID(). '#'.$this->commentNumber; $xaction_view->setAnchor($anchor_name, $anchor_text); } } $xaction_view->appendChild($comment_block); return $xaction_view->render(); } private function renderSupplementalInfoForEmail($transaction) { $handles = $this->handles; $type = $transaction->getTransactionType(); $new = $transaction->getNewValue(); $old = $transaction->getOldValue(); switch ($type) { case ManiphestTransactionType::TYPE_DESCRIPTION: return "NEW DESCRIPTION\n ".trim($new)."\n\n". "PREVIOUS DESCRIPTION\n ".trim($old); case ManiphestTransactionType::TYPE_ATTACH: $old_raw = nonempty($old, array()); $new_raw = nonempty($new, array()); $attach_types = array( DifferentialPHIDTypeRevision::TYPECONST, PhabricatorFilePHIDTypeFile::TYPECONST, ); foreach ($attach_types as $attach_type) { $old = array_keys(idx($old_raw, $attach_type, array())); $new = array_keys(idx($new_raw, $attach_type, array())); if ($old != $new) { break; } } $added = array_diff($new, $old); if (!$added) { break; } $links = array(); foreach (array_select_keys($handles, $added) as $handle) { $links[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI()); } $links = implode("\n", $links); switch ($attach_type) { case DifferentialPHIDTypeRevision::TYPECONST: $title = 'ATTACHED REVISIONS'; break; case PhabricatorFilePHIDTypeFile::TYPECONST: $title = 'ATTACHED FILES'; break; } return $title."\n".$links; case ManiphestTransactionType::TYPE_EDGE: $add = array_diff_key($new, $old); if (!$add) { break; } $links = array(); foreach ($add as $phid => $ignored) { $handle = $handles[$phid]; $links[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI()); } $links = implode("\n", $links); $edge_type = $transaction->getMetadataValue('edge:type'); $title = $this->getEdgeEmailTitle($edge_type, $add); return $title."\n".$links; default: break; } return null; } private function describeAction($transaction) { $verb = null; $desc = null; $classes = array(); $handles = $this->handles; $type = $transaction->getTransactionType(); $author_phid = $transaction->getAuthorPHID(); $new = $transaction->getNewValue(); $old = $transaction->getOldValue(); switch ($type) { case ManiphestTransactionType::TYPE_TITLE: $verb = 'Retitled'; $desc = 'changed the title from '.$this->renderString($old). ' to '.$this->renderString($new); break; case ManiphestTransactionType::TYPE_DESCRIPTION: $verb = 'Edited'; if ($this->forEmail || $this->getRenderFullSummary()) { $desc = 'updated the task description'; } else { $desc = 'updated the task description; '. $this->renderExpandLink($transaction); } break; case ManiphestTransactionType::TYPE_NONE: $verb = 'Commented On'; $desc = 'added a comment'; break; case ManiphestTransactionType::TYPE_OWNER: if ($transaction->getAuthorPHID() == $new) { $verb = 'Claimed'; $desc = 'claimed this task'; $classes[] = 'claimed'; } else if (!$new) { $verb = 'Up For Grabs'; $desc = 'placed this task up for grabs'; $classes[] = 'upforgrab'; } else if (!$old) { $verb = 'Assigned'; $desc = 'assigned this task to '.$this->renderHandles(array($new)); $classes[] = 'assigned'; } else { $verb = 'Reassigned'; $desc = 'reassigned this task from '. $this->renderHandles(array($old)). ' to '. $this->renderHandles(array($new)); $classes[] = 'reassigned'; } break; case ManiphestTransactionType::TYPE_CCS: $added = array_diff($new, $old); $removed = array_diff($old, $new); // can only add in preview so just show placeholder if nothing to add if ($this->preview && empty($added)) { $verb = 'Changed CC'; $desc = 'changed CCs..'; break; } if ($added && !$removed) { $verb = 'Added CC'; if (count($added) == 1) { $desc = 'added '.$this->renderHandles($added).' to CC'; } else { $desc = 'added CCs: '.$this->renderHandles($added); } } else if ($removed && !$added) { $verb = 'Removed CC'; if (count($removed) == 1) { $desc = 'removed '.$this->renderHandles($removed).' from CC'; } else { $desc = 'removed CCs: '.$this->renderHandles($removed); } } else { $verb = 'Changed CC'; $desc = 'changed CCs, added: '.$this->renderHandles($added).'; '. 'removed: '.$this->renderHandles($removed); } break; case ManiphestTransactionType::TYPE_EDGE: $edge_type = $transaction->getMetadataValue('edge:type'); $add = array_diff_key($new, $old); $rem = array_diff_key($old, $new); if ($add && !$rem) { $verb = $this->getEdgeAddVerb($edge_type); $desc = $this->getEdgeAddList($edge_type, $add); } else if ($rem && !$add) { $verb = $this->getEdgeRemVerb($edge_type); $desc = $this->getEdgeRemList($edge_type, $rem); } else { $verb = $this->getEdgeEditVerb($edge_type); $desc = $this->getEdgeEditList($edge_type, $add, $rem); } break; case ManiphestTransactionType::TYPE_PROJECTS: $added = array_diff($new, $old); $removed = array_diff($old, $new); // can only add in preview so just show placeholder if nothing to add if ($this->preview && empty($added)) { $verb = 'Changed Projects'; $desc = 'changed projects..'; break; } if ($added && !$removed) { $verb = 'Added Project'; if (count($added) == 1) { $desc = 'added project '.$this->renderHandles($added); } else { $desc = 'added projects: '.$this->renderHandles($added); } } else if ($removed && !$added) { $verb = 'Removed Project'; if (count($removed) == 1) { $desc = 'removed project '.$this->renderHandles($removed); } else { $desc = 'removed projects: '.$this->renderHandles($removed); } } else { $verb = 'Changed Projects'; $desc = 'changed projects, added: '.$this->renderHandles($added).'; '. 'removed: '.$this->renderHandles($removed); } break; case ManiphestTransactionType::TYPE_STATUS: if ($new == ManiphestTaskStatus::STATUS_OPEN) { if ($old) { $verb = 'Reopened'; $desc = 'reopened this task'; $classes[] = 'reopened'; } else { $verb = 'Created'; $desc = 'created this task'; $classes[] = 'created'; } } else if ($new == ManiphestTaskStatus::STATUS_CLOSED_SPITE) { $verb = 'Spited'; $desc = 'closed this task out of spite'; $classes[] = 'spited'; } else if ($new == ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE) { $verb = 'Merged'; $desc = 'closed this task as a duplicate'; $classes[] = 'duplicate'; } else { $verb = 'Closed'; $full = idx(ManiphestTaskStatus::getTaskStatusMap(), $new, '???'); $desc = 'closed this task as "'.$full.'"'; $classes[] = 'closed'; } break; case ManiphestTransactionType::TYPE_PRIORITY: $old_name = ManiphestTaskPriority::getTaskPriorityName($old); $new_name = ManiphestTaskPriority::getTaskPriorityName($new); if ($old == ManiphestTaskPriority::getDefaultPriority()) { $verb = 'Triaged'; $desc = 'triaged this task as "'.$new_name.'" priority'; } else if ($old > $new) { $verb = 'Lowered Priority'; $desc = 'lowered the priority of this task from "'.$old_name.'" to '. '"'.$new_name.'"'; } else { $verb = 'Raised Priority'; $desc = 'raised the priority of this task from "'.$old_name.'" to '. '"'.$new_name.'"'; } break; case ManiphestTransactionType::TYPE_ATTACH: if ($this->preview) { $verb = 'Changed Attached'; $desc = 'changed attachments..'; break; } $old_raw = nonempty($old, array()); $new_raw = nonempty($new, array()); foreach (array( DifferentialPHIDTypeRevision::TYPECONST, ManiphestPHIDTypeTask::TYPECONST, PhabricatorFilePHIDTypeFile::TYPECONST) as $attach_type) { $old = array_keys(idx($old_raw, $attach_type, array())); $new = array_keys(idx($new_raw, $attach_type, array())); if ($old != $new) { break; } } $added = array_diff($new, $old); $removed = array_diff($old, $new); $add_desc = $this->renderHandles($added); $rem_desc = $this->renderHandles($removed); if ($added && !$removed) { $verb = 'Attached'; $desc = 'attached '. $this->getAttachName($attach_type, count($added)).': '. $add_desc; } else if ($removed && !$added) { $verb = 'Detached'; $desc = 'detached '. $this->getAttachName($attach_type, count($removed)).': '. $rem_desc; } else { $verb = 'Changed Attached'; $desc = 'changed attached '. $this->getAttachName($attach_type, count($added) + count($removed)). ', added: '.$add_desc.'; '. 'removed: '.$rem_desc; } break; case ManiphestTransactionType::TYPE_AUXILIARY: $aux_key = $transaction->getMetadataValue('aux:key'); // TODO: Migrate all legacy data when everything migrates for T2217. $aux_field = $this->getAuxiliaryField($aux_key); if (!$aux_field) { $aux_field = $this->getAuxiliaryField('std:maniphest:'.$aux_key); } $verb = null; if ($aux_field) { $verb = $aux_field->renderTransactionEmailVerb($transaction); } if ($verb === null) { if ($old === null) { $verb = "Set Field"; } else if ($new === null) { $verb = "Removed Field"; } else { $verb = "Updated Field"; } } $desc = null; if ($aux_field) { $use_field = $aux_field; } else { $use_field = id(new ManiphestAuxiliaryFieldDefaultSpecification()) ->setFieldType( ManiphestAuxiliaryFieldDefaultSpecification::TYPE_STRING); } $desc = $use_field->renderTransactionDescription( $transaction, $this->forEmail ? ManiphestAuxiliaryFieldSpecification::RENDER_TARGET_TEXT : ManiphestAuxiliaryFieldSpecification::RENDER_TARGET_HTML); break; default: return array($type, ' brazenly '.$type."'d", $classes); } // TODO: [HTML] This code will all be rewritten when we switch to using // ApplicationTransactions. It does not handle HTML or translations // correctly right now. $desc = phutil_safe_html($desc); return array($verb, $desc, $classes); } private function renderFullSummary($transaction) { switch ($transaction->getTransactionType()) { case ManiphestTransactionType::TYPE_DESCRIPTION: $id = $transaction->getID(); $old_text = phutil_utf8_hard_wrap($transaction->getOldValue(), 80); $old_text = implode("\n", $old_text); $new_text = phutil_utf8_hard_wrap($transaction->getNewValue(), 80); $new_text = implode("\n", $new_text); $engine = new PhabricatorDifferenceEngine(); $changeset = $engine->generateChangesetFromFileContent($old_text, $new_text); $whitespace_mode = DifferentialChangesetParser::WHITESPACE_SHOW_ALL; $parser = new DifferentialChangesetParser(); $parser->setChangeset($changeset); $parser->setRenderingReference($id); $parser->setMarkupEngine($this->markupEngine); $parser->setWhitespaceMode($whitespace_mode); $spec = $this->getRangeSpecification(); list($range_s, $range_e, $mask) = DifferentialChangesetParser::parseRangeSpecification($spec); $output = $parser->render($range_s, $range_e, $mask); return $output; } return null; } private function renderExpandLink($transaction) { $id = $transaction->getID(); Javelin::initBehavior('maniphest-transaction-expand'); return javelin_tag( 'a', array( 'href' => '/maniphest/task/descriptionchange/'.$id.'/', 'sigil' => 'maniphest-expand-transaction', 'mustcapture' => true, ), 'show details'); } private function renderHandles($phids, $full = false) { $links = array(); foreach ($phids as $phid) { if ($this->forEmail) { if ($full) { $links[] = $this->handles[$phid]->getFullName(); } else { $links[] = $this->handles[$phid]->getName(); } } else { $links[] = $this->handles[$phid]->renderLink(); } } if ($this->forEmail) { return implode(', ', $links); } else { return phutil_implode_html(', ', $links); } } private function renderString($string) { if ($this->forEmail) { return '"'.$string.'"'; } else { return '"'.phutil_escape_html($string).'"'; } } /* -( Strings )------------------------------------------------------------ */ /** * @task strings */ private function getAttachName($attach_type, $count) { switch ($attach_type) { case DifferentialPHIDTypeRevision::TYPECONST: return pht('Differential Revision(s)', $count); case PhabricatorFilePHIDTypeFile::TYPECONST: return pht('file(s)', $count); case ManiphestPHIDTypeTask::TYPECONST: return pht('Maniphest Task(s)', $count); } } /** * @task strings */ private function getEdgeEmailTitle($type, array $list) { $count = count($list); switch ($type) { case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV: return pht('DIFFERENTIAL %d REVISION(S)', $count); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK: return pht('DEPENDS ON %d TASK(S)', $count); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK: return pht('DEPENDENT %d TASK(s)', $count); case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT: return pht('ATTACHED %d COMMIT(S)', $count); case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK: return pht('ATTACHED %d MOCK(S)', $count); default: return pht('ATTACHED %d OBJECT(S)', $count); } } /** * @task strings */ private function getEdgeAddVerb($type) { switch ($type) { case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV: return pht('Added Revision'); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK: return pht('Added Dependency'); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK: return pht('Added Dependent Task'); case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT: return pht('Added Commit'); case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK: return pht('Added Mock'); default: return pht('Added Object'); } } /** * @task strings */ private function getEdgeRemVerb($type) { switch ($type) { case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV: return pht('Removed Revision'); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK: return pht('Removed Dependency'); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK: return pht('Removed Dependent Task'); case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT: return pht('Removed Commit'); case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK: return pht('Removed Mock'); default: return pht('Removed Object'); } } /** * @task strings */ private function getEdgeEditVerb($type) { switch ($type) { case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV: return pht('Changed Revisions'); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK: return pht('Changed Dependencies'); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK: return pht('Changed Dependent Tasks'); case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT: return pht('Changed Commits'); case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK: return pht('Changed Mocks'); default: return pht('Changed Objects'); } } /** * @task strings */ private function getEdgeAddList($type, array $add) { $list = $this->renderHandles(array_keys($add), $full = true); $count = count($add); switch ($type) { case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV: return pht('added %d revision(s): %s', $count, $list); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK: return pht('added %d dependencie(s): %s', $count, $list); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK: return pht('added %d dependent task(s): %s', $count, $list); case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT: return pht('added %d commit(s): %s', $count, $list); case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK: return pht('added %d mock(s): %s', $count, $list); default: return pht('added %d object(s): %s', $count, $list); } } /** * @task strings */ private function getEdgeRemList($type, array $rem) { $list = $this->renderHandles(array_keys($rem), $full = true); $count = count($rem); switch ($type) { case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV: return pht('removed %d revision(s): %s', $count, $list); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK: return pht('removed %d dependencie(s): %s', $count, $list); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK: return pht('removed %d dependent task(s): %s', $count, $list); case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT: return pht('removed %d commit(s): %s', $count, $list); case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK: return pht('removed %d mock(s): %s', $count, $list); default: return pht('removed %d object(s): %s', $count, $list); } } /** * @task strings */ private function getEdgeEditList($type, array $add, array $rem) { $add_list = $this->renderHandles(array_keys($add), $full = true); $rem_list = $this->renderHandles(array_keys($rem), $full = true); $add_count = count($add_list); $rem_count = count($rem_list); switch ($type) { case PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV: return pht( 'changed %d revision(s), added %d: %s; removed %d: %s', $add_count + $rem_count, $add_count, $add_list, $rem_count, $rem_list); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK: return pht( 'changed %d dependencie(s), added %d: %s; removed %d: %s', $add_count + $rem_count, $add_count, $add_list, $rem_count, $rem_list); case PhabricatorEdgeConfig::TYPE_TASK_DEPENDED_ON_BY_TASK: return pht( 'changed %d dependent task(s), added %d: %s; removed %d: %s', $add_count + $rem_count, $add_count, $add_list, $rem_count, $rem_list); case PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT: return pht( 'changed %d commit(s), added %d: %s; removed %d: %s', $add_count + $rem_count, $add_count, $add_list, $rem_count, $rem_list); case PhabricatorEdgeConfig::TYPE_TASK_HAS_MOCK: return pht( 'changed %d mock(s), added %d: %s; removed %d: %s', $add_count + $rem_count, $add_count, $add_list, $rem_count, $rem_list); default: return pht( 'changed %d object(s), added %d: %s; removed %d: %s', $add_count + $rem_count, $add_count, $add_list, $rem_count, $rem_list); } } }