diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index d05ed2d525..73c1e8c39c 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -1,743 +1,734 @@ getViewer(); $id = $request->getURIData('id'); $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); $rule_type_map = HeraldRuleTypeConfig::getRuleTypeMap(); if ($id) { $rule = id(new HeraldRuleQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$rule) { return new Aphront404Response(); } $cancel_uri = '/'.$rule->getMonogram(); } else { $new_uri = $this->getApplicationURI('new/'); $rule = new HeraldRule(); $rule->setAuthorPHID($viewer->getPHID()); $rule->setMustMatchAll(1); $content_type = $request->getStr('content_type'); $rule->setContentType($content_type); $rule_type = $request->getStr('rule_type'); if (!isset($rule_type_map[$rule_type])) { return $this->newDialog() ->setTitle(pht('Invalid Rule Type')) ->appendParagraph( pht( 'The selected rule type ("%s") is not recognized by Herald.', $rule_type)) ->addCancelButton($new_uri); } $rule->setRuleType($rule_type); try { $adapter = HeraldAdapter::getAdapterForContentType( $rule->getContentType()); } catch (Exception $ex) { return $this->newDialog() ->setTitle(pht('Invalid Content Type')) ->appendParagraph( pht( 'The selected content type ("%s") is not recognized by '. 'Herald.', $rule->getContentType())) ->addCancelButton($new_uri); } if (!$adapter->supportsRuleType($rule->getRuleType())) { return $this->newDialog() ->setTitle(pht('Rule/Content Mismatch')) ->appendParagraph( pht( 'The selected rule type ("%s") is not supported by the selected '. 'content type ("%s").', $rule->getRuleType(), $rule->getContentType())) ->addCancelButton($new_uri); } if ($rule->isObjectRule()) { $rule->setTriggerObjectPHID($request->getStr('targetPHID')); $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($rule->getTriggerObjectPHID())) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$object) { throw new Exception( pht('No valid object provided for object rule!')); } if (!$adapter->canTriggerOnObject($object)) { throw new Exception( pht('Object is of wrong type for adapter!')); } } $cancel_uri = $this->getApplicationURI(); } if ($rule->isGlobalRule()) { $this->requireApplicationCapability( HeraldManageGlobalRulesCapability::CAPABILITY); } $adapter = HeraldAdapter::getAdapterForContentType($rule->getContentType()); $local_version = id(new HeraldRule())->getConfigVersion(); if ($rule->getConfigVersion() > $local_version) { throw new Exception( pht( 'This rule was created with a newer version of Herald. You can not '. 'view or edit it in this older version. Upgrade your Phabricator '. 'deployment.')); } // Upgrade rule version to our version, since we might add newly-defined // conditions, etc. $rule->setConfigVersion($local_version); $rule_conditions = $rule->loadConditions(); $rule_actions = $rule->loadActions(); $rule->attachConditions($rule_conditions); $rule->attachActions($rule_actions); $e_name = true; $errors = array(); if ($request->isFormPost() && $request->getStr('save')) { list($e_name, $errors) = $this->saveRule($adapter, $rule, $request); if (!$errors) { $id = $rule->getID(); $uri = '/'.$rule->getMonogram(); return id(new AphrontRedirectResponse())->setURI($uri); } } $must_match_selector = $this->renderMustMatchSelector($rule); $repetition_selector = $this->renderRepetitionSelector($rule, $adapter); $handles = $this->loadHandlesForRule($rule); require_celerity_resource('herald-css'); $content_type_name = $content_type_map[$rule->getContentType()]; $rule_type_name = $rule_type_map[$rule->getRuleType()]; $form = id(new AphrontFormView()) ->setUser($viewer) ->setID('herald-rule-edit-form') ->addHiddenInput('content_type', $rule->getContentType()) ->addHiddenInput('rule_type', $rule->getRuleType()) ->addHiddenInput('save', 1) ->appendChild( // Build this explicitly (instead of using addHiddenInput()) // so we can add a sigil to it. javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'rule', 'sigil' => 'rule', ))) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Rule Name')) ->setName('name') ->setError($e_name) ->setValue($rule->getName())); $trigger_object_control = false; if ($rule->isObjectRule()) { $trigger_object_control = id(new AphrontFormStaticControl()) ->setValue( pht( 'This rule triggers for %s.', $handles[$rule->getTriggerObjectPHID()]->renderLink())); } $form ->appendChild( id(new AphrontFormMarkupControl()) ->setValue(pht( 'This %s rule triggers for %s.', phutil_tag('strong', array(), $rule_type_name), phutil_tag('strong', array(), $content_type_name)))) ->appendChild($trigger_object_control) ->appendChild( id(new PHUIFormInsetView()) ->setTitle(pht('Conditions')) ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button button-green', 'sigil' => 'create-condition', 'mustcapture' => true, ), pht('New Condition'))) ->setDescription( pht('When %s these conditions are met:', $must_match_selector)) ->setContent(javelin_tag( 'table', array( 'sigil' => 'rule-conditions', 'class' => 'herald-condition-table', ), ''))) ->appendChild( id(new PHUIFormInsetView()) ->setTitle(pht('Action')) ->setRightButton(javelin_tag( 'a', array( 'href' => '#', 'class' => 'button button-green', 'sigil' => 'create-action', 'mustcapture' => true, ), pht('New Action'))) ->setDescription(pht( 'Take these actions %s', $repetition_selector)) ->setContent(javelin_tag( 'table', array( 'sigil' => 'rule-actions', 'class' => 'herald-action-table', ), ''))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Save Rule')) ->addCancelButton($cancel_uri)); $this->setupEditorBehavior($rule, $handles, $adapter); $title = $rule->getID() ? pht('Edit Herald Rule: %s', $rule->getName()) : pht('Create Herald Rule: %s', idx($content_type_map, $content_type)); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) ->setFormErrors($errors) ->setForm($form); $crumbs = $this ->buildApplicationCrumbs() ->addTextCrumb($title) ->setBorder(true); $view = id(new PHUITwoColumnView()) ->setFooter($form_box); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild( array( $view, )); } private function saveRule(HeraldAdapter $adapter, $rule, $request) { $new_name = $request->getStr('name'); $match_all = ($request->getStr('must_match') == 'all'); $repetition_policy = $request->getStr('repetition_policy'); // If the user selected an invalid policy, or there's only one possible // value so we didn't render a control, adjust the value to the first // valid policy value. $repetition_options = $this->getRepetitionOptionMap($adapter); if (!isset($repetition_options[$repetition_policy])) { $repetition_policy = head_key($repetition_options); } $e_name = true; $errors = array(); if (!strlen($new_name)) { $e_name = pht('Required'); $errors[] = pht('Rule must have a name.'); } $data = null; try { $data = phutil_json_decode($request->getStr('rule')); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht('Failed to decode rule data.'), $ex); } if (!is_array($data) || !$data['conditions'] || !$data['actions']) { throw new Exception(pht('Failed to decode rule data.')); } $conditions = array(); foreach ($data['conditions'] as $condition) { if ($condition === null) { // We manage this as a sparse array on the client, so may receive // NULL if conditions have been removed. continue; } $obj = new HeraldCondition(); $obj->setFieldName($condition[0]); $obj->setFieldCondition($condition[1]); if (is_array($condition[2])) { $obj->setValue(array_keys($condition[2])); } else { $obj->setValue($condition[2]); } try { $adapter->willSaveCondition($obj); } catch (HeraldInvalidConditionException $ex) { $errors[] = $ex->getMessage(); } $conditions[] = $obj; } $actions = array(); foreach ($data['actions'] as $action) { if ($action === null) { // Sparse on the client; removals can give us NULLs. continue; } if (!isset($action[1])) { // Legitimate for any action which doesn't need a target, like // "Do nothing". $action[1] = null; } $obj = new HeraldActionRecord(); $obj->setAction($action[0]); $obj->setTarget($action[1]); try { $adapter->willSaveAction($rule, $obj); } catch (HeraldInvalidActionException $ex) { $errors[] = $ex->getMessage(); } $actions[] = $obj; } if (!$errors) { $new_state = id(new HeraldRuleSerializer())->serializeRuleComponents( $match_all, $conditions, $actions, $repetition_policy); $xactions = array(); // Until this moves to EditEngine, manually add a "CREATE" transaction // if we're creating a new rule. This improves rendering of the initial // group of transactions. $is_new = (bool)(!$rule->getID()); if ($is_new) { $xactions[] = id(new HeraldRuleTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_CREATE); } $xactions[] = id(new HeraldRuleTransaction()) ->setTransactionType(HeraldRuleEditTransaction::TRANSACTIONTYPE) ->setNewValue($new_state); $xactions[] = id(new HeraldRuleTransaction()) ->setTransactionType(HeraldRuleNameTransaction::TRANSACTIONTYPE) ->setNewValue($new_name); try { id(new HeraldRuleEditor()) ->setActor($this->getViewer()) ->setContinueOnNoEffect(true) ->setContentSourceFromRequest($request) ->applyTransactions($rule, $xactions); return array(null, null); } catch (Exception $ex) { $errors[] = $ex->getMessage(); } } // mutate current rule, so it would be sent to the client in the right state $rule->setMustMatchAll((int)$match_all); $rule->setName($new_name); $rule->setRepetitionPolicyStringConstant($repetition_policy); $rule->attachConditions($conditions); $rule->attachActions($actions); return array($e_name, $errors); } private function setupEditorBehavior( HeraldRule $rule, array $handles, HeraldAdapter $adapter) { $all_rules = $this->loadRulesThisRuleMayDependUpon($rule); - $all_rules = mpull($all_rules, 'getName', 'getPHID'); - asort($all_rules); + $all_rules = msortv($all_rules, 'getEditorSortVector'); + $all_rules = mpull($all_rules, 'getEditorDisplayName', 'getPHID'); $all_fields = $adapter->getFieldNameMap(); $all_conditions = $adapter->getConditionNameMap(); $all_actions = $adapter->getActionNameMap($rule->getRuleType()); $fields = $adapter->getFields(); $field_map = array_select_keys($all_fields, $fields); // Populate any fields which exist in the rule but which we don't know the // names of, so that saving a rule without touching anything doesn't change // it. foreach ($rule->getConditions() as $condition) { $field_name = $condition->getFieldName(); if (empty($field_map[$field_name])) { $field_map[$field_name] = pht('', $field_name); } } $actions = $adapter->getActions($rule->getRuleType()); $action_map = array_select_keys($all_actions, $actions); // Populate any actions which exist in the rule but which we don't know the // names of, so that saving a rule without touching anything doesn't change // it. foreach ($rule->getActions() as $action) { $action_name = $action->getAction(); if (empty($action_map[$action_name])) { $action_map[$action_name] = pht('', $action_name); } } $config_info = array(); $config_info['fields'] = $this->getFieldGroups($adapter, $field_map); $config_info['conditions'] = $all_conditions; $config_info['actions'] = $this->getActionGroups($adapter, $action_map); $config_info['valueMap'] = array(); foreach ($field_map as $field => $name) { try { $field_conditions = $adapter->getConditionsForField($field); } catch (Exception $ex) { $field_conditions = array(HeraldAdapter::CONDITION_UNCONDITIONALLY); } $config_info['conditionMap'][$field] = $field_conditions; } foreach ($field_map as $field => $fname) { foreach ($config_info['conditionMap'][$field] as $condition) { $value_key = $adapter->getValueTypeForFieldAndCondition( $field, $condition); if ($value_key instanceof HeraldFieldValue) { $value_key->setViewer($this->getViewer()); $spec = $value_key->getControlSpecificationDictionary(); $value_key = $value_key->getFieldValueKey(); $config_info['valueMap'][$value_key] = $spec; } $config_info['values'][$field][$condition] = $value_key; } } $config_info['rule_type'] = $rule->getRuleType(); foreach ($action_map as $action => $name) { try { $value_key = $adapter->getValueTypeForAction( $action, $rule->getRuleType()); } catch (Exception $ex) { $value_key = new HeraldEmptyFieldValue(); } if ($value_key instanceof HeraldFieldValue) { $value_key->setViewer($this->getViewer()); $spec = $value_key->getControlSpecificationDictionary(); $value_key = $value_key->getFieldValueKey(); $config_info['valueMap'][$value_key] = $spec; } $config_info['targets'][$action] = $value_key; } $default_group = head($config_info['fields']); $default_field = head_key($default_group['options']); $default_condition = head($config_info['conditionMap'][$default_field]); $default_actions = head($config_info['actions']); $default_action = head_key($default_actions['options']); if ($rule->getConditions()) { $serial_conditions = array(); foreach ($rule->getConditions() as $condition) { $value = $adapter->getEditorValueForCondition( $this->getViewer(), $condition); $serial_conditions[] = array( $condition->getFieldName(), $condition->getFieldCondition(), $value, ); } } else { $serial_conditions = array( array($default_field, $default_condition, null), ); } if ($rule->getActions()) { $serial_actions = array(); foreach ($rule->getActions() as $action) { $value = $adapter->getEditorValueForAction( $this->getViewer(), $action); $serial_actions[] = array( $action->getAction(), $value, ); } } else { $serial_actions = array( array($default_action, null), ); } Javelin::initBehavior( 'herald-rule-editor', array( 'root' => 'herald-rule-edit-form', 'default' => array( 'field' => $default_field, 'condition' => $default_condition, 'action' => $default_action, ), 'conditions' => (object)$serial_conditions, 'actions' => (object)$serial_actions, 'template' => $this->buildTokenizerTemplates() + array( 'rules' => $all_rules, ), 'info' => $config_info, )); } private function loadHandlesForRule($rule) { $phids = array(); foreach ($rule->getActions() as $action) { if (!is_array($action->getTarget())) { continue; } foreach ($action->getTarget() as $target) { $target = (array)$target; foreach ($target as $phid) { $phids[] = $phid; } } } foreach ($rule->getConditions() as $condition) { $value = $condition->getValue(); if (is_array($value)) { foreach ($value as $phid) { $phids[] = $phid; } } } $phids[] = $rule->getAuthorPHID(); if ($rule->isObjectRule()) { $phids[] = $rule->getTriggerObjectPHID(); } return $this->loadViewerHandles($phids); } /** * Render the selector for the "When (all of | any of) these conditions are * met:" element. */ private function renderMustMatchSelector($rule) { return AphrontFormSelectControl::renderSelectTag( $rule->getMustMatchAll() ? 'all' : 'any', array( 'all' => pht('all of'), 'any' => pht('any of'), ), array( 'name' => 'must_match', )); } /** * Render the selector for "Take these actions (every time | only the first * time) this rule matches..." element. */ private function renderRepetitionSelector($rule, HeraldAdapter $adapter) { $repetition_policy = $rule->getRepetitionPolicyStringConstant(); $repetition_map = $this->getRepetitionOptionMap($adapter); if (count($repetition_map) < 2) { return head($repetition_map); } else { return AphrontFormSelectControl::renderSelectTag( $repetition_policy, $repetition_map, array( 'name' => 'repetition_policy', )); } } private function getRepetitionOptionMap(HeraldAdapter $adapter) { $repetition_options = $adapter->getRepetitionOptions(); $repetition_names = HeraldRule::getRepetitionPolicySelectOptionMap(); return array_select_keys($repetition_names, $repetition_options); } protected function buildTokenizerTemplates() { $template = new AphrontTokenizerTemplateView(); $template = $template->render(); return array( 'markup' => $template, ); } /** * Load rules for the "Another Herald rule..." condition dropdown, which * allows one rule to depend upon the success or failure of another rule. */ private function loadRulesThisRuleMayDependUpon(HeraldRule $rule) { $viewer = $this->getRequest()->getUser(); // Any rule can depend on a global rule. $all_rules = id(new HeraldRuleQuery()) ->setViewer($viewer) ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_GLOBAL)) ->withContentTypes(array($rule->getContentType())) ->execute(); if ($rule->isObjectRule()) { // Object rules may depend on other rules for the same object. $all_rules += id(new HeraldRuleQuery()) ->setViewer($viewer) ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_OBJECT)) ->withContentTypes(array($rule->getContentType())) ->withTriggerObjectPHIDs(array($rule->getTriggerObjectPHID())) ->execute(); } if ($rule->isPersonalRule()) { // Personal rules may depend upon your other personal rules. $all_rules += id(new HeraldRuleQuery()) ->setViewer($viewer) ->withRuleTypes(array(HeraldRuleTypeConfig::RULE_TYPE_PERSONAL)) ->withContentTypes(array($rule->getContentType())) ->withAuthorPHIDs(array($rule->getAuthorPHID())) ->execute(); } - // mark disabled rules as disabled since they are not useful as such; - // don't filter though to keep edit cases sane / expected - foreach ($all_rules as $current_rule) { - if ($current_rule->getIsDisabled()) { - $current_rule->makeEphemeral(); - $current_rule->setName($rule->getName().' '.pht('(Disabled)')); - } - } - // A rule can not depend upon itself. unset($all_rules[$rule->getID()]); return $all_rules; } private function getFieldGroups(HeraldAdapter $adapter, array $field_map) { $group_map = array(); foreach ($field_map as $field_key => $field_name) { $group_key = $adapter->getFieldGroupKey($field_key); $group_map[$group_key][$field_key] = $field_name; } return $this->getGroups( $group_map, HeraldFieldGroup::getAllFieldGroups()); } private function getActionGroups(HeraldAdapter $adapter, array $action_map) { $group_map = array(); foreach ($action_map as $action_key => $action_name) { $group_key = $adapter->getActionGroupKey($action_key); $group_map[$group_key][$action_key] = $action_name; } return $this->getGroups( $group_map, HeraldActionGroup::getAllActionGroups()); } private function getGroups(array $item_map, array $group_list) { assert_instances_of($group_list, 'HeraldGroup'); $groups = array(); foreach ($item_map as $group_key => $options) { asort($options); $group_object = idx($group_list, $group_key); if ($group_object) { $group_label = $group_object->getGroupLabel(); $group_order = $group_object->getSortKey(); } else { $group_label = nonempty($group_key, pht('Other')); $group_order = 'Z'; } $groups[] = array( 'label' => $group_label, 'options' => $options, 'order' => $group_order, ); } return array_values(isort($groups, 'order')); } } diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index a9c131e717..07be991bda 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -1,394 +1,410 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort255', 'contentType' => 'text255', 'mustMatchAll' => 'bool', 'configVersion' => 'uint32', 'repetitionPolicy' => 'text32', 'ruleType' => 'text32', 'isDisabled' => 'uint32', 'triggerObjectPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_name' => array( 'columns' => array('name(128)'), ), 'key_author' => array( 'columns' => array('authorPHID'), ), 'key_ruletype' => array( 'columns' => array('ruleType'), ), 'key_trigger' => array( 'columns' => array('triggerObjectPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(HeraldRulePHIDType::TYPECONST); } public function getRuleApplied($phid) { return $this->assertAttachedKey($this->ruleApplied, $phid); } public function setRuleApplied($phid, $applied) { if ($this->ruleApplied === self::ATTACHABLE) { $this->ruleApplied = array(); } $this->ruleApplied[$phid] = $applied; return $this; } public function loadConditions() { if (!$this->getID()) { return array(); } return id(new HeraldCondition())->loadAllWhere( 'ruleID = %d', $this->getID()); } public function attachConditions(array $conditions) { assert_instances_of($conditions, 'HeraldCondition'); $this->conditions = $conditions; return $this; } public function getConditions() { // TODO: validate conditions have been attached. return $this->conditions; } public function loadActions() { if (!$this->getID()) { return array(); } return id(new HeraldActionRecord())->loadAllWhere( 'ruleID = %d', $this->getID()); } public function attachActions(array $actions) { // TODO: validate actions have been attached. assert_instances_of($actions, 'HeraldActionRecord'); $this->actions = $actions; return $this; } public function getActions() { return $this->actions; } public function saveConditions(array $conditions) { assert_instances_of($conditions, 'HeraldCondition'); return $this->saveChildren( id(new HeraldCondition())->getTableName(), $conditions); } public function saveActions(array $actions) { assert_instances_of($actions, 'HeraldActionRecord'); return $this->saveChildren( id(new HeraldActionRecord())->getTableName(), $actions); } protected function saveChildren($table_name, array $children) { assert_instances_of($children, 'HeraldDAO'); if (!$this->getID()) { throw new PhutilInvalidStateException('save'); } foreach ($children as $child) { $child->setRuleID($this->getID()); } $this->openTransaction(); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', $table_name, $this->getID()); foreach ($children as $child) { $child->save(); } $this->saveTransaction(); } public function delete() { $this->openTransaction(); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', id(new HeraldCondition())->getTableName(), $this->getID()); queryfx( $this->establishConnection('w'), 'DELETE FROM %T WHERE ruleID = %d', id(new HeraldActionRecord())->getTableName(), $this->getID()); $result = parent::delete(); $this->saveTransaction(); return $result; } public function hasValidAuthor() { return $this->assertAttached($this->validAuthor); } public function attachValidAuthor($valid) { $this->validAuthor = $valid; return $this; } public function getAuthor() { return $this->assertAttached($this->author); } public function attachAuthor(PhabricatorUser $user) { $this->author = $user; return $this; } public function isGlobalRule() { return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_GLOBAL); } public function isPersonalRule() { return ($this->getRuleType() === HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); } public function isObjectRule() { return ($this->getRuleType() == HeraldRuleTypeConfig::RULE_TYPE_OBJECT); } public function attachTriggerObject($trigger_object) { $this->triggerObject = $trigger_object; return $this; } public function getTriggerObject() { return $this->assertAttached($this->triggerObject); } /** * Get a sortable key for rule execution order. * * Rules execute in a well-defined order: personal rules first, then object * rules, then global rules. Within each rule type, rules execute from lowest * ID to highest ID. * * This ordering allows more powerful rules (like global rules) to override * weaker rules (like personal rules) when multiple rules exist which try to * affect the same field. Executing from low IDs to high IDs makes * interactions easier to understand when adding new rules, because the newest * rules always happen last. * * @return string A sortable key for this rule. */ public function getRuleExecutionOrderSortKey() { $rule_type = $this->getRuleType(); switch ($rule_type) { case HeraldRuleTypeConfig::RULE_TYPE_PERSONAL: $type_order = 1; break; case HeraldRuleTypeConfig::RULE_TYPE_OBJECT: $type_order = 2; break; case HeraldRuleTypeConfig::RULE_TYPE_GLOBAL: $type_order = 3; break; default: throw new Exception(pht('Unknown rule type "%s"!', $rule_type)); } return sprintf('~%d%010d', $type_order, $this->getID()); } public function getMonogram() { return 'H'.$this->getID(); } public function getURI() { return '/'.$this->getMonogram(); } + public function getEditorSortVector() { + return id(new PhutilSortVector()) + ->addInt($this->getIsDisabled() ? 1 : 0) + ->addString($this->getName()); + } + + public function getEditorDisplayName() { + $name = pht('%s %s', $this->getMonogram(), $this->getName()); + + if ($this->getIsDisabled()) { + $name = pht('%s (Disabled)', $name); + } + + return $name; + } + /* -( Repetition Policies )------------------------------------------------ */ public function getRepetitionPolicyStringConstant() { return $this->getRepetitionPolicy(); } public function setRepetitionPolicyStringConstant($value) { $map = self::getRepetitionPolicyMap(); if (!isset($map[$value])) { throw new Exception( pht( 'Rule repetition string constant "%s" is unknown.', $value)); } return $this->setRepetitionPolicy($value); } public function isRepeatEvery() { return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_EVERY); } public function isRepeatFirst() { return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_FIRST); } public function isRepeatOnChange() { return ($this->getRepetitionPolicyStringConstant() === self::REPEAT_CHANGE); } public static function getRepetitionPolicySelectOptionMap() { $map = self::getRepetitionPolicyMap(); return ipull($map, 'select'); } private static function getRepetitionPolicyMap() { return array( self::REPEAT_EVERY => array( 'select' => pht('every time this rule matches:'), ), self::REPEAT_FIRST => array( 'select' => pht('only the first time this rule matches:'), ), self::REPEAT_CHANGE => array( 'select' => pht('if this rule did not match the last time:'), ), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HeraldRuleEditor(); } public function getApplicationTransactionTemplate() { return new HeraldRuleTransaction(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { return PhabricatorPolicies::getMostOpenPolicy(); } if ($this->isGlobalRule()) { $app = 'PhabricatorHeraldApplication'; $herald = PhabricatorApplication::getByClass($app); $global = HeraldManageGlobalRulesCapability::CAPABILITY; return $herald->getPolicy($global); } else if ($this->isObjectRule()) { return $this->getTriggerObject()->getPolicy($capability); } else { return $this->getAuthorPHID(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { return null; } if ($this->isGlobalRule()) { return pht( 'Global Herald rules can be edited by users with the "Can Manage '. 'Global Rules" Herald application permission.'); } else if ($this->isObjectRule()) { return pht('Object rules inherit the edit policies of their objects.'); } else { return pht('A personal rule can only be edited by its owner.'); } } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return $this->isPersonalRule() && $phid == $this->getAuthorPHID(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); $this->saveTransaction(); } }