diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index 641ca8191c..eb11aedd02 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -1,772 +1,785 @@ contentSource = $content_source; return $this; } public function getContentSource() { return $this->contentSource; } abstract public function getPHID(); abstract public function getHeraldName(); public function getHeraldField($field_name) { switch ($field_name) { case self::FIELD_RULE: return null; case self::FIELD_CONTENT_SOURCE: return $this->getContentSource()->getSource(); default: throw new Exception( "Unknown field '{$field_name}'!"); } } abstract public function applyHeraldEffects(array $effects); public function isEnabled() { return true; } /** * NOTE: You generally should not override this; it exists to support legacy * adapters which had hard-coded content types. */ public function getAdapterContentType() { return get_class($this); } abstract public function getAdapterContentName(); /* -( Fields )------------------------------------------------------------- */ abstract public function getFields(); public function getFieldNameMap() { return array( self::FIELD_TITLE => pht('Title'), self::FIELD_BODY => pht('Body'), self::FIELD_AUTHOR => pht('Author'), self::FIELD_COMMITTER => pht('Committer'), self::FIELD_REVIEWER => pht('Reviewer'), self::FIELD_REVIEWERS => pht('Reviewers'), self::FIELD_CC => pht('CCs'), self::FIELD_TAGS => pht('Tags'), self::FIELD_DIFF_FILE => pht('Any changed filename'), self::FIELD_DIFF_CONTENT => pht('Any changed file content'), self::FIELD_REPOSITORY => pht('Repository'), self::FIELD_RULE => pht('Another Herald rule'), self::FIELD_AFFECTED_PACKAGE => pht('Any affected package'), self::FIELD_AFFECTED_PACKAGE_OWNER => pht("Any affected package's owner"), self:: FIELD_CONTENT_SOURCE => pht('Content Source') ); } /* -( Conditions )--------------------------------------------------------- */ public function getConditionNameMap() { return array( self::CONDITION_CONTAINS => pht('contains'), self::CONDITION_NOT_CONTAINS => pht('does not contain'), self::CONDITION_IS => pht('is'), self::CONDITION_IS_NOT => pht('is not'), self::CONDITION_IS_ANY => pht('is any of'), self::CONDITION_IS_NOT_ANY => pht('is not any of'), self::CONDITION_INCLUDE_ALL => pht('include all of'), self::CONDITION_INCLUDE_ANY => pht('include any of'), self::CONDITION_INCLUDE_NONE => pht('include none of'), self::CONDITION_IS_ME => pht('is myself'), self::CONDITION_IS_NOT_ME => pht('is not myself'), self::CONDITION_REGEXP => pht('matches regexp'), self::CONDITION_RULE => pht('matches:'), self::CONDITION_NOT_RULE => pht('does not match:'), self::CONDITION_EXISTS => pht('exists'), self::CONDITION_NOT_EXISTS => pht('does not exist'), self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'), ); } public function getConditionsForField($field) { switch ($field) { case self::FIELD_TITLE: case self::FIELD_BODY: return array( self::CONDITION_CONTAINS, self::CONDITION_NOT_CONTAINS, self::CONDITION_IS, self::CONDITION_IS_NOT, self::CONDITION_REGEXP, ); case self::FIELD_AUTHOR: case self::FIELD_COMMITTER: case self::FIELD_REPOSITORY: case self::FIELD_REVIEWER: return array( self::CONDITION_IS_ANY, self::CONDITION_IS_NOT_ANY, ); case self::FIELD_TAGS: case self::FIELD_REVIEWERS: case self::FIELD_CC: return array( self::CONDITION_INCLUDE_ALL, self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_DIFF_FILE: return array( self::CONDITION_CONTAINS, self::CONDITION_REGEXP, ); case self::FIELD_DIFF_CONTENT: return array( self::CONDITION_CONTAINS, self::CONDITION_REGEXP, self::CONDITION_REGEXP_PAIR, ); case self::FIELD_RULE: return array( self::CONDITION_RULE, self::CONDITION_NOT_RULE, ); case self::FIELD_AFFECTED_PACKAGE: case self::FIELD_AFFECTED_PACKAGE_OWNER: return array( self::CONDITION_INCLUDE_ANY, self::CONDITION_INCLUDE_NONE, ); case self::FIELD_CONTENT_SOURCE: return array( self::CONDITION_IS, self::CONDITION_IS_NOT, ); default: throw new Exception( "This adapter does not define conditions for field '{$field}'!"); } } public function doesConditionMatch( HeraldEngine $engine, HeraldRule $rule, HeraldCondition $condition, $field_value) { $condition_type = $condition->getFieldCondition(); $condition_value = $condition->getValue(); switch ($condition_type) { case self::CONDITION_CONTAINS: // "Contains" can take an array of strings, as in "Any changed // filename" for diffs. foreach ((array)$field_value as $value) { if (stripos($value, $condition_value) !== false) { return true; } } return false; case self::CONDITION_NOT_CONTAINS: return (stripos($field_value, $condition_value) === false); case self::CONDITION_IS: return ($field_value == $condition_value); case self::CONDITION_IS_NOT: return ($field_value != $condition_value); case self::CONDITION_IS_ME: return ($field_value == $rule->getAuthorPHID()); case self::CONDITION_IS_NOT_ME: return ($field_value != $rule->getAuthorPHID()); case self::CONDITION_IS_ANY: if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( "Expected condition value to be an array."); } $condition_value = array_fuse($condition_value); return isset($condition_value[$field_value]); case self::CONDITION_IS_NOT_ANY: if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( "Expected condition value to be an array."); } $condition_value = array_fuse($condition_value); return !isset($condition_value[$field_value]); case self::CONDITION_INCLUDE_ALL: if (!is_array($field_value)) { throw new HeraldInvalidConditionException( "Object produced non-array value!"); } if (!is_array($condition_value)) { throw new HeraldInvalidConditionException( "Expected conditionv value to be an array."); } $have = array_select_keys(array_fuse($field_value), $condition_value); return (count($have) == count($condition_value)); case self::CONDITION_INCLUDE_ANY: return (bool)array_select_keys( array_fuse($field_value), $condition_value); case self::CONDITION_INCLUDE_NONE: return !array_select_keys( array_fuse($field_value), $condition_value); case self::CONDITION_EXISTS: return (bool)$field_value; case self::CONDITION_NOT_EXISTS: return !$field_value; case self::CONDITION_REGEXP: foreach ((array)$field_value as $value) { // We add the 'S' flag because we use the regexp multiple times. // It shouldn't cause any troubles if the flag is already there // - /.*/S is evaluated same as /.*/SS. $result = @preg_match($condition_value . 'S', $value); if ($result === false) { throw new HeraldInvalidConditionException( "Regular expression is not valid!"); } if ($result) { return true; } } return false; case self::CONDITION_REGEXP_PAIR: // Match a JSON-encoded pair of regular expressions against a // dictionary. The first regexp must match the dictionary key, and the // second regexp must match the dictionary value. If any key/value pair // in the dictionary matches both regexps, the condition is satisfied. $regexp_pair = json_decode($condition_value, true); if (!is_array($regexp_pair)) { throw new HeraldInvalidConditionException( "Regular expression pair is not valid JSON!"); } if (count($regexp_pair) != 2) { throw new HeraldInvalidConditionException( "Regular expression pair is not a pair!"); } $key_regexp = array_shift($regexp_pair); $value_regexp = array_shift($regexp_pair); foreach ((array)$field_value as $key => $value) { $key_matches = @preg_match($key_regexp, $key); if ($key_matches === false) { throw new HeraldInvalidConditionException( "First regular expression is invalid!"); } if ($key_matches) { $value_matches = @preg_match($value_regexp, $value); if ($value_matches === false) { throw new HeraldInvalidConditionException( "Second regular expression is invalid!"); } if ($value_matches) { return true; } } } return false; case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: $rule = $engine->getRule($condition_value); if (!$rule) { throw new HeraldInvalidConditionException( "Condition references a rule which does not exist!"); } $is_not = ($condition_type == self::CONDITION_NOT_RULE); $result = $engine->doesRuleMatch($rule, $this); if ($is_not) { $result = !$result; } return $result; default: throw new HeraldInvalidConditionException( "Unknown condition '{$condition_type}'."); } } public function willSaveCondition(HeraldCondition $condition) { $condition_type = $condition->getFieldCondition(); $condition_value = $condition->getValue(); switch ($condition_type) { case self::CONDITION_REGEXP: $ok = @preg_match($condition_value, ''); if ($ok === false) { throw new HeraldInvalidConditionException( pht( 'The regular expression "%s" is not valid. Regular expressions '. 'must have enclosing characters (e.g. "@/path/to/file@", not '. '"/path/to/file") and be syntactically correct.', $condition_value)); } break; case self::CONDITION_REGEXP_PAIR: $json = json_decode($condition_value, true); if (!is_array($json)) { throw new HeraldInvalidConditionException( pht( 'The regular expression pair "%s" is not valid JSON. Enter a '. 'valid JSON array with two elements.', $condition_value)); } if (count($json) != 2) { throw new HeraldInvalidConditionException( pht( 'The regular expression pair "%s" must have exactly two '. 'elements.', $condition_value)); } $key_regexp = array_shift($json); $val_regexp = array_shift($json); $key_ok = @preg_match($key_regexp, ''); if ($key_ok === false) { throw new HeraldInvalidConditionException( pht( 'The first regexp in the regexp pair, "%s", is not a valid '. 'regexp.', $key_regexp)); } $val_ok = @preg_match($val_regexp, ''); if ($val_ok === false) { throw new HeraldInvalidConditionException( pht( 'The second regexp in the regexp pair, "%s", is not a valid '. 'regexp.', $val_regexp)); } break; case self::CONDITION_CONTAINS: case self::CONDITION_NOT_CONTAINS: case self::CONDITION_IS: case self::CONDITION_IS_NOT: case self::CONDITION_IS_ANY: case self::CONDITION_IS_NOT_ANY: case self::CONDITION_INCLUDE_ALL: case self::CONDITION_INCLUDE_ANY: case self::CONDITION_INCLUDE_NONE: case self::CONDITION_IS_ME: case self::CONDITION_IS_NOT_ME: case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: // No explicit validation for these types, although there probably // should be in some cases. break; default: throw new HeraldInvalidConditionException( pht( 'Unknown condition "%s"!', $condition_type)); } } /* -( Actions )------------------------------------------------------------ */ abstract public function getActions($rule_type); public function getActionNameMap($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: return array( - self::ACTION_NOTHING => pht('Do nothing'), - self::ACTION_ADD_CC => pht('Add emails to CC'), - self::ACTION_REMOVE_CC => pht('Remove emails from CC'), - self::ACTION_EMAIL => pht('Send an email to'), - self::ACTION_AUDIT => pht('Trigger an Audit by'), - self::ACTION_FLAG => pht('Mark with flag'), + self::ACTION_NOTHING => pht('Do nothing'), + self::ACTION_ADD_CC => pht('Add emails to CC'), + self::ACTION_REMOVE_CC => pht('Remove emails from CC'), + self::ACTION_EMAIL => pht('Send an email to'), + self::ACTION_AUDIT => pht('Trigger an Audit by'), + self::ACTION_FLAG => pht('Mark with flag'), + self::ACTION_ASSIGN_TASK => pht('Assign task to'), + self::ACTION_ADD_PROJECTS => pht('Add projects'), ); case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return array( - self::ACTION_NOTHING => pht('Do nothing'), - self::ACTION_ADD_CC => pht('Add me to CC'), - self::ACTION_REMOVE_CC => pht('Remove me from CC'), - self::ACTION_EMAIL => pht('Send me an email'), - self::ACTION_AUDIT => pht('Trigger an Audit by me'), - self::ACTION_FLAG => pht('Mark with flag'), + self::ACTION_NOTHING => pht('Do nothing'), + self::ACTION_ADD_CC => pht('Add me to CC'), + self::ACTION_REMOVE_CC => pht('Remove me from CC'), + self::ACTION_EMAIL => pht('Send me an email'), + self::ACTION_AUDIT => pht('Trigger an Audit by me'), + self::ACTION_FLAG => pht('Mark with flag'), + self::ACTION_ASSIGN_TASK => pht('Assign task to me.'), + self::ACTION_ADD_PROJECTS => pht('Add projects'), ); default: throw new Exception("Unknown rule type '{$rule_type}'!"); } } public function willSaveAction( HeraldRule $rule, HeraldAction $action) { $target = $action->getTarget(); if (is_array($target)) { $target = array_keys($target); } $author_phid = $rule->getAuthorPHID(); $rule_type = $rule->getRuleType(); if ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL) { switch ($action->getAction()) { case self::ACTION_EMAIL: case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_AUDIT: + case self::ACTION_ASSIGN_TASK: // For personal rules, force these actions to target the rule owner. $target = array($author_phid); break; case self::ACTION_FLAG: // Make sure flag color is valid; set to blue if not. $color_map = PhabricatorFlagColor::getColorNameMap(); if (empty($color_map[$target])) { $target = PhabricatorFlagColor::COLOR_BLUE; } break; case self::ACTION_NOTHING: break; default: throw new HeraldInvalidActionException( pht( 'Unrecognized action type "%s"!', $action->getAction())); } } $action->setTarget($target); } /* -( Values )------------------------------------------------------------- */ public function getValueTypeForFieldAndCondition($field, $condition) { switch ($condition) { case self::CONDITION_CONTAINS: case self::CONDITION_NOT_CONTAINS: case self::CONDITION_REGEXP: case self::CONDITION_REGEXP_PAIR: return self::VALUE_TEXT; case self::CONDITION_IS: case self::CONDITION_IS_NOT: switch ($field) { case self::FIELD_CONTENT_SOURCE: return self::VALUE_CONTENT_SOURCE; default: return self::VALUE_TEXT; } break; case self::CONDITION_IS_ANY: case self::CONDITION_IS_NOT_ANY: switch ($field) { case self::FIELD_REPOSITORY: return self::VALUE_REPOSITORY; default: return self::VALUE_USER; } break; case self::CONDITION_INCLUDE_ALL: case self::CONDITION_INCLUDE_ANY: case self::CONDITION_INCLUDE_NONE: switch ($field) { case self::FIELD_REPOSITORY: return self::VALUE_REPOSITORY; case self::FIELD_CC: return self::VALUE_EMAIL; case self::FIELD_TAGS: return self::VALUE_TAG; case self::FIELD_AFFECTED_PACKAGE: return self::VALUE_OWNERS_PACKAGE; default: return self::VALUE_USER; } break; case self::CONDITION_IS_ME: case self::CONDITION_IS_NOT_ME: case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: return self::VALUE_NONE; case self::CONDITION_RULE: case self::CONDITION_NOT_RULE: return self::VALUE_RULE; default: throw new Exception("Unknown condition '{$condition}'."); } } public static function getValueTypeForAction($action, $rule_type) { $is_personal = ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); if ($is_personal) { switch ($action) { case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_EMAIL: case self::ACTION_NOTHING: case self::ACTION_AUDIT: + case self::ACTION_ASSIGN_TASK: return self::VALUE_NONE; case self::ACTION_FLAG: return self::VALUE_FLAG_COLOR; + case self::ACTION_ADD_PROJECTS: + return self::VALUE_PROJECT; default: throw new Exception("Unknown or invalid action '{$action}'."); } } else { switch ($action) { case self::ACTION_ADD_CC: case self::ACTION_REMOVE_CC: case self::ACTION_EMAIL: return self::VALUE_EMAIL; case self::ACTION_NOTHING: return self::VALUE_NONE; case self::ACTION_AUDIT: + case self::ACTION_ADD_PROJECTS: return self::VALUE_PROJECT; case self::ACTION_FLAG: return self::VALUE_FLAG_COLOR; + case self::ACTION_ASSIGN_TASK: + return self::VALUE_USER; default: throw new Exception("Unknown or invalid action '{$action}'."); } } } /* -( Repetition )--------------------------------------------------------- */ public function getRepetitionOptions() { return array( HeraldRepetitionPolicyConfig::EVERY, ); } public static function applyFlagEffect(HeraldEffect $effect, $phid) { $color = $effect->getTarget(); // TODO: Silly that we need to load this again here. $rule = id(new HeraldRule())->load($effect->getRuleID()); $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $rule->getAuthorPHID()); $flag = PhabricatorFlagQuery::loadUserFlag($user, $phid); if ($flag) { return new HeraldApplyTranscript( $effect, false, pht('Object already flagged.')); } $handle = id(new PhabricatorHandleQuery()) ->setViewer($user) ->withPHIDs(array($phid)) ->executeOne(); $flag = new PhabricatorFlag(); $flag->setOwnerPHID($user->getPHID()); $flag->setType($handle->getType()); $flag->setObjectPHID($handle->getPHID()); // TOOD: Should really be transcript PHID, but it doesn't exist yet. $flag->setReasonPHID($user->getPHID()); $flag->setColor($color); $flag->setNote( pht('Flagged by Herald Rule "%s".', $rule->getName())); $flag->save(); return new HeraldApplyTranscript( $effect, true, pht('Added flag.')); } public static function getAllAdapters() { static $adapters; if (!$adapters) { $adapters = id(new PhutilSymbolLoader()) ->setAncestorClass(__CLASS__) ->loadObjects(); } return $adapters; } public static function getAllEnabledAdapters() { $adapters = self::getAllAdapters(); foreach ($adapters as $key => $adapter) { if (!$adapter->isEnabled()) { unset($adapters[$key]); } } return $adapters; } public static function getAdapterForContentType($content_type) { $adapters = self::getAllAdapters(); foreach ($adapters as $adapter) { if ($adapter->getAdapterContentType() == $content_type) { return $adapter; } } throw new Exception( pht( 'No adapter exists for Herald content type "%s".', $content_type)); } public static function getEnabledAdapterMap() { $map = array(); $adapters = HeraldAdapter::getAllEnabledAdapters(); foreach ($adapters as $adapter) { $type = $adapter->getAdapterContentType(); $name = $adapter->getAdapterContentName(); $map[$type] = $name; } asort($map); return $map; } public function renderRuleAsText(HeraldRule $rule) { $out = array(); if ($rule->getMustMatchAll()) { $out[] = pht('When all of these conditions are met:'); } else { $out[] = pht('When any of these conditions are met:'); } $out[] = null; foreach ($rule->getConditions() as $condition) { $out[] = " ".$this->renderConditionAsText($condition); } $out[] = null; if ($rule->getRepetitionPolicy() == HeraldRepetitionPolicyConfig::EVERY) { $out[] = pht('Take these actions every time this rule matches:'); } else { $out[] = pht('Take these actions the first time this rule matches:'); } $out[] = null; foreach ($rule->getActions() as $action) { $out[] = " ".$this->renderActionAsText($action); } return implode("\n", $out); } private function renderConditionAsText(HeraldCondition $condition) { $field_type = $condition->getFieldName(); $field_name = idx($this->getFieldNameMap(), $field_type); $condition_type = $condition->getFieldCondition(); $condition_name = idx($this->getConditionNameMap(), $condition_type); $value = $this->renderConditionValueAsText($condition); return "{$field_name} {$condition_name} {$value}"; } private function renderActionAsText(HeraldAction $action) { $rule_global = HeraldRuleTypeConfig::RULE_TYPE_GLOBAL; $action_type = $action->getAction(); $action_name = idx($this->getActionNameMap($rule_global), $action_type); $target = $this->renderActionTargetAsText($action); return "{$action_name} {$target}"; } private function renderConditionValueAsText(HeraldCondition $condition) { // TODO: This produces sketchy results for many conditions. $value = $condition->getValue(); if (is_array($value)) { $value = implode(', ', $value); } return $value; } private function renderActionTargetAsText(HeraldAction $action) { // TODO: This produces sketchy results for Flags and PHIDs. $target = $action->getTarget(); if (is_array($target)) { $target = implode(', ', $target); } return $target; } } diff --git a/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php b/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php index cd275afd00..b939a9ad72 100644 --- a/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php +++ b/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php @@ -1,115 +1,156 @@ task = $task; return $this; } public function getTask() { return $this->task; } private function setCcPHIDs(array $cc_phids) { $this->ccPHIDs = $cc_phids; return $this; } public function getCcPHIDs() { return $this->ccPHIDs; } + public function setAssignPHID($assign_phid) { + $this->assignPHID = $assign_phid; + return $this; + } + public function getAssignPHID() { + return $this->assignPHID; + } + + public function setProjectPHIDs(array $project_phids) { + $this->projectPHIDs = $project_phids; + return $this; + } + public function getProjectPHIDs() { + return $this->projectPHIDs; + } + public function getAdapterContentName() { return pht('Maniphest Tasks'); } public function getFields() { return array( self::FIELD_TITLE, self::FIELD_BODY, self::FIELD_AUTHOR, self::FIELD_CC, self::FIELD_CONTENT_SOURCE, ); } public function getActions($rule_type) { switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: return array( self::ACTION_ADD_CC, + self::ACTION_ASSIGN_TASK, + self::ACTION_ADD_PROJECTS, self::ACTION_NOTHING, ); case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: return array( self::ACTION_ADD_CC, self::ACTION_FLAG, + self::ACTION_ASSIGN_TASK, self::ACTION_NOTHING, ); } } public function getPHID() { return $this->getTask()->getPHID(); } public function getHeraldName() { return 'T'.$this->getTask()->getID(); } public function getHeraldField($field) { switch ($field) { case self::FIELD_TITLE: return $this->getTask()->getTitle(); case self::FIELD_BODY: return $this->getTask()->getDescription(); case self::FIELD_AUTHOR: return $this->getTask()->getAuthorPHID(); case self::FIELD_CC: return $this->getTask()->getCCPHIDs(); } return parent::getHeraldField($field); } public function applyHeraldEffects(array $effects) { assert_instances_of($effects, 'HeraldEffect'); $result = array(); foreach ($effects as $effect) { $action = $effect->getAction(); switch ($action) { case self::ACTION_NOTHING: $result[] = new HeraldApplyTranscript( $effect, true, pht('Great success at doing nothing.')); break; case self::ACTION_ADD_CC: $add_cc = array(); foreach ($effect->getTarget() as $phid) { $add_cc[$phid] = true; } $this->setCcPHIDs(array_keys($add_cc)); $result[] = new HeraldApplyTranscript( $effect, true, pht('Added address to cc list.')); break; case self::ACTION_FLAG: $result[] = parent::applyFlagEffect( $effect, $this->getTask()->getPHID()); break; + case self::ACTION_ASSIGN_TASK: + $target_array = $effect->getTarget(); + $assign_phid = reset($target_array); + $this->setAssignPHID($assign_phid); + $result[] = new HeraldApplyTranscript( + $effect, + true, + pht('Assigned task.')); + break; + case self::ACTION_ADD_PROJECTS: + $add_projects = array(); + foreach ($effect->getTarget() as $phid) { + $add_projects[$phid] = true; + } + $this->setProjectPHIDs(array_keys($add_projects)); + $result[] = new HeraldApplyTranscript( + $effect, + true, + pht('Added projects.')); + break; default: throw new Exception("No rules to handle action '{$action}'."); } } return $result; } } diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditorPro.php b/src/applications/maniphest/editor/ManiphestTransactionEditorPro.php index 8e912dde6a..ed20c3bf7d 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditorPro.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditorPro.php @@ -1,229 +1,249 @@ getTransactionType()) { case ManiphestTransaction::TYPE_PRIORITY: 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(); } } 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: 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: return $object->setOwnerPHID($xaction->getNewValue()); 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; } } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { } protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { return true; } 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(); } } }