diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index c6551b3f15..fa8879efa5 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -1,7 +1,7 @@ contentSource = $content_source; @@ -132,6 +134,10 @@ abstract class HeraldAdapter { case self::FIELD_IS_NEW_OBJECT: return $this->getIsNewObject(); default: + if ($this->isHeraldCustomKey($field_name)) { + return $this->getCustomFieldValue($field_name); + } + throw new Exception( "Unknown field '{$field_name}'!"); } @@ -195,10 +201,20 @@ abstract class HeraldAdapter { public function getFields() { - return array( - self::FIELD_ALWAYS, - self::FIELD_RULE, - ); + $fields = array(); + + $fields[] = self::FIELD_ALWAYS; + $fields[] = self::FIELD_RULE; + + $custom_fields = $this->getCustomFields(); + if ($custom_fields) { + foreach ($custom_fields->getFields() as $custom_field) { + $key = $custom_field->getFieldKey(); + $fields[] = $this->getHeraldKeyFromCustomKey($key); + } + } + + return $fields; } public function getFieldNameMap() { @@ -242,7 +258,7 @@ abstract class HeraldAdapter { self::FIELD_TASK_PRIORITY => pht('Task priority'), self::FIELD_ARCANIST_PROJECT => pht('Arcanist Project'), self::FIELD_PUSHER_IS_COMMITTER => pht('Pusher same as committer'), - ); + ) + $this->getCustomFieldNameMap(); } @@ -270,6 +286,7 @@ abstract class HeraldAdapter { self::CONDITION_EXISTS => pht('exists'), self::CONDITION_NOT_EXISTS => pht('does not exist'), self::CONDITION_UNCONDITIONALLY => '', // don't show anything! + self::CONDITION_NEVER => '', // don't show anything! self::CONDITION_REGEXP_PAIR => pht('matches regexp pair'), self::CONDITION_HAS_BIT => pht('has bit'), self::CONDITION_NOT_BIT => pht('lacks bit'), @@ -380,6 +397,9 @@ abstract class HeraldAdapter { self::CONDITION_IS_FALSE, ); default: + if ($this->isHeraldCustomKey($field)) { + return $this->getCustomFieldConditions($field); + } throw new Exception( "This adapter does not define conditions for field '{$field}'!"); } @@ -456,6 +476,8 @@ abstract class HeraldAdapter { return !$field_value; case self::CONDITION_UNCONDITIONALLY: return (bool)$field_value; + case self::CONDITION_NEVER: + return false; case self::CONDITION_REGEXP: foreach ((array)$field_value as $value) { // We add the 'S' flag because we use the regexp multiple times. @@ -602,6 +624,7 @@ abstract class HeraldAdapter { case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: case self::CONDITION_UNCONDITIONALLY: + case self::CONDITION_NEVER: case self::CONDITION_HAS_BIT: case self::CONDITION_NOT_BIT: case self::CONDITION_IS_TRUE: @@ -710,6 +733,16 @@ abstract class HeraldAdapter { public function getValueTypeForFieldAndCondition($field, $condition) { + + if ($this->isHeraldCustomKey($field)) { + $value_type = $this->getCustomFieldValueTypeForFieldAndCondition( + $field, + $condition); + if ($value_type !== null) { + return $value_type; + } + } + switch ($condition) { case self::CONDITION_CONTAINS: case self::CONDITION_NOT_CONTAINS: @@ -766,6 +799,7 @@ abstract class HeraldAdapter { case self::CONDITION_EXISTS: case self::CONDITION_NOT_EXISTS: case self::CONDITION_UNCONDITIONALLY: + case self::CONDITION_NEVER: case self::CONDITION_IS_TRUE: case self::CONDITION_IS_FALSE: return self::VALUE_NONE; @@ -959,7 +993,12 @@ abstract class HeraldAdapter { array $handles) { $field_type = $condition->getFieldName(); - $field_name = idx($this->getFieldNameMap(), $field_type); + + $default = $this->isHeraldCustomKey($field_type) + ? pht('(Unknown Custom Field "%s")', $field_type) + : pht('(Unknown Field "%s")', $field_type); + + $field_name = idx($this->getFieldNameMap(), $field_type, $default); $condition_type = $condition->getFieldCondition(); $condition_name = idx($this->getConditionNameMap(), $condition_type); @@ -1085,4 +1124,205 @@ abstract class HeraldAdapter { return $phids; } +/* -( Custom Field Integration )------------------------------------------- */ + + + /** + * Return an object which custom fields can be generated from while editing + * rules. Adapters must return an object from this method to enable custom + * field rules. + * + * Normally, you'll return an empty version of the adapted object, assuming + * it implements @{interface:PhabricatorCustomFieldInterface}: + * + * return new ApplicationObject(); + * + * This is normally the only adapter method you need to override to enable + * Herald rules to run against custom fields. + * + * @return null|PhabricatorCustomFieldInterface Template object. + * @task customfield + */ + protected function getCustomFieldTemplateObject() { + return null; + } + + + /** + * Returns the prefix used to namespace Herald fields which are based on + * custom fields. + * + * @return string Key prefix. + * @task customfield + */ + private function getCustomKeyPrefix() { + return 'herald.custom/'; + } + + + /** + * Determine if a field key is based on a custom field or a regular internal + * field. + * + * @param string Field key. + * @return bool True if the field key is based on a custom field. + * @task customfield + */ + private function isHeraldCustomKey($key) { + $prefix = $this->getCustomKeyPrefix(); + return (strncmp($key, $prefix, strlen($prefix)) == 0); + } + + + /** + * Convert a custom field key into a Herald field key. + * + * @param string Custom field key. + * @return string Herald field key. + * @task customfield + */ + private function getHeraldKeyFromCustomKey($key) { + return $this->getCustomKeyPrefix().$key; + } + + + /** + * Get custom fields for this adapter, if appliable. This will either return + * a field list or `null` if the adapted object does not implement custom + * fields or the adapter does not support them. + * + * @return PhabricatorCustomFieldList|null List of fields, or `null`. + * @task customfield + */ + private function getCustomFields() { + if ($this->customFields === false) { + $this->customFields = null; + + + $template_object = $this->getCustomFieldTemplateObject(); + if ($template_object) { + $object = $this->getObject(); + if (!$object) { + $object = $template_object; + } + + $fields = PhabricatorCustomField::getObjectFields( + $object, + PhabricatorCustomField::ROLE_HERALD); + $fields->setViewer(PhabricatorUser::getOmnipotentUser()); + $fields->readFieldsFromStorage($object); + + $this->customFields = $fields; + } + } + + return $this->customFields; + } + + + /** + * Get a custom field by Herald field key, or `null` if it does not exist + * or custom fields are not supported. + * + * @param string Herald field key. + * @return PhabricatorCustomField|null Matching field, if it exists. + * @task customfield + */ + private function getCustomField($herald_field_key) { + $fields = $this->getCustomFields(); + if (!$fields) { + return null; + } + + foreach ($fields->getFields() as $custom_field) { + $key = $custom_field->getFieldKey(); + if ($this->getHeraldKeyFromCustomKey($key) == $herald_field_key) { + return $custom_field; + } + } + + return null; + } + + + /** + * Get the field map for custom fields. + * + * @return map Map of Herald field keys to field names. + * @task customfield + */ + private function getCustomFieldNameMap() { + $fields = $this->getCustomFields(); + if (!$fields) { + return array(); + } + + $map = array(); + foreach ($fields->getFields() as $field) { + $key = $field->getFieldKey(); + $name = $field->getHeraldFieldName(); + $map[$this->getHeraldKeyFromCustomKey($key)] = $name; + } + + return $map; + } + + + /** + * Get the value for a custom field. + * + * @param string Herald field key. + * @return wild Custom field value. + * @task customfield + */ + private function getCustomFieldValue($field_key) { + $field = $this->getCustomField($field_key); + if (!$field) { + return null; + } + + return $field->getHeraldFieldValue(); + } + + + /** + * Get the Herald conditions for a custom field. + * + * @param string Herald field key. + * @return list List of Herald conditions. + * @task customfield + */ + private function getCustomFieldConditions($field_key) { + $field = $this->getCustomField($field_key); + if (!$field) { + return array( + self::CONDITION_NEVER, + ); + } + + return $field->getHeraldFieldConditions(); + } + + + /** + * Get the Herald value type for a custom field and condition. + * + * @param string Herald field key. + * @param const Herald condition constant. + * @return const|null Herald value type constant, or null to use the default. + * @task customfield + */ + private function getCustomFieldValueTypeForFieldAndCondition( + $field_key, + $condition) { + + $field = $this->getCustomField($field_key); + if (!$field) { + return self::VALUE_NONE; + } + + return $field->getHeraldFieldValueType($condition); + } + + } diff --git a/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php b/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php index 38153e7442..7fcecb20ff 100644 --- a/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php +++ b/src/applications/herald/adapter/HeraldManiphestTaskAdapter.php @@ -205,4 +205,9 @@ final class HeraldManiphestTaskAdapter extends HeraldAdapter { } return $result; } + + protected function getCustomFieldTemplateObject() { + return new ManiphestTask(); + } + } diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index b9a74fda23..c6a4b483f8 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -424,6 +424,15 @@ final class HeraldRuleController extends HeraldController { $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) { + if (empty($field_map[$condition->getFieldName()])) { + $field_map[$condition->getFieldName()] = pht(''); + } + } + $actions = $adapter->getActions($rule->getRuleType()); $action_map = array_select_keys($all_actions, $actions); diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index 11e83f1570..d57be2dba5 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -17,7 +17,7 @@ final class HeraldRule extends HeraldDAO protected $isDisabled = 0; protected $triggerObjectPHID; - protected $configVersion = 35; + protected $configVersion = 36; // phids for which this rule has been applied private $ruleApplied = self::ATTACHABLE; diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index 062b9da9aa..2d1d45d709 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -13,6 +13,7 @@ * @task appxaction Integration with ApplicationTransactions * @task xactionmail Integration with Transaction Mail * @task globalsearch Integration with Global Search + * @task herald Integration with Herald */ abstract class PhabricatorCustomField { @@ -30,6 +31,7 @@ abstract class PhabricatorCustomField { const ROLE_LIST = 'list'; const ROLE_GLOBALSEARCH = 'GlobalSearch'; const ROLE_CONDUIT = 'conduit'; + const ROLE_HERALD = 'herald'; /* -( Building Applications with Custom Fields )--------------------------- */ @@ -268,6 +270,8 @@ abstract class PhabricatorCustomField { return $this->shouldAppearInConduitDictionary(); case self::ROLE_TRANSACTIONMAIL: return $this->shouldAppearInTransactionMail(); + case self::ROLE_HERALD: + return $this->shouldAppearInHerald(); case self::ROLE_DEFAULT: return true; default: @@ -1257,4 +1261,80 @@ abstract class PhabricatorCustomField { throw new PhabricatorCustomFieldImplementationIncompleteException($this); } + +/* -( Herald )------------------------------------------------------------- */ + + + /** + * Return `true` to make this field available in Herald. + * + * @return bool True to expose the field in Herald. + * @task herald + */ + public function shouldAppearInHerald() { + if ($this->proxy) { + return $this->proxy->shouldAppearInHerald(); + } + return false; + } + + + /** + * Get the name of the field in Herald. By default, this uses the + * normal field name. + * + * @return string Herald field name. + * @task herald + */ + public function getHeraldFieldName() { + if ($this->proxy) { + return $this->proxy->getHeraldFieldName(); + } + return $this->getFieldName(); + } + + + /** + * Get the field value for evaluation by Herald. + * + * @return wild Field value. + * @task herald + */ + public function getHeraldFieldValue() { + if ($this->proxy) { + return $this->proxy->getHeraldFieldValue(); + } + throw new PhabricatorCustomFieldImplementationIncompleteException($this); + } + + + /** + * Get the available conditions for this field in Herald. + * + * @return list List of Herald condition constants. + * @task herald + */ + public function getHeraldFieldConditions() { + if ($this->proxy) { + return $this->proxy->getHeraldFieldConditions(); + } + throw new PhabricatorCustomFieldImplementationIncompleteException($this); + } + + + /** + * Get the Herald value type for the given condition. + * + * @param const Herald condition constant. + * @return const|null Herald value type, or null to use the default. + * @task herald + */ + public function getHeraldFieldValueType($condition) { + if ($this->proxy) { + return $this->proxy->getHeraldFieldValueType($condition); + } + return null; + } + + } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php index 9df9e6f1e8..0a7ef680fd 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php @@ -382,5 +382,8 @@ abstract class PhabricatorStandardCustomField } } + public function getHeraldFieldValue() { + return $this->getFieldValue(); + } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php index 75a1bf0b29..cde3beb5d9 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php @@ -111,5 +111,15 @@ final class PhabricatorStandardCustomFieldBool } } + public function shouldAppearInHerald() { + return true; + } + + public function getHeraldFieldConditions() { + return array( + HeraldAdapter::CONDITION_IS_TRUE, + HeraldAdapter::CONDITION_IS_FALSE, + ); + } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php index 0d6992e282..4eb06e66f2 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php @@ -158,4 +158,18 @@ abstract class PhabricatorStandardCustomFieldPHIDs } } + public function shouldAppearInHerald() { + return true; + } + + public function getHeraldFieldConditions() { + return array( + HeraldAdapter::CONDITION_INCLUDE_ALL, + HeraldAdapter::CONDITION_INCLUDE_ANY, + HeraldAdapter::CONDITION_INCLUDE_NONE, + HeraldAdapter::CONDITION_EXISTS, + HeraldAdapter::CONDITION_NOT_EXISTS, + ); + } + } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php index 3bd3bcd85a..931bfc6c9b 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php @@ -49,5 +49,19 @@ final class PhabricatorStandardCustomFieldRemarkup $this->getFieldName()); } + public function shouldAppearInHerald() { + return true; + } + + public function getHeraldFieldConditions() { + return array( + HeraldAdapter::CONDITION_CONTAINS, + HeraldAdapter::CONDITION_NOT_CONTAINS, + HeraldAdapter::CONDITION_IS, + HeraldAdapter::CONDITION_IS_NOT, + HeraldAdapter::CONDITION_REGEXP, + ); + } + } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php index ef4c0a53aa..92967c8de9 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php @@ -50,4 +50,18 @@ final class PhabricatorStandardCustomFieldText ->setValue($value)); } + public function shouldAppearInHerald() { + return true; + } + + public function getHeraldFieldConditions() { + return array( + HeraldAdapter::CONDITION_CONTAINS, + HeraldAdapter::CONDITION_NOT_CONTAINS, + HeraldAdapter::CONDITION_IS, + HeraldAdapter::CONDITION_IS_NOT, + HeraldAdapter::CONDITION_REGEXP, + ); + } + } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php index 4534191766..2ff1d4589c 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php @@ -44,4 +44,9 @@ final class PhabricatorStandardCustomFieldUsers $form->appendChild($control); } + + public function getHeraldFieldValueType($condition) { + return HeraldAdapter::VALUE_USER; + } + }