diff --git a/src/applications/maniphest/controller/ManiphestSubpriorityController.php b/src/applications/maniphest/controller/ManiphestSubpriorityController.php index 1dc95ad674..8ba98c957b 100644 --- a/src/applications/maniphest/controller/ManiphestSubpriorityController.php +++ b/src/applications/maniphest/controller/ManiphestSubpriorityController.php @@ -1,73 +1,63 @@ getRequest(); $user = $request->getUser(); if (!$request->validateCSRF()) { return new Aphront403Response(); } $task = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array($request->getInt('task'))) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$task) { return new Aphront404Response(); } if ($request->getInt('after')) { $after_task = id(new ManiphestTaskQuery()) ->setViewer($user) ->withIDs(array($request->getInt('after'))) ->executeOne(); if (!$after_task) { return new Aphront404Response(); } $after_pri = $after_task->getPriority(); $after_sub = $after_task->getSubpriority(); } else { $after_pri = $request->getInt('priority'); $after_sub = null; } - $new_sub = ManiphestTransactionEditor::getNextSubpriority( - $after_pri, - $after_sub); - - $task->setSubpriority($new_sub); - - if ($after_pri != $task->getPriority()) { - $xactions = array(); - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTransaction::TYPE_PRIORITY) - ->setNewValue($after_pri); - - $editor = id(new ManiphestTransactionEditor()) - ->setActor($user) - ->setContinueOnMissingFields(true) - ->setContinueOnNoEffect(true) - ->setContentSourceFromRequest($request); - - $editor->applyTransactions($task, $xactions); - } else { - $task->save(); - } + $xactions = array(id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY) + ->setNewValue(array( + 'newPriority' => $after_pri, + 'newSubpriorityBase' => $after_sub))); + $editor = id(new ManiphestTransactionEditor()) + ->setActor($user) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($task, $xactions); return id(new AphrontAjaxResponse())->setContent( array( 'tasks' => $this->renderSingleTask($task), )); } } diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 7a99c1cb36..a899192006 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -1,333 +1,374 @@ getTransactionType()) { case ManiphestTransaction::TYPE_PRIORITY: if ($this->getIsNewObject()) { return null; } return (int)$object->getPriority(); case ManiphestTransaction::TYPE_STATUS: if ($this->getIsNewObject()) { return null; } return (int)$object->getStatus(); case ManiphestTransaction::TYPE_TITLE: if ($this->getIsNewObject()) { return null; } return $object->getTitle(); case ManiphestTransaction::TYPE_DESCRIPTION: if ($this->getIsNewObject()) { return null; } return $object->getDescription(); case ManiphestTransaction::TYPE_OWNER: return nonempty($object->getOwnerPHID(), null); case ManiphestTransaction::TYPE_CCS: return array_values(array_unique($object->getCCPHIDs())); case ManiphestTransaction::TYPE_PROJECTS: return array_values(array_unique($object->getProjectPHIDs())); case ManiphestTransaction::TYPE_ATTACH: return $object->getAttached(); case ManiphestTransaction::TYPE_EDGE: // These are pre-populated. return $xaction->getOldValue(); + case ManiphestTransaction::TYPE_SUBPRIORITY: + return $object->getSubpriority(); } } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTransaction::TYPE_PRIORITY: case ManiphestTransaction::TYPE_STATUS: return (int)$xaction->getNewValue(); case ManiphestTransaction::TYPE_CCS: case ManiphestTransaction::TYPE_PROJECTS: return array_values(array_unique($xaction->getNewValue())); case ManiphestTransaction::TYPE_OWNER: return nonempty($xaction->getNewValue(), null); case ManiphestTransaction::TYPE_TITLE: case ManiphestTransaction::TYPE_DESCRIPTION: case ManiphestTransaction::TYPE_ATTACH: case ManiphestTransaction::TYPE_EDGE: + case ManiphestTransaction::TYPE_SUBPRIORITY: return $xaction->getNewValue(); } } protected function transactionHasEffect( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); switch ($xaction->getTransactionType()) { case ManiphestTransaction::TYPE_PROJECTS: case ManiphestTransaction::TYPE_CCS: sort($old); sort($new); return ($old !== $new); } return parent::transactionHasEffect($object, $xaction); } - protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTransaction::TYPE_PRIORITY: return $object->setPriority($xaction->getNewValue()); case ManiphestTransaction::TYPE_STATUS: return $object->setStatus($xaction->getNewValue()); case ManiphestTransaction::TYPE_TITLE: return $object->setTitle($xaction->getNewValue()); case ManiphestTransaction::TYPE_DESCRIPTION: return $object->setDescription($xaction->getNewValue()); case ManiphestTransaction::TYPE_OWNER: $phid = $xaction->getNewValue(); // Update the "ownerOrdering" column to contain the full name of the // owner, if the task is assigned. $handle = null; if ($phid) { $handle = id(new PhabricatorHandleQuery()) ->setViewer($this->getActor()) ->withPHIDs(array($phid)) ->executeOne(); } if ($handle) { $object->setOwnerOrdering($handle->getName()); } else { $object->setOwnerOrdering(null); } return $object->setOwnerPHID($phid); case ManiphestTransaction::TYPE_CCS: return $object->setCCPHIDs($xaction->getNewValue()); case ManiphestTransaction::TYPE_PROJECTS: return $object->setProjectPHIDs($xaction->getNewValue()); case ManiphestTransaction::TYPE_ATTACH: return $object->setAttached($xaction->getNewValue()); case ManiphestTransaction::TYPE_EDGE: // These are a weird, funky mess and are already being applied by the // time we reach this. return; + case ManiphestTransaction::TYPE_SUBPRIORITY: + $data = $xaction->getNewValue(); + $new_sub = $this->getNextSubpriority( + $data['newPriority'], + $data['newSubpriorityBase']); + $object->setSubpriority($new_sub); + return; } } + protected function expandTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + $xactions = parent::expandTransaction($object, $xaction); + switch ($xaction->getTransactionType()) { + case ManiphestTransaction::TYPE_SUBPRIORITY: + $data = $xaction->getNewValue(); + $new_pri = $data['newPriority']; + if ($new_pri != $object->getPriority()) { + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_PRIORITY) + ->setNewValue($new_pri); + } + break; + default: + break; + } + + return $xactions; + } + protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { - return true; + $should_mail = true; + if (count($xactions) == 1) { + $xaction = head($xactions); + switch ($xaction->getTransactionType()) { + case ManiphestTransaction::TYPE_SUBPRIORITY: + $should_mail = false; + break; + default: + $should_mail = true; + break; + } + } + return $should_mail; } protected function getMailSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix'); } protected function getMailThreadID(PhabricatorLiskDAO $object) { return 'maniphest-task-'.$object->getPHID(); } protected function getMailTo(PhabricatorLiskDAO $object) { return array( $object->getOwnerPHID(), $this->requireActor()->getPHID(), ); } protected function getMailCC(PhabricatorLiskDAO $object) { return $object->getCCPHIDs(); } protected function buildReplyHandler(PhabricatorLiskDAO $object) { return id(new ManiphestReplyHandler()) ->setMailReceiver($object); } protected function buildMailTemplate(PhabricatorLiskDAO $object) { $id = $object->getID(); $title = $object->getTitle(); return id(new PhabricatorMetaMTAMail()) ->setSubject("T{$id}: {$title}") ->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle()); } protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { $body = parent::buildMailBody($object, $xactions); if ($this->getIsNewObject()) { $body->addTextSection( pht('TASK DESCRIPTION'), $object->getDescription()); } $body->addTextSection( pht('TASK DETAIL'), PhabricatorEnv::getProductionURI('/T'.$object->getID())); return $body; } protected function supportsFeed() { return true; } protected function supportsSearch() { return true; } protected function supportsHerald() { return true; } protected function buildHeraldAdapter( PhabricatorLiskDAO $object, array $xactions) { return id(new HeraldManiphestTaskAdapter()) ->setTask($object); } protected function didApplyHeraldRules( PhabricatorLiskDAO $object, HeraldAdapter $adapter, HeraldTranscript $transcript) { $save_again = false; $cc_phids = $adapter->getCcPHIDs(); if ($cc_phids) { $existing_cc = $object->getCCPHIDs(); $new_cc = array_unique(array_merge($cc_phids, $existing_cc)); $object->setCCPHIDs($new_cc); $save_again = true; } $assign_phid = $adapter->getAssignPHID(); if ($assign_phid) { $object->setOwnerPHID($assign_phid); $save_again = true; } $project_phids = $adapter->getProjectPHIDs(); if ($project_phids) { $existing_projects = $object->getProjectPHIDs(); $new_projects = array_unique( array_merge($project_phids, $existing_projects)); $object->setProjectPHIDs($new_projects); $save_again = true; } if ($save_again) { $object->save(); } } protected function requireCapabilities( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { parent::requireCapabilities($object, $xaction); $app_capability_map = array( ManiphestTransaction::TYPE_PRIORITY => ManiphestCapabilityEditPriority::CAPABILITY, ManiphestTransaction::TYPE_STATUS => ManiphestCapabilityEditStatus::CAPABILITY, ManiphestTransaction::TYPE_PROJECTS => ManiphestCapabilityEditProjects::CAPABILITY, ManiphestTransaction::TYPE_OWNER => ManiphestCapabilityEditAssign::CAPABILITY, PhabricatorTransactions::TYPE_EDIT_POLICY => ManiphestCapabilityEditPolicies::CAPABILITY, PhabricatorTransactions::TYPE_VIEW_POLICY => ManiphestCapabilityEditPolicies::CAPABILITY, ); $transaction_type = $xaction->getTransactionType(); $app_capability = idx($app_capability_map, $transaction_type); if ($app_capability) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($this->getActor()) ->withClasses(array('PhabricatorApplicationManiphest')) ->executeOne(); PhabricatorPolicyFilter::requireCapability( $this->getActor(), $app, $app_capability); } } - public static function getNextSubpriority($pri, $sub) { - - // TODO: T603 Figure out what the policies here should be once this gets - // cleaned up. + private 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); } } diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php index a66b8500fe..627066cddd 100644 --- a/src/applications/maniphest/storage/ManiphestTransaction.php +++ b/src/applications/maniphest/storage/ManiphestTransaction.php @@ -1,669 +1,672 @@ getNewValue(); $old = $this->getOldValue(); switch ($this->getTransactionType()) { case self::TYPE_OWNER: if ($new) { $phids[] = $new; } if ($old) { $phids[] = $old; } break; case self::TYPE_CCS: case self::TYPE_PROJECTS: $phids = array_mergev( array( $phids, nonempty($old, array()), nonempty($new, array()), )); break; case self::TYPE_EDGE: $phids = array_mergev( array( $phids, array_keys(nonempty($old, array())), array_keys(nonempty($new, array())), )); break; case self::TYPE_ATTACH: $old = nonempty($old, array()); $new = nonempty($new, array()); $phids = array_mergev( array( $phids, array_keys(idx($new, 'FILE', array())), array_keys(idx($old, 'FILE', array())), )); break; } return $phids; } public function shouldHide() { switch ($this->getTransactionType()) { case self::TYPE_TITLE: case self::TYPE_DESCRIPTION: case self::TYPE_PRIORITY: if ($this->getOldValue() === null) { return true; } else { return false; } break; + case self::TYPE_SUBPRIORITY: + return true; } return false; } public function getActionStrength() { switch ($this->getTransactionType()) { case self::TYPE_STATUS: return 1.3; case self::TYPE_OWNER: return 1.2; case self::TYPE_PRIORITY: return 1.1; } return parent::getActionStrength(); } public function getColor() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_OWNER: if ($this->getAuthorPHID() == $new) { return 'green'; } else if (!$new) { return 'black'; } else if (!$old) { return 'green'; } else { return 'green'; } case self::TYPE_STATUS: if ($new == ManiphestTaskStatus::STATUS_OPEN) { return 'green'; } else { return 'black'; } case self::TYPE_PRIORITY: if ($old == ManiphestTaskPriority::getDefaultPriority()) { return 'green'; } else if ($old > $new) { return 'grey'; } else { return 'yellow'; } } return parent::getColor(); } public function getActionName() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: return pht('Retitled'); case self::TYPE_STATUS: switch ($new) { case ManiphestTaskStatus::STATUS_OPEN: if ($old === null) { return pht('Created'); } else { return pht('Reopened'); } case ManiphestTaskStatus::STATUS_CLOSED_SPITE: return pht('Spited'); case ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE: return pht('Merged'); default: return pht('Closed'); } case self::TYPE_DESCRIPTION: return pht('Edited'); case self::TYPE_OWNER: if ($this->getAuthorPHID() == $new) { return pht('Claimed'); } else if (!$new) { return pht('Up For Grabs'); } else if (!$old) { return pht('Assigned'); } else { return pht('Reassigned'); } case self::TYPE_CCS: return pht('Changed CC'); case self::TYPE_PROJECTS: return pht('Changed Projects'); case self::TYPE_PRIORITY: if ($old == ManiphestTaskPriority::getDefaultPriority()) { return pht('Triaged'); } else if ($old > $new) { return pht('Lowered Priority'); } else { return pht('Raised Priority'); } case self::TYPE_EDGE: case self::TYPE_ATTACH: return pht('Attached'); } return parent::getActionName(); } public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_OWNER: return 'user'; case self::TYPE_CCS: return 'meta-mta'; case self::TYPE_TITLE: return 'edit'; case self::TYPE_STATUS: switch ($new) { case ManiphestTaskStatus::STATUS_OPEN: return 'create'; case ManiphestTaskStatus::STATUS_CLOSED_SPITE: return 'dislike'; case ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE: return 'delete'; default: return 'check'; } case self::TYPE_DESCRIPTION: return 'edit'; case self::TYPE_PROJECTS: return 'project'; case self::TYPE_PRIORITY: if ($old == ManiphestTaskPriority::getDefaultPriority()) { return 'normal-priority'; return pht('Triaged'); } else if ($old > $new) { return 'lower-priority'; } else { return 'raise-priority'; } case self::TYPE_EDGE: case self::TYPE_ATTACH: return 'attach'; } return parent::getIcon(); } public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: return pht( '%s changed the title from "%s" to "%s".', $this->renderHandleLink($author_phid), $old, $new); case self::TYPE_DESCRIPTION: return pht( '%s edited the task description.', $this->renderHandleLink($author_phid)); case self::TYPE_STATUS: switch ($new) { case ManiphestTaskStatus::STATUS_OPEN: if ($old === null) { return pht( '%s created this task.', $this->renderHandleLink($author_phid)); } else { return pht( '%s reopened this task.', $this->renderHandleLink($author_phid)); } case ManiphestTaskStatus::STATUS_CLOSED_SPITE: return pht( '%s closed this task out of spite.', $this->renderHandleLink($author_phid)); case ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE: return pht( '%s closed this task as a duplicate.', $this->renderHandleLink($author_phid)); default: $status_name = idx( ManiphestTaskStatus::getTaskStatusMap(), $new, '???'); return pht( '%s closed this task as "%s".', $this->renderHandleLink($author_phid), $status_name); } case self::TYPE_OWNER: if ($author_phid == $new) { return pht( '%s claimed this task.', $this->renderHandleLink($author_phid)); } else if (!$new) { return pht( '%s placed this task up for grabs.', $this->renderHandleLink($author_phid)); } else if (!$old) { return pht( '%s assigned this task to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($new)); } else { return pht( '%s reassigned this task from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($old), $this->renderHandleLink($new)); } case self::TYPE_PROJECTS: $added = array_diff($new, $old); $removed = array_diff($old, $new); if ($added && !$removed) { return pht( '%s added %d project(s): %s', $this->renderHandleLink($author_phid), count($added), $this->renderHandleList($added)); } else if ($removed && !$added) { return pht( '%s removed %d project(s): %s', $this->renderHandleLink($author_phid), count($removed), $this->renderHandleList($removed)); } else if ($removed && $added) { return pht( '%s changed project(s), added %d: %s; removed %d: %s', $this->renderHandleLink($author_phid), count($added), $this->renderHandleList($added), count($removed), $this->renderHandleList($removed)); } else { // This is hit when rendering previews. return pht( '%s changed projects...', $this->renderHandleLink($author_phid)); } case self::TYPE_PRIORITY: $old_name = ManiphestTaskPriority::getTaskPriorityName($old); $new_name = ManiphestTaskPriority::getTaskPriorityName($new); if ($old == ManiphestTaskPriority::getDefaultPriority()) { return pht( '%s triaged this task as "%s" priority.', $this->renderHandleLink($author_phid), $new_name); } else if ($old > $new) { return pht( '%s lowered the priority of this task from "%s" to "%s".', $this->renderHandleLink($author_phid), $old_name, $new_name); } else { return pht( '%s raised the priority of this task from "%s" to "%s".', $this->renderHandleLink($author_phid), $old_name, $new_name); } case self::TYPE_CCS: // TODO: Remove this when we switch to subscribers. Just reuse the // code in the parent. $clone = clone $this; $clone->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); return $clone->getTitle(); case self::TYPE_EDGE: // TODO: Remove this when we switch to real edges. Just reuse the // code in the parent; $clone = clone $this; $clone->setTransactionType(PhabricatorTransactions::TYPE_EDGE); return $clone->getTitle(); case self::TYPE_ATTACH: $old = nonempty($old, array()); $new = nonempty($new, array()); $new = array_keys(idx($new, 'FILE', array())); $old = array_keys(idx($old, 'FILE', array())); $added = array_diff($new, $old); $removed = array_diff($old, $new); if ($added && !$removed) { return pht( '%s attached %d file(s): %s', $this->renderHandleLink($author_phid), count($added), $this->renderHandleList($added)); } else if ($removed && !$added) { return pht( '%s detached %d file(s): %s', $this->renderHandleLink($author_phid), count($removed), $this->renderHandleList($removed)); } else { return pht( '%s changed file(s), attached %d: %s; detached %d: %s', $this->renderHandleLink($author_phid), count($added), $this->renderHandleList($added), count($removed), $this->renderHandleList($removed)); } } return parent::getTitle(); } public function getTitleForFeed(PhabricatorFeedStory $story) { $author_phid = $this->getAuthorPHID(); $object_phid = $this->getObjectPHID(); $old = $this->getOldValue(); $new = $this->getNewValue(); switch ($this->getTransactionType()) { case self::TYPE_TITLE: return pht( '%s renamed %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old, $new); case self::TYPE_DESCRIPTION: return pht( '%s edited the description of %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case self::TYPE_STATUS: switch ($new) { case ManiphestTaskStatus::STATUS_OPEN: if ($old === null) { return pht( '%s created %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else { return pht( '%s reopened %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } case ManiphestTaskStatus::STATUS_CLOSED_SPITE: return pht( '%s closed %s out of spite.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); case ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE: return pht( '%s closed %s as a duplicate.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); default: $status_name = idx( ManiphestTaskStatus::getTaskStatusMap(), $new, '???'); return pht( '%s closed %s as "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $status_name); } case self::TYPE_OWNER: if ($author_phid == $new) { return pht( '%s claimed %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else if (!$new) { return pht( '%s placed %s up for grabs.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); } else if (!$old) { return pht( '%s assigned %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $this->renderHandleLink($new)); } else { return pht( '%s reassigned %s from %s to %s.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $this->renderHandleLink($old), $this->renderHandleLink($new)); } case self::TYPE_PROJECTS: $added = array_diff($new, $old); $removed = array_diff($old, $new); if ($added && !$removed) { return pht( '%s added %d project(s) to %s: %s', $this->renderHandleLink($author_phid), count($added), $this->renderHandleLink($object_phid), $this->renderHandleList($added)); } else if ($removed && !$added) { return pht( '%s removed %d project(s) from %s: %s', $this->renderHandleLink($author_phid), count($removed), $this->renderHandleLink($object_phid), $this->renderHandleList($removed)); } else if ($removed && $added) { return pht( '%s changed project(s) of %s, added %d: %s; removed %d: %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), count($added), $this->renderHandleList($added), count($removed), $this->renderHandleList($removed)); } case self::TYPE_PRIORITY: $old_name = ManiphestTaskPriority::getTaskPriorityName($old); $new_name = ManiphestTaskPriority::getTaskPriorityName($new); if ($old == ManiphestTaskPriority::getDefaultPriority()) { return pht( '%s triaged %s as "%s" priority.', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $new_name); } else if ($old > $new) { return pht( '%s lowered the priority of %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old_name, $new_name); } else { return pht( '%s raised the priority of %s from "%s" to "%s".', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), $old_name, $new_name); } case self::TYPE_CCS: // TODO: Remove this when we switch to subscribers. Just reuse the // code in the parent. $clone = clone $this; $clone->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); return $clone->getTitleForFeed($story); case self::TYPE_EDGE: // TODO: Remove this when we switch to real edges. Just reuse the // code in the parent; $clone = clone $this; $clone->setTransactionType(PhabricatorTransactions::TYPE_EDGE); return $clone->getTitleForFeed($story); case self::TYPE_ATTACH: $old = nonempty($old, array()); $new = nonempty($new, array()); $new = array_keys(idx($new, 'FILE', array())); $old = array_keys(idx($old, 'FILE', array())); $added = array_diff($new, $old); $removed = array_diff($old, $new); if ($added && !$removed) { return pht( '%s attached %d file(s) of %s: %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), count($added), $this->renderHandleList($added)); } else if ($removed && !$added) { return pht( '%s detached %d file(s) of %s: %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), count($removed), $this->renderHandleList($removed)); } else { return pht( '%s changed file(s) for %s, attached %d: %s; detached %d: %s', $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid), count($added), $this->renderHandleList($added), count($removed), $this->renderHandleList($removed)); } } return parent::getTitleForFeed($story); } public function hasChangeDetails() { switch ($this->getTransactionType()) { case self::TYPE_DESCRIPTION: return true; } return parent::hasChangeDetails(); } public function renderChangeDetails(PhabricatorUser $viewer) { return $this->renderTextCorpusChangeDetails( $viewer, $this->getOldValue(), $this->getNewValue()); } public function getMailTags() { $tags = array(); switch ($this->getTransactionType()) { case self::TYPE_STATUS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_STATUS; break; case self::TYPE_OWNER: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OWNER; break; case self::TYPE_CCS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_CC; break; case self::TYPE_PROJECTS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS; break; case self::TYPE_PRIORITY: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PRIORITY; break; case PhabricatorTransactions::TYPE_COMMENT: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_COMMENT; break; default: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OTHER; break; } return $tags; } } diff --git a/src/applications/project/controller/PhabricatorProjectBoardController.php b/src/applications/project/controller/PhabricatorProjectBoardController.php index f7ad05708b..f0e1591f43 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardController.php @@ -1,224 +1,226 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->needImages(true) ->withIDs(array($this->id)) ->executeOne(); if (!$project) { return new Aphront404Response(); } $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array($project->getPHID())) ->execute(); $columns = mpull($columns, null, 'getSequence'); // If there's no default column, create one now. if (empty($columns[0])) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $column = PhabricatorProjectColumn::initializeNewColumn($viewer) ->setSequence(0) ->setProjectPHID($project->getPHID()) ->save(); $column->attachProject($project); $columns[0] = $column; unset($unguarded); } ksort($columns); $tasks = id(new ManiphestTaskQuery()) ->setViewer($viewer) ->withAllProjects(array($project->getPHID())) ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) ->execute(); $tasks = mpull($tasks, null, 'getPHID'); if ($tasks) { $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN; $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($tasks, 'getPHID')) ->withEdgeTypes(array($edge_type)) ->withDestinationPHIDs(mpull($columns, 'getPHID')); $edge_query->execute(); } $task_map = array(); $default_phid = $columns[0]->getPHID(); foreach ($tasks as $task) { $task_phid = $task->getPHID(); $column_phids = $edge_query->getDestinationPHIDs(array($task_phid)); $column_phid = head($column_phids); $column_phid = nonempty($column_phid, $default_phid); $task_map[$column_phid][] = $task_phid; } $board_id = celerity_generate_unique_node_id(); $board = id(new PHUIWorkboardView()) ->setUser($viewer) ->setFluidishLayout(true) ->setID($board_id); $this->initBehavior( 'project-boards', array( 'boardID' => $board_id, 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), )); $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); foreach ($columns as $column) { $panel = id(new PHUIWorkpanelView()) ->setHeader($column->getDisplayName()) - ->setHeaderColor($column->getHeaderColor()) - ->setEditURI('edit/'.$column->getID().'/'); + ->setHeaderColor($column->getHeaderColor()); + if (!$column->isDefaultColumn()) { + $panel->setEditURI('edit/'.$column->getID().'/'); + } $cards = id(new PHUIObjectItemListView()) ->setUser($viewer) ->setCards(true) ->setFlush(true) ->setAllowEmptyList(true) ->addSigil('project-column') ->setMetadata( array( 'columnPHID' => $column->getPHID(), )); $task_phids = idx($task_map, $column->getPHID(), array()); foreach (array_select_keys($tasks, $task_phids) as $task) { $cards->addItem($this->renderTaskCard($task)); } $panel->setCards($cards); if (!$task_phids) { $cards->addClass('project-column-empty'); } $board->addPanel($panel); } $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb( $project->getName(), $this->getApplicationURI('view/'.$project->getID().'/')); $crumbs->addTextCrumb(pht('Board')); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $actions = id(new PhabricatorActionListView()) ->setUser($viewer) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Add Column')) ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')) ->setIcon('create') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $plist = id(new PHUIPropertyListView()); // TODO: Need this to get actions to render. $plist->addProperty( pht('Project Boards'), phutil_tag( 'em', array(), pht( 'This feature is beta, but should mostly work.'))); $plist->setActionList($actions); $header = id(new PHUIHeaderView()) ->setHeader($project->getName()) ->setUser($viewer) ->setImage($project->getProfileImageURI()) ->setPolicyObject($project); $box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($plist); $board_box = id(new PHUIBoxView()) ->appendChild($board) ->addMargin(PHUI::MARGIN_LARGE); return $this->buildApplicationPage( array( $crumbs, $box, $board_box, ), array( 'title' => pht('%s Board', $project->getName()), 'device' => true, )); } private function renderTaskCard(ManiphestTask $task) { $request = $this->getRequest(); $viewer = $request->getUser(); $handles = $this->handles; $color_map = ManiphestTaskPriority::getColorMap(); $bar_color = idx($color_map, $task->getPriority(), 'grey'); // TODO: Batch this earlier on. $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $task, PhabricatorPolicyCapability::CAN_EDIT); $card = id(new PHUIObjectItemView()) ->setObjectName('T'.$task->getID()) ->setHeader($task->getTitle()) ->setGrippable($can_edit) ->setHref('/T'.$task->getID()) ->addSigil('project-card') ->setMetadata( array( 'objectPHID' => $task->getPHID(), )) ->addAction( id(new PHUIListItemView()) ->setName(pht('Edit')) ->setIcon('edit') ->setHref('/maniphest/task/edit/'.$task->getID().'/') ->setWorkflow(true)) ->setBarColor($bar_color); if ($task->getOwnerPHID()) { $owner = $handles[$task->getOwnerPHID()]; $card->addAttribute($owner->renderLink()); } return $card; } } diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 7478a7dc79..be17d3362d 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -1,120 +1,143 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $column_phid = $request->getStr('columnPHID'); $object_phid = $request->getStr('objectPHID'); $after_phid = $request->getStr('afterPHID'); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->withIDs(array($this->id)) ->executeOne(); if (!$project) { return new Aphront404Response(); } // NOTE: I'm not requiring EDIT on the object for now, since we require // EDIT on the project anyway and this relationship is more owned by the // project than the object. Maybe this is worth revisiting eventually. $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($object_phid)) ->executeOne(); if (!$object) { return new Aphront404Response(); } $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array($project->getPHID())) ->execute(); $columns = mpull($columns, null, 'getPHID'); if (empty($columns[$column_phid])) { // User is trying to drop this object into a nonexistent column, just kick // them out. return new Aphront404Response(); } $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_COLUMN; $query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($object->getPHID())) ->withEdgeTypes(array($edge_type)) ->withDestinationPHIDs(array_keys($columns)); $query->execute(); $edge_phids = $query->getDestinationPHIDs(); $this->rewriteEdges( $object->getPHID(), $edge_type, $column_phid, $edge_phids); - // TODO: We also need to deal with priorities, so far this only gets stuff - // in the correct column. + if ($after_phid) { + $after_task = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs(array($after_phid)) + ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) + ->executeOne(); + if (!$after_task) { + return new Aphront404Response(); + } + $after_pri = $after_task->getPriority(); + $after_sub = $after_task->getSubpriority(); + + $xactions = array(id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY) + ->setNewValue(array( + 'newPriority' => $after_pri, + 'newSubpriorityBase' => $after_sub))); + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($object, $xactions); + } return id(new AphrontAjaxResponse())->setContent(array()); } private function rewriteEdges($src, $edge_type, $dst, array $edges) { $viewer = $this->getRequest()->getUser(); // NOTE: Normally, we expect only one edge to exist, but this works in a // general way so it will repair any stray edges. $remove = array(); $edge_missing = true; foreach ($edges as $phid) { if ($phid == $dst) { $edge_missing = false; } else { $remove[] = $phid; } } $add = array(); if ($edge_missing) { $add[] = $dst; } if (!$add && !$remove) { return; } $editor = id(new PhabricatorEdgeEditor()) ->setActor($viewer) ->setSuppressEvents(true); foreach ($add as $phid) { $editor->addEdge($src, $edge_type, $phid); } foreach ($remove as $phid) { $editor->removeEdge($src, $edge_type, $phid); } $editor->save(); } } diff --git a/src/view/phui/PHUIWorkpanelView.php b/src/view/phui/PHUIWorkpanelView.php index 2d4b946c1a..dd80edd587 100644 --- a/src/view/phui/PHUIWorkpanelView.php +++ b/src/view/phui/PHUIWorkpanelView.php @@ -1,86 +1,91 @@ cards[] = $cards; return $this; } public function setHeader($header) { $this->header = $header; return $this; } public function setEditURI($edit_uri) { $this->editURI = $edit_uri; return $this; } public function setFooterAction(PHUIListItemView $footer_action) { $this->footerAction = $footer_action; return $this; } public function setHeaderColor($header_color) { $this->headerColor = $header_color; return $this; } public function getTagAttributes() { return array( 'class' => 'phui-workpanel-view', ); } public function getTagContent() { require_celerity_resource('phui-workpanel-view-css'); $footer = ''; if ($this->footerAction) { $footer_tag = $this->footerAction; $footer = phutil_tag( 'ul', array( 'class' => 'phui-workpanel-footer-action mst ps' ), $footer_tag); } - $header_edit = id(new PHUIIconView()) - ->setSpriteSheet(PHUIIconView::SPRITE_ACTIONS) - ->setSpriteIcon('settings-grey') - ->setHref($this->editURI); + $header_edit = null; + if ($this->editURI) { + $header_edit = id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_ACTIONS) + ->setSpriteIcon('settings-grey') + ->setHref($this->editURI); + } $header = id(new PhabricatorActionHeaderView()) ->setHeaderTitle($this->header) - ->setHeaderColor($this->headerColor) - ->addAction($header_edit); + ->setHeaderColor($this->headerColor); + if ($header_edit) { + $header->addAction($header_edit); + } $body = phutil_tag( 'div', array( 'class' => 'phui-workpanel-body' ), $this->cards); $view = phutil_tag( 'div', array( 'class' => 'phui-workpanel-view-inner', ), array( $header, $body, $footer, )); return $view; } }