diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php index eab1a25a16..5b1c71ad9c 100644 --- a/src/applications/maniphest/controller/ManiphestBatchEditController.php +++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php @@ -1,326 +1,331 @@ getRequest(); $user = $request->getUser(); $task_ids = $request->getArr('batch'); $tasks = id(new ManiphestTask())->loadAllWhere( 'id IN (%Ld)', $task_ids); $actions = $request->getStr('actions'); if ($actions) { $actions = json_decode($actions, true); } if ($request->isFormPost() && is_array($actions)) { foreach ($tasks as $task) { + $field_list = PhabricatorCustomField::getObjectFields( + $task, + PhabricatorCustomField::ROLE_EDIT); + $field_list->readFieldsFromStorage($task); + $xactions = $this->buildTransactions($actions, $task); if ($xactions) { - $editor = new ManiphestTransactionEditor(); - $editor->setActor($user); - $editor->applyTransactions($task, $xactions); + // TODO: Set content source to "batch edit". + + $editor = id(new ManiphestTransactionEditorPro()) + ->setActor($user) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->applyTransactions($task, $xactions); } } $task_ids = implode(',', mpull($tasks, 'getID')); return id(new AphrontRedirectResponse()) ->setURI('/maniphest/?ids='.$task_ids); } $handle_phids = mpull($tasks, 'getOwnerPHID'); $handles = $this->loadViewerHandles($handle_phids); $list = new ManiphestTaskListView(); $list->setTasks($tasks); $list->setUser($user); $list->setHandles($handles); $template = new AphrontTokenizerTemplateView(); $template = $template->render(); require_celerity_resource('maniphest-batch-editor'); Javelin::initBehavior( 'maniphest-batch-editor', array( 'root' => 'maniphest-batch-edit-form', 'tokenizerTemplate' => $template, 'sources' => array( 'project' => array( 'src' => '/typeahead/common/projects/', 'placeholder' => pht('Type a project name...'), ), 'owner' => array( 'src' => '/typeahead/common/searchowner/', 'placeholder' => pht('Type a user name...'), 'limit' => 1, ), 'cc' => array( 'src' => '/typeahead/common/mailable/', 'placeholder' => pht('Type a user name...'), ) ), 'input' => 'batch-form-actions', 'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(), 'statusMap' => ManiphestTaskStatus::getTaskStatusMap(), )); $form = new AphrontFormView(); $form->setUser($user); $form->setID('maniphest-batch-edit-form'); foreach ($tasks as $task) { $form->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'batch[]', 'value' => $task->getID(), ))); } $form->appendChild( phutil_tag( 'input', array( 'type' => 'hidden', 'name' => 'actions', 'id' => 'batch-form-actions', ))); $form->appendChild( phutil_tag('p', array(), pht('These tasks will be edited:'))); $form->appendChild($list); $form->appendChild( id(new AphrontFormInsetView()) ->setTitle('Actions') ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'add-action', 'mustcapture' => true, ), pht('Add Another Action'))) ->setContent(javelin_tag( 'table', array( 'sigil' => 'maniphest-batch-actions', 'class' => 'maniphest-batch-actions-table', ), ''))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Update Tasks')) ->addCancelButton('/maniphest/')); $title = pht('Batch Editor'); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName($title)); $form_box = id(new PHUIFormBoxView()) ->setHeaderText(pht('Batch Edit Tasks')) ->setForm($form); return $this->buildApplicationPage( array( $crumbs, $form_box, ), array( 'title' => $title, )); } private function buildTransactions($actions, ManiphestTask $task) { $value_map = array(); $type_map = array( 'add_comment' => PhabricatorTransactions::TYPE_COMMENT, 'assign' => ManiphestTransactionType::TYPE_OWNER, 'status' => ManiphestTransactionType::TYPE_STATUS, 'priority' => ManiphestTransactionType::TYPE_PRIORITY, 'add_project' => ManiphestTransactionType::TYPE_PROJECTS, 'remove_project' => ManiphestTransactionType::TYPE_PROJECTS, 'add_ccs' => ManiphestTransactionType::TYPE_CCS, 'remove_ccs' => ManiphestTransactionType::TYPE_CCS, ); $edge_edit_types = array( 'add_project' => true, 'remove_project' => true, 'add_ccs' => true, 'remove_ccs' => true, ); $xactions = array(); foreach ($actions as $action) { if (empty($type_map[$action['action']])) { throw new Exception("Unknown batch edit action '{$action}'!"); } $type = $type_map[$action['action']]; // Figure out the current value, possibly after modifications by other // batch actions of the same type. For example, if the user chooses to // "Add Comment" twice, we should add both comments. More notably, if the // user chooses "Remove Project..." and also "Add Project...", we should // avoid restoring the removed project in the second transaction. if (array_key_exists($type, $value_map)) { $current = $value_map[$type]; } else { switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: $current = null; break; case ManiphestTransactionType::TYPE_OWNER: $current = $task->getOwnerPHID(); break; case ManiphestTransactionType::TYPE_STATUS: $current = $task->getStatus(); break; case ManiphestTransactionType::TYPE_PRIORITY: $current = $task->getPriority(); break; case ManiphestTransactionType::TYPE_PROJECTS: $current = $task->getProjectPHIDs(); break; case ManiphestTransactionType::TYPE_CCS: $current = $task->getCCPHIDs(); break; } } // Check if the value is meaningful / provided, and normalize it if // necessary. This discards, e.g., empty comments and empty owner // changes. $value = $action['value']; switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: if (!strlen($value)) { continue 2; } break; case ManiphestTransactionType::TYPE_OWNER: if (empty($value)) { continue 2; } $value = head($value); if ($value === ManiphestTaskOwner::OWNER_UP_FOR_GRABS) { $value = null; } break; case ManiphestTransactionType::TYPE_PROJECTS: if (empty($value)) { continue 2; } break; case ManiphestTransactionType::TYPE_CCS: if (empty($value)) { continue 2; } break; } // If the edit doesn't change anything, go to the next action. This // check is only valid for changes like "owner", "status", etc, not // for edge edits, because we should still apply an edit like // "Remove Projects: A, B" to a task with projects "A, B". if (empty($edge_edit_types[$action['action']])) { if ($value == $current) { continue; } } // Apply the value change; for most edits this is just replacement, but // some need to merge the current and edited values (add/remove project). switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: if (strlen($current)) { $value = $current."\n\n".$value; } break; case ManiphestTransactionType::TYPE_PROJECTS: case ManiphestTransactionType::TYPE_CCS: $remove_actions = array( 'remove_project' => true, 'remove_ccs' => true, ); $is_remove = isset($remove_actions[$action['action']]); $current = array_fill_keys($current, true); $value = array_fill_keys($value, true); $new = $current; $did_something = false; if ($is_remove) { foreach ($value as $phid => $ignored) { if (isset($new[$phid])) { unset($new[$phid]); $did_something = true; } } } else { foreach ($value as $phid => $ignored) { if (empty($new[$phid])) { $new[$phid] = true; $did_something = true; } } } if (!$did_something) { continue 2; } $value = array_keys($new); break; } $value_map[$type] = $value; } - $template = new ManiphestTransaction(); - $template->setAuthorPHID($this->getRequest()->getUser()->getPHID()); - - // TODO: Set content source to "batch edit". - - $template->setContentSource( - PhabricatorContentSource::newFromRequest($this->getRequest())); + $template = new ManiphestTransactionPro(); foreach ($value_map as $type => $value) { $xaction = clone $template; $xaction->setTransactionType($type); switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: - $xaction->setComments($value); + $xaction->attachComment( + id(new ManiphestTransactionComment()) + ->setContent($value)); break; default: $xaction->setNewValue($value); break; } $xactions[] = $xaction; } return $xactions; } } diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index b0f5bad0e3..f3ae5876fa 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -1,509 +1,510 @@ 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 PhabricatorTransactions::TYPE_COMMENT: $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 PhabricatorTransactions::TYPE_CUSTOMFIELD: $aux_key = $transaction->getMetadataValue('customfield:key'); if (!$aux_key) { throw new Exception( "Expected 'customfield:key' metadata on TYPE_CUSTOMFIELD ". "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( PhabricatorTransactions::TYPE_COMMENT); } } else { switch ($type) { case PhabricatorTransactions::TYPE_COMMENT: 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 PhabricatorTransactions::TYPE_CUSTOMFIELD: $aux_key = $transaction->getMetadataValue('customfield: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->setTransactionTask($task); $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(); $main_body = array(); foreach ($transactions as $transaction) { $main_body[] = id(clone $transaction->getModernTransaction()) + ->attachViewer($this->getActor()) ->setHandles($handles) ->setRenderingTarget('text') ->getTitle(); } foreach ($transactions as $transaction) { if ($transaction->getComments()) { $main_body[] = null; $main_body[] = $transaction->getComments(); } } $main_body = implode("\n", $main_body); $action = head($transactions)->getModernTransaction()->getActionName(); $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 PhabricatorTransactions::TYPE_COMMENT: // 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->setTransactionTask($task); $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->setTransactionTask($task); $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/transactions/query/PhabricatorApplicationTransactionQuery.php b/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php index 8ddad3a366..00523cffe4 100644 --- a/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php +++ b/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php @@ -1,159 +1,163 @@ phids = $phids; return $this; } public function withObjectPHIDs(array $object_phids) { $this->objectPHIDs = $object_phids; return $this; } public function withAuthorPHIDs(array $author_phids) { $this->authorPHIDs = $author_phids; return $this; } public function withTransactionTypes(array $transaction_types) { $this->transactionTypes = $transaction_types; return $this; } public function needComments($need) { $this->needComments = $need; return $this; } public function needHandles($need) { $this->needHandles = $need; return $this; } protected function loadPage() { $table = $this->getTemplateApplicationTransaction(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T x %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $xactions = $table->loadAllFromArray($data); + foreach ($xactions as $xaction) { + $xaction->attachViewer($this->getViewer()); + } + if ($this->needComments) { $comment_phids = array_filter(mpull($xactions, 'getCommentPHID')); $comments = array(); if ($comment_phids) { $comments = id(new PhabricatorApplicationTransactionCommentQuery()) ->setTemplate($table->getApplicationTransactionCommentObject()) ->setViewer($this->getViewer()) ->withPHIDs($comment_phids) ->execute(); $comments = mpull($comments, null, 'getPHID'); } foreach ($xactions as $xaction) { if ($xaction->getCommentPHID()) { $comment = idx($comments, $xaction->getCommentPHID()); if ($comment) { $xaction->attachComment($comment); } } } } else { foreach ($xactions as $xaction) { $xaction->setCommentNotLoaded(true); } } if ($this->needHandles) { $phids = array(); foreach ($xactions as $xaction) { $phids[$xaction->getPHID()] = $xaction->getRequiredHandlePHIDs(); } $handles = array(); $merged = array_mergev($phids); if ($merged) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getViewer()) ->withPHIDs($merged) ->execute(); } foreach ($xactions as $xaction) { $xaction->setHandles( array_select_keys( $handles, $phids[$xaction->getPHID()])); } } return $xactions; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid IN (%Ls)', $this->phids); } if ($this->objectPHIDs) { $where[] = qsprintf( $conn_r, 'objectPHID IN (%Ls)', $this->objectPHIDs); } if ($this->authorPHIDs) { $where[] = qsprintf( $conn_r, 'authorPHID IN (%Ls)', $this->authorPHIDs); } if ($this->transactionTypes) { $where[] = qsprintf( $conn_r, 'transactionType IN (%Ls)', $this->transactionTypes); } foreach ($this->buildMoreWhereClauses($conn_r) as $clause) { $where[] = $clause; } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 71e4bedd52..1f3089caf7 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -1,506 +1,517 @@ getApplicationTransactionType()); if ($type) { return $type->getTypeName(); } return pht('Object'); } public function getApplicationTransactionCommentObject() { throw new Exception("Not implemented!"); } public function getApplicationTransactionViewObject() { return new PhabricatorApplicationTransactionView(); } public function getMetadataValue($key, $default = null) { return idx($this->metadata, $key, $default); } public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function generatePHID() { $type = PhabricatorApplicationTransactionPHIDTypeTransaction::TYPECONST; $subtype = $this->getApplicationTransactionType(); return PhabricatorPHID::generateNewPHID($type, $subtype); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'oldValue' => self::SERIALIZATION_JSON, 'newValue' => self::SERIALIZATION_JSON, 'metadata' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source->serialize(); return $this; } public function getContentSource() { return PhabricatorContentSource::newFromSerialized($this->contentSource); } public function hasComment() { return $this->getComment() && strlen($this->getComment()->getContent()); } public function getComment() { if ($this->commentNotLoaded) { throw new Exception("Comment for this transaction was not loaded."); } return $this->comment; } public function attachComment( PhabricatorApplicationTransactionComment $comment) { $this->comment = $comment; $this->commentNotLoaded = false; return $this; } public function setCommentNotLoaded($not_loaded) { $this->commentNotLoaded = $not_loaded; return $this; } /* -( Rendering )---------------------------------------------------------- */ public function setRenderingTarget($rendering_target) { $this->renderingTarget = $rendering_target; return $this; } public function getRenderingTarget() { return $this->renderingTarget; } + public function attachViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->assertAttached($this->viewer); + } + public function getRequiredHandlePHIDs() { $phids = array(); $old = $this->getOldValue(); $new = $this->getNewValue(); $phids[] = array($this->getAuthorPHID()); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_SUBSCRIBERS: $phids[] = $old; $phids[] = $new; break; case PhabricatorTransactions::TYPE_EDGE: $phids[] = ipull($old, 'dst'); $phids[] = ipull($new, 'dst'); break; case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_VIEW_POLICY: if (!PhabricatorPolicyQuery::isGlobalPolicy($old)) { $phids[] = array($old); } if (!PhabricatorPolicyQuery::isGlobalPolicy($new)) { $phids[] = array($new); } break; } return array_mergev($phids); } public function setHandles(array $handles) { $this->handles = $handles; return $this; } public function getHandle($phid) { if (empty($this->handles[$phid])) { throw new Exception( "Transaction requires a handle ('{$phid}') it did not load."); } return $this->handles[$phid]; } public function getHandleIfExists($phid) { return idx($this->handles, $phid); } public function getHandles() { if ($this->handles === null) { throw new Exception( 'Transaction requires handles and it did not load them.' ); } return $this->handles; } public function renderHandleLink($phid) { if ($this->renderingTarget == self::TARGET_HTML) { return $this->getHandle($phid)->renderLink(); } else { return hsprintf('%s', $this->getHandle($phid)->getName()); } } public function renderHandleList(array $phids) { $links = array(); foreach ($phids as $phid) { $links[] = $this->renderHandleLink($phid); } return phutil_implode_html(', ', $links); } public function getIcon() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return 'comment'; case PhabricatorTransactions::TYPE_SUBSCRIBERS: return 'message'; case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: return 'lock'; case PhabricatorTransactions::TYPE_EDGE: return 'link'; } return null; } public function getColor() { return null; } public function shouldHide() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: if ($this->getOldValue() === null) { return true; } else { return false; } break; } return false; } public function shouldHideForMail() { return $this->shouldHide(); } public function getNoEffectDescription() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht('You can not post an empty comment.'); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( 'This %s already has that view policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( 'This %s already has that edit policy.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht( 'All users are already subscribed to this %s.', $this->getApplicationObjectTypeName()); case PhabricatorTransactions::TYPE_EDGE: return pht('Edges already exist; transaction has no effect.'); } return pht('Transaction has no effect.'); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht( '%s added a comment.', $this->renderHandleLink($author_phid)); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( '%s changed the visibility of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), PhabricatorPolicy::newFromPolicyAndHandle( $old, $this->getHandleIfExists($old))->renderDescription(), PhabricatorPolicy::newFromPolicyAndHandle( $new, $this->getHandleIfExists($new))->renderDescription()); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( '%s changed the edit policy of this %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName(), PhabricatorPolicy::newFromPolicyAndHandle( $old, $this->getHandleIfExists($old))->renderDescription(), PhabricatorPolicy::newFromPolicyAndHandle( $new, $this->getHandleIfExists($new))->renderDescription()); case PhabricatorTransactions::TYPE_SUBSCRIBERS: $add = array_diff($new, $old); $rem = array_diff($old, $new); if ($add && $rem) { return pht( '%s edited subscriber(s), added %d: %s; removed %d: %s.', $this->renderHandleLink($author_phid), count($add), $this->renderHandleList($add), count($rem), $this->renderHandleList($rem)); } else if ($add) { return pht( '%s added %d subscriber(s): %s.', $this->renderHandleLink($author_phid), count($add), $this->renderHandleList($add)); } else if ($rem) { return pht( '%s removed %d subscriber(s): %s.', $this->renderHandleLink($author_phid), count($rem), $this->renderHandleList($rem)); } else { // This is used when rendering previews, before the user actually // selects any CCs. return pht( '%s updated subscribers...', $this->renderHandleLink($author_phid)); } break; case PhabricatorTransactions::TYPE_EDGE: $new = ipull($new, 'dst'); $old = ipull($old, 'dst'); $add = array_diff($new, $old); $rem = array_diff($old, $new); $type = $this->getMetadata('edge:type'); $type = head($type); if ($add && $rem) { $string = PhabricatorEdgeConfig::getEditStringForEdgeType($type); return pht( $string, $this->renderHandleLink($author_phid), count($add), $this->renderHandleList($add), count($rem), $this->renderHandleList($rem)); } else if ($add) { $string = PhabricatorEdgeConfig::getAddStringForEdgeType($type); return pht( $string, $this->renderHandleLink($author_phid), count($add), $this->renderHandleList($add)); } else { $string = PhabricatorEdgeConfig::getRemoveStringForEdgeType($type); return pht( $string, $this->renderHandleLink($author_phid), count($rem), $this->renderHandleList($rem)); } case PhabricatorTransactions::TYPE_CUSTOMFIELD: $key = $this->getMetadataValue('customfield:key'); $field = PhabricatorCustomField::getObjectField( // TODO: This is a giant hack, but we currently don't have a way to // get the contextual object and this pathway is only hit by // Maniphest. We should provide a way to get the actual object here. new ManiphestTask(), PhabricatorCustomField::ROLE_APPLICATIONTRANSACTIONS, $key); if ($field) { + $field->setViewer($this->getViewer()); return $field->getApplicationTransactionTitle($this); } else { return pht( '%s edited a custom field.', $this->renderHandleLink($author_phid)); } default: return pht( '%s edited this %s.', $this->renderHandleLink($author_phid), $this->getApplicationObjectTypeName()); } } public function getTitleForFeed(PhabricatorFeedStory $story) { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht( '%s added a comment to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_VIEW_POLICY: return pht( '%s changed the visibility for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht( '%s changed the edit policy for %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht( '%s updated subscribers of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case PhabricatorTransactions::TYPE_EDGE: $type = $this->getMetadata('edge:type'); $type = head($type); $string = PhabricatorEdgeConfig::getFeedStringForEdgeType($type); return pht( $string, $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } return $this->getTitle(); } public function getBodyForFeed(PhabricatorFeedStory $story) { $old = $this->getOldValue(); $new = $this->getNewValue(); $body = null; switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: $text = $this->getComment()->getContent(); $body = phutil_escape_html_newlines( phutil_utf8_shorten($text, 128)); break; } return $body; } public function getActionStrength() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return 0.5; } return 1.0; } public function getActionName() { switch ($this->getTransactionType()) { case PhabricatorTransactions::TYPE_COMMENT: return pht('Commented On'); case PhabricatorTransactions::TYPE_VIEW_POLICY: case PhabricatorTransactions::TYPE_EDIT_POLICY: return pht('Changed Policy'); case PhabricatorTransactions::TYPE_SUBSCRIBERS: return pht('Changed Subscribers'); default: return pht('Updated'); } } public function getMailTags() { return array(); } public function hasChangeDetails() { return false; } public function renderChangeDetails(PhabricatorUser $viewer) { return null; } public function attachTransactionGroup(array $group) { assert_instances_of($group, 'PhabricatorApplicationTransaction'); $this->transactionGroup = $group; return $this; } public function getTransactionGroup() { return $this->transactionGroup; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return ($viewer->getPHID() == $this->getAuthorPHID()); } }