diff --git a/moz-extensions/src/__phutil_library_map__.php b/moz-extensions/src/__phutil_library_map__.php index 2baf878368..600953cfb5 100644 --- a/moz-extensions/src/__phutil_library_map__.php +++ b/moz-extensions/src/__phutil_library_map__.php @@ -18,6 +18,9 @@ 'DifferentialBugzillaBugIDCustomFieldTestCase' => 'differential/customfield/__tests__/DifferentialBugzillaIdCustomFieldTestCase.php', 'DifferentialBugzillaBugIDField' => 'differential/customfield/DifferentialBugzillaBugIDField.php', 'DifferentialBugzillaBugIDValidator' => 'differential/customfield/DifferentialBugzillaBugIDValidator.php', + 'DifferentialUpliftRequestCustomField' => 'differential/customfield/DifferentialUpliftRequestCustomField.php', + 'DifferentialUpliftRequestCustomFieldTestCase' => 'differential/customfield/__tests__/DifferentialUpliftRequestCustomFieldTestCase.php', + 'PhabricatorUpdateUpliftCommentAction' => 'applications/transactions/commentaction/PhabricatorUpdateUpliftCommentAction.php', 'DifferentialRevisionWarning' => 'differential/view/DifferentialRevisionWarning.php', 'EmailAPIAuthorization' => 'email/EmailAPIAuthorization.php', 'EmailAffectedFile' => 'email/model/EmailAffectedFile.php', @@ -119,8 +122,11 @@ 'CreatePolicyConduitAPIMethod' => 'ConduitAPIMethod', 'DifferentialBugzillaBugIDCommitMessageField' => 'DifferentialCommitMessageCustomField', 'DifferentialBugzillaBugIDCustomFieldTestCase' => 'PhabricatorTestCase', + 'DifferentialUpliftRequestCustomFieldTestCase' => 'PhabricatorTestCase', 'DifferentialBugzillaBugIDField' => 'DifferentialStoredCustomField', 'DifferentialBugzillaBugIDValidator' => 'Phobject', + 'DifferentialUpliftRequestCustomField' => 'DifferentialStoredCustomField', + 'PhabricatorUpdateUpliftCommentAction' => 'PhabricatorEditEngineCommentAction', 'DifferentialRevisionWarning' => 'Phobject', 'EmailRevisionAbandoned' => 'PublicEmailBody', 'EmailRevisionAccepted' => 'PublicEmailBody', diff --git a/moz-extensions/src/applications/transactions/commentaction/PhabricatorUpdateUpliftCommentAction.php b/moz-extensions/src/applications/transactions/commentaction/PhabricatorUpdateUpliftCommentAction.php new file mode 100644 index 0000000000..72527e9997 --- /dev/null +++ b/moz-extensions/src/applications/transactions/commentaction/PhabricatorUpdateUpliftCommentAction.php @@ -0,0 +1,26 @@ +<?php + +class PhabricatorUpdateUpliftCommentAction + extends PhabricatorEditEngineCommentAction { + + public function getPHUIXControlType() { + return 'remarkup'; + } + + public function getPHUIXControlSpecification() { + $value = $this->getValue(); + + if (empty($value)) { + $value = $this->getInitialValue(); + } + + if (empty($value)) { + $value = null; + } + + return array( + 'value' => pht($value), + ); + } + +} diff --git a/moz-extensions/src/differential/customfield/DifferentialUpliftRequestCustomField.php b/moz-extensions/src/differential/customfield/DifferentialUpliftRequestCustomField.php new file mode 100644 index 0000000000..7f5e8283ee --- /dev/null +++ b/moz-extensions/src/differential/customfield/DifferentialUpliftRequestCustomField.php @@ -0,0 +1,288 @@ +<?php +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +/** + * Extends Differential with a 'Uplift Request' field. + */ +final class DifferentialUpliftRequestCustomField + extends DifferentialStoredCustomField { + + const BETA_UPLIFT_FIELDS = array( + "User impact if declined", + "Code covered by automated testing", + "Fix verified in Nightly", + "Needs manual QE test", + "Steps to reproduce for manual QE testing", + "Risk associated with taking this patch", + "Explanation of risk level", + "String changes made/needed", + ); + + // How each field is formatted in ReMarkup. + const QUESTION_FORMATTING = "==== %s ===="; + + private $proxy; + +/* -( Core Properties and Field Identity )--------------------------------- */ + + public function readValueFromRequest(AphrontRequest $request) { + $uplift_data = $request->getStr($this->getFieldKey()); + $this->setValue($uplift_data); + } + + public function getFieldKey() { + return 'differential:uplift-request'; + } + + public function getFieldKeyForConduit() { + return 'uplift.request'; + } + + public function getFieldValue() { + return $this->getValue(); + } + + public function getFieldName() { + return pht('Uplift Request form'); + } + + public function getFieldDescription() { + // Rendered in 'Config > Differential > differential.fields' + return pht('Renders uplift request form.'); + } + + public function isFieldEnabled() { + return true; + } + + public function canDisableField() { + // Field can't be switched off in configuration + return false; + } + +/* -( ApplicationTransactions )-------------------------------------------- */ + + public function shouldAppearInApplicationTransactions() { + // Required to be editable + return true; + } + +/* -( Edit View )---------------------------------------------------------- */ + + public function shouldAppearInEditView() { + // Should the field appear in Edit Revision feature + return true; + } + + // How the uplift text is rendered in the "Details" section. + public function renderPropertyViewValue(array $handles) { + if (!strlen($this->getValue())) { + return null; + } + + return new PHUIRemarkupView($this->getViewer(), pht($this->getValue())); + } + + // How the field can be edited in the "Edit Revision" menu. + public function renderEditControl(array $handles) { + if (!$this->isUpliftTagSet()) { + return null; + } + + return id(new PhabricatorRemarkupControl()) + ->setLabel($this->getFieldName()) + ->setCaption(pht('Please answer all questions.')) + ->setName($this->getFieldKey()) + ->setValue($this->getValue(), ''); + } + + // -- Comment action things + + public function getCommentActionLabel() { + return pht('Request Uplift'); + } + + // Return `true` if the `uplift` tag is set on the repository belonging to + // this revision. + private function isUpliftTagSet() { + $revision = $this->getObject(); + $viewer = $this->getViewer(); + + if ($revision == null || $viewer == null) { + return false; + } + + try { + $repository_projects = PhabricatorEdgeQuery::loadDestinationPHIDs( + $revision->getFieldValuesForConduit()['repositoryPHID'], + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); + } catch (Exception $e) { + return false; + } + + if (!(bool)$repository_projects) { + return false; + } + + $uplift_project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withNames(array('uplift')) + ->executeOne(); + + // If the `uplift` project PHID is in the set of all project PHIDs + // attached to the repo, return `true`. + if (in_array($uplift_project->getPHID(), $repository_projects)) { + return true; + } + + return false; + } + + private function getUpliftFormQuestions() { + $questions = array(); + + foreach (self::BETA_UPLIFT_FIELDS as $section) { + $questions[] = sprintf(self::QUESTION_FORMATTING, $section); + $questions[] = "\n"; + } + + return implode("\n", $questions); + } + + public function newCommentAction() { + // Returning `null` causes no comment action to render, effectively + // "disabling" the field. + if (!$this->isUpliftTagSet()) { + return null; + } + + $action = id(new PhabricatorUpdateUpliftCommentAction()) + ->setConflictKey('revision.action') + ->setValue($this->getValue()) + ->setInitialValue($this->getUpliftFormQuestions()) + ->setSubmitButtonText(pht('Request Uplift')); + + return $action; + } + + public function validateUpliftForm($form) { + $validation_errors = array(); + + # Allow clearing the form. + if (empty($form)) { + return $validation_errors; + } + + # Check each question in the form is present as a header + # in the field. + foreach(self::BETA_UPLIFT_FIELDS as $section) { + if (strpos($form, sprintf(self::QUESTION_FORMATTING, $section)) === false) { + $validation_errors[] = "Missing the '$section' field"; + } + } + + return $validation_errors; + } + + public function validateApplicationTransactions( + PhabricatorApplicationTransactionEditor $editor, + $type, array $xactions) { + + $errors = parent::validateApplicationTransactions($editor, $type, $xactions); + + foreach($xactions as $xaction) { + // Validate that the form is correctly filled out + $validation_errors = $this->validateUpliftForm( + $xaction->getNewValue(), + ); + + // Push errors into the revision save stack + foreach($validation_errors as $validation_error) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + '', + pht($validation_error) + ); + } + } + + return $errors; + } + +/* -( Property View )------------------------------------------------------ */ + + public function shouldAppearInPropertyView() { + return true; + } + +/* -( List View )---------------------------------------------------------- */ + + // Switched of as renderOnListItem is undefined + // public function shouldAppearInListView() { + // return true; + // } + + // TODO Find out if/how to implement renderOnListItem + // It throws Incomplete if not overriden, but doesn't appear anywhere else + // except of it's definition in `PhabricatorCustomField` + +/* -( Global Search )------------------------------------------------------ */ + + public function shouldAppearInGlobalSearch() { + return true; + } + +/* -( Conduit )------------------------------------------------------------ */ + + public function shouldAppearInConduitDictionary() { + // Should the field appear in `differential.revision.search` + return true; + } + + public function shouldAppearInConduitTransactions() { + // Required if needs to be saved via Conduit (i.e. from `arc diff`) + return true; + } + + protected function newConduitSearchParameterType() { + return new ConduitStringParameterType(); + } + + protected function newConduitEditParameterType() { + // Define the type of the parameter for Conduit + return new ConduitStringParameterType(); + } + + public function readFieldValueFromConduit(string $value) { + return $value; + } + + public function isFieldEditable() { + // Has to be editable to be written from `arc diff` + return true; + } + + // TODO see what this controls and consider using it + public function shouldDisableByDefault() { + return false; + } + + public function shouldOverwriteWhenCommitMessageIsEdited() { + return false; + } + + public function getApplicationTransactionTitle( + PhabricatorApplicationTransaction $xaction) { + + if($this->proxy) { + return $this->proxy->getApplicationTransactionTitle($xaction); + } + + $author_phid = $xaction->getAuthorPHID(); + + return pht('%s updated the uplift request field.', $xaction->renderHandleLink($author_phid)); + } +} + diff --git a/moz-extensions/src/differential/customfield/__tests__/DifferentialUpliftRequestCustomFieldTestCase.php b/moz-extensions/src/differential/customfield/__tests__/DifferentialUpliftRequestCustomFieldTestCase.php new file mode 100644 index 0000000000..32864e1fcd --- /dev/null +++ b/moz-extensions/src/differential/customfield/__tests__/DifferentialUpliftRequestCustomFieldTestCase.php @@ -0,0 +1,27 @@ +<?php +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +final class DifferentialUpliftRequestCustomFieldTestCase + extends PhabricatorTestCase { + + public function testFormValidation() { + $field = new DifferentialUpliftRequestCustomField(); + // Ensure the field can't be filled without answering all questions + $errors = $field->validateUpliftForm("=== junk ==="); + + $expected = array(); + foreach(DifferentialUpliftRequestCustomField::BETA_UPLIFT_FIELDS as $err) { + $expected[] = "Missing the '$err' field"; + } + $this->assertEqual($expected, $errors); + + // Ensure the field can be set as empty + $errors = $field->validateUpliftForm(""); + $this->assertEqual( + array(), + $errors, + "The empty form leads to errors - should be allowed."); + } +}