1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 23:02:42 +01:00

Distinguish between "bad record format" and "bad record value" when validating Trigger rules

Summary:
Depends on D20416. Ref T13269. See D20329.

If you try to save an "Assign to" rule with no assignee, we currently replace the control with an "InvalidRule" control that isn't editable. We'd prefer to give you an empty field back and let you pick a different value.

Differentiate between "bad record format" (i.e., we can't really do anything with this) and "bad record value" (i.e., everything is structurally fine, you just typed the wrong thing). In the latter case, we still build a properly typed rule for the UI, we just refuse to update storage until you fix the problem.

Test Plan:
First, hit the original issue and got a nicer UI with a more consistent control width (note full-width error):

{F6374205}

Then, applied the rest of the patch and got a normal "fix the issue" form state instead of a dead-end:

{F6374211}

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13269

Differential Revision: https://secure.phabricator.com/D20417
This commit is contained in:
epriestley 2019-04-13 09:29:37 -07:00
parent 5b6d6c4fb3
commit 870b01f2d0
13 changed files with 113 additions and 35 deletions

View file

@ -100,7 +100,7 @@ return array(
'rsrc/css/application/policy/policy.css' => 'ceb56a08', 'rsrc/css/application/policy/policy.css' => 'ceb56a08',
'rsrc/css/application/ponder/ponder-view.css' => '05a09d0a', 'rsrc/css/application/ponder/ponder-view.css' => '05a09d0a',
'rsrc/css/application/project/project-card-view.css' => '4e7371cd', 'rsrc/css/application/project/project-card-view.css' => '4e7371cd',
'rsrc/css/application/project/project-triggers.css' => 'cb866c2d', 'rsrc/css/application/project/project-triggers.css' => 'cd9c8bb9',
'rsrc/css/application/project/project-view.css' => '567858b3', 'rsrc/css/application/project/project-view.css' => '567858b3',
'rsrc/css/application/releeph/releeph-core.css' => 'f81ff2db', 'rsrc/css/application/releeph/releeph-core.css' => 'f81ff2db',
'rsrc/css/application/releeph/releeph-preview-branch.css' => '22db5c07', 'rsrc/css/application/releeph/releeph-preview-branch.css' => '22db5c07',
@ -882,7 +882,7 @@ return array(
'policy-transaction-detail-css' => 'c02b8384', 'policy-transaction-detail-css' => 'c02b8384',
'ponder-view-css' => '05a09d0a', 'ponder-view-css' => '05a09d0a',
'project-card-view-css' => '4e7371cd', 'project-card-view-css' => '4e7371cd',
'project-triggers-css' => 'cb866c2d', 'project-triggers-css' => 'cd9c8bb9',
'project-view-css' => '567858b3', 'project-view-css' => '567858b3',
'releeph-core' => 'f81ff2db', 'releeph-core' => 'f81ff2db',
'releeph-preview-branch' => '22db5c07', 'releeph-preview-branch' => '22db5c07',

View file

@ -104,9 +104,9 @@ final class PhabricatorProjectTrigger
$allow_invalid, $allow_invalid,
PhabricatorUser $viewer) { PhabricatorUser $viewer) {
// NOTE: With "$allow_invalid" set, we're trying to preserve the database // NOTE: With "$allow_invalid" set, we're trying to preserve the database
// state in the rule structure, even if it includes rule types we don't // state in the rule structure, even if it includes rule types we don't
// ha ve implementations for, or rules with invalid rule values. // have implementations for, or rules with invalid rule values.
// If an administrator adds or removes extensions which add rules, or // If an administrator adds or removes extensions which add rules, or
// an upgrade affects rule validity, existing rules may become invalid. // an upgrade affects rule validity, existing rules may become invalid.

View file

@ -5,15 +5,15 @@ final class PhabricatorProjectTriggerAddProjectsRule
const TRIGGERTYPE = 'task.projects.add'; const TRIGGERTYPE = 'task.projects.add';
public function getSelectControLname() { public function getSelectControlName() {
return pht('Add projects'); return pht('Add project tags');
} }
protected function getValueForEditorField() { protected function getValueForEditorField() {
return $this->getDatasource()->getWireTokens($this->getValue()); return $this->getDatasource()->getWireTokens($this->getValue());
} }
protected function assertValidRuleValue($value) { protected function assertValidRuleRecordFormat($value) {
if (!is_array($value)) { if (!is_array($value)) {
throw new Exception( throw new Exception(
pht( pht(
@ -23,6 +23,14 @@ final class PhabricatorProjectTriggerAddProjectsRule
} }
} }
protected function assertValidRuleRecordValue($value) {
if (!$value) {
throw new Exception(
pht(
'You must select at least one project tag to add.'));
}
}
protected function newDropTransactions($object, $value) { protected function newDropTransactions($object, $value) {
$project_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $project_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
@ -78,12 +86,12 @@ final class PhabricatorProjectTriggerAddProjectsRule
} }
public function getRuleViewLabel() { public function getRuleViewLabel() {
return pht('Add Projects'); return pht('Add Project Tags');
} }
public function getRuleViewDescription($value) { public function getRuleViewDescription($value) {
return pht( return pht(
'Add projects: %s.', 'Add project tags: %s.',
phutil_tag( phutil_tag(
'strong', 'strong',
array(), array(),

View file

@ -24,7 +24,7 @@ final class PhabricatorProjectTriggerInvalidRule
return false; return false;
} }
protected function assertValidRuleValue($value) { protected function assertValidRuleRecordFormat($value) {
return; return;
} }

View file

@ -21,21 +21,43 @@ final class PhabricatorProjectTriggerManiphestOwnerRule
return $value; return $value;
} }
protected function assertValidRuleValue($value) { protected function assertValidRuleRecordFormat($value) {
if (!is_array($value)) { if (!is_array($value)) {
throw new Exception( throw new Exception(
pht( pht(
'Owner rule value should be a list, but is not (value is "%s").', 'Owner rule value should be a list, but is not (value is "%s").',
phutil_describe_type($value))); phutil_describe_type($value)));
} }
}
if (count($value) != 1) { protected function assertValidRuleRecordValue($value) {
if (!$value) {
throw new Exception( throw new Exception(
pht( pht(
'Owner rule value should be a list of exactly one user PHID, or the '. 'Owner rule value is required. Specify a user to assign tasks '.
'token "none()" (value is "%s").', 'to, or the token "none()" to unassign tasks.'));
}
if (count($value) > 1) {
throw new Exception(
pht(
'Owner rule value must have only one elmement (value is "%s").',
implode(', ', $value))); implode(', ', $value)));
} }
$owner_phid = $this->convertTokenizerValueToOwner($value);
if ($owner_phid !== null) {
$user = id(new PhabricatorPeopleQuery())
->setViewer($this->getViewer())
->withPHIDs(array($owner_phid))
->executeOne();
if (!$user) {
throw new Exception(
pht(
'User PHID ("%s") is not a valid user.',
$owner_phid));
}
}
} }
protected function newDropTransactions($object, $value) { protected function newDropTransactions($object, $value) {

View file

@ -9,20 +9,24 @@ final class PhabricatorProjectTriggerManiphestPriorityRule
return pht('Change priority to'); return pht('Change priority to');
} }
protected function assertValidRuleValue($value) { protected function assertValidRuleRecordFormat($value) {
if (!is_string($value)) { if (!is_string($value)) {
throw new Exception( throw new Exception(
pht( pht(
'Priority rule value should be a string, but is not (value is "%s").', 'Priority rule value should be a string, but is not (value is "%s").',
phutil_describe_type($value))); phutil_describe_type($value)));
} }
}
protected function assertValidRuleRecordValue($value) {
$map = ManiphestTaskPriority::getTaskPriorityMap(); $map = ManiphestTaskPriority::getTaskPriorityMap();
if (!isset($map[$value])) { if (!isset($map[$value])) {
throw new Exception( throw new Exception(
pht( pht(
'Rule value ("%s") is not a valid task priority.', 'Task priority value ("%s") is not a valid task priority. '.
$value)); 'Valid priorities are: %s.',
$value,
implode(', ', array_keys($map))));
} }
} }

View file

@ -9,20 +9,24 @@ final class PhabricatorProjectTriggerManiphestStatusRule
return pht('Change status to'); return pht('Change status to');
} }
protected function assertValidRuleValue($value) { protected function assertValidRuleRecordFormat($value) {
if (!is_string($value)) { if (!is_string($value)) {
throw new Exception( throw new Exception(
pht( pht(
'Status rule value should be a string, but is not (value is "%s").', 'Status rule value should be a string, but is not (value is "%s").',
phutil_describe_type($value))); phutil_describe_type($value)));
} }
}
protected function assertValidRuleRecordValue($value) {
$map = ManiphestTaskStatus::getTaskStatusMap(); $map = ManiphestTaskStatus::getTaskStatusMap();
if (!isset($map[$value])) { if (!isset($map[$value])) {
throw new Exception( throw new Exception(
pht( pht(
'Rule value ("%s") is not a valid task status.', 'Task status value ("%s") is not a valid task status. '.
$value)); 'Valid statues are: %s.',
$value,
implode(', ', array_keys($map))));
} }
} }

View file

@ -9,20 +9,21 @@ final class PhabricatorProjectTriggerPlaySoundRule
return pht('Play sound'); return pht('Play sound');
} }
protected function assertValidRuleValue($value) { protected function assertValidRuleRecordFormat($value) {
if (!is_string($value)) { if (!is_string($value)) {
throw new Exception( throw new Exception(
pht( pht(
'Status rule value should be a string, but is not (value is "%s").', 'Status rule value should be a string, but is not (value is "%s").',
phutil_describe_type($value))); phutil_describe_type($value)));
} }
}
protected function assertValidRuleRecordValue($value) {
$map = self::getSoundMap(); $map = self::getSoundMap();
if (!isset($map[$value])) { if (!isset($map[$value])) {
throw new Exception( throw new Exception(
pht( pht(
'Rule value ("%s") is not a valid sound.', 'Sound ("%s") is not a valid sound.',
$value)); $value));
} }
} }

View file

@ -5,15 +5,15 @@ final class PhabricatorProjectTriggerRemoveProjectsRule
const TRIGGERTYPE = 'task.projects.remove'; const TRIGGERTYPE = 'task.projects.remove';
public function getSelectControLname() { public function getSelectControlname() {
return pht('Remove projects'); return pht('Remove project tags');
} }
protected function getValueForEditorField() { protected function getValueForEditorField() {
return $this->getDatasource()->getWireTokens($this->getValue()); return $this->getDatasource()->getWireTokens($this->getValue());
} }
protected function assertValidRuleValue($value) { protected function assertValidRuleRecordFormat($value) {
if (!is_array($value)) { if (!is_array($value)) {
throw new Exception( throw new Exception(
pht( pht(
@ -23,6 +23,14 @@ final class PhabricatorProjectTriggerRemoveProjectsRule
} }
} }
protected function assertValidRuleRecordValue($value) {
if (!$value) {
throw new Exception(
pht(
'You must select at least one project tag to remove.'));
}
}
protected function newDropTransactions($object, $value) { protected function newDropTransactions($object, $value) {
$project_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $project_edge_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST;
@ -78,12 +86,12 @@ final class PhabricatorProjectTriggerRemoveProjectsRule
} }
public function getRuleViewLabel() { public function getRuleViewLabel() {
return pht('Remove Projects'); return pht('Remove Project Tags');
} }
public function getRuleViewDescription($value) { public function getRuleViewDescription($value) {
return pht( return pht(
'Remove projects: %s.', 'Remove project tags: %s.',
phutil_tag( phutil_tag(
'strong', 'strong',
array(), array(),

View file

@ -23,7 +23,7 @@ abstract class PhabricatorProjectTriggerRule
final public function setRecord(PhabricatorProjectTriggerRuleRecord $record) { final public function setRecord(PhabricatorProjectTriggerRuleRecord $record) {
$value = $record->getValue(); $value = $record->getValue();
$this->assertValidRuleValue($value); $this->assertValidRuleRecordFormat($value);
$this->record = $record; $this->record = $record;
return $this; return $this;
@ -45,7 +45,22 @@ abstract class PhabricatorProjectTriggerRule
abstract public function getRuleViewLabel(); abstract public function getRuleViewLabel();
abstract public function getRuleViewDescription($value); abstract public function getRuleViewDescription($value);
abstract public function getRuleViewIcon($value); abstract public function getRuleViewIcon($value);
abstract protected function assertValidRuleValue($value); abstract protected function assertValidRuleRecordFormat($value);
final public function getRuleRecordValueValidationException() {
try {
$this->assertValidRuleRecordValue($this->getRecord()->getValue());
} catch (Exception $ex) {
return $ex;
}
return null;
}
protected function assertValidRuleRecordValue($value) {
return;
}
abstract protected function newDropTransactions($object, $value); abstract protected function newDropTransactions($object, $value);
abstract protected function newDropEffects($value); abstract protected function newDropEffects($value);
abstract protected function getDefaultValue(); abstract protected function getDefaultValue();

View file

@ -13,7 +13,7 @@ final class PhabricatorProjectTriggerUnknownRule
return false; return false;
} }
protected function assertValidRuleValue($value) { protected function assertValidRuleRecordFormat($value) {
return; return;
} }

View file

@ -20,16 +20,18 @@ final class PhabricatorProjectTriggerRulesetTransaction
} }
public function validateTransactions($object, array $xactions) { public function validateTransactions($object, array $xactions) {
$actor = $this->getActor();
$errors = array(); $errors = array();
foreach ($xactions as $xaction) { foreach ($xactions as $xaction) {
$ruleset = $xaction->getNewValue(); $ruleset = $xaction->getNewValue();
try { try {
PhabricatorProjectTrigger::newTriggerRulesFromRuleSpecifications( $rules =
$ruleset, PhabricatorProjectTrigger::newTriggerRulesFromRuleSpecifications(
$allow_invalid = false, $ruleset,
$xaction->getViewer()); $allow_invalid = false,
$actor);
} catch (PhabricatorProjectTriggerCorruptionException $ex) { } catch (PhabricatorProjectTriggerCorruptionException $ex) {
$errors[] = $this->newInvalidError( $errors[] = $this->newInvalidError(
pht( pht(
@ -38,6 +40,19 @@ final class PhabricatorProjectTriggerRulesetTransaction
$xaction); $xaction);
continue; continue;
} }
foreach ($rules as $rule) {
$exception = $rule->getRuleRecordValueValidationException();
if ($exception) {
$errors[] = $this->newInvalidError(
pht(
'Value for "%s" rule is invalid: %s',
$rule->getSelectControlName(),
$exception->getMessage()),
$xaction);
continue;
}
}
} }
return $errors; return $errors;

View file

@ -27,6 +27,7 @@
.trigger-rules-table td.invalid-cell { .trigger-rules-table td.invalid-cell {
padding-left: 12px; padding-left: 12px;
width: 100%;
} }
.trigger-rules-table td.invalid-cell .phui-icon-view { .trigger-rules-table td.invalid-cell .phui-icon-view {