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

Validate menu item fields (links, projects, dashboards, applications, forms, etc)

Summary:
Ref T12128. This adds validation to menu items.

This feels a touch flimsy-ish (kind of copy/paste heavy?) but maybe it can be cleaned up a bit once some similar lightweight modular item types (build steps in Harbormaster, blueprints in Drydock) convert.

Test Plan:
  - Tried to create each item with errors (no dashboard, no project, etc). Got appropriate form errors.
  - Created valid items of each type.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T12128

Differential Revision: https://secure.phabricator.com/D17235
This commit is contained in:
epriestley 2017-01-20 11:03:09 -08:00
parent 98a29f3de9
commit 8113b76910
10 changed files with 396 additions and 7 deletions

View file

@ -149,4 +149,47 @@ final class PhabricatorProfileMenuEditEngine
return $fields; return $fields;
} }
protected function getValidationExceptionShortMessage(
PhabricatorApplicationTransactionValidationException $ex,
PhabricatorEditField $field) {
// Menu item properties all have the same transaction type, so we need
// to make sure errors about a specific property (like the URI for a
// link) are only applied to the field for that particular property. If
// we don't do this, the red error text like "Required" will display
// next to every field.
$property_type =
PhabricatorProfileMenuItemConfigurationTransaction::TYPE_PROPERTY;
$xaction_type = $field->getTransactionType();
if ($xaction_type == $property_type) {
$field_key = $field->getKey();
foreach ($ex->getErrors() as $error) {
if ($error->getType() !== $xaction_type) {
continue;
}
$xaction = $error->getTransaction();
if (!$xaction) {
continue;
}
$xaction_setting = $xaction->getMetadataValue('property.key');
if ($xaction_setting != $field_key) {
continue;
}
$short_message = $error->getShortMessage();
if ($short_message !== null) {
return $short_message;
}
}
return null;
}
return parent::getValidationExceptionShortMessage($ex, $field);
}
} }

View file

@ -87,4 +87,39 @@ final class PhabricatorProfileMenuEditor
return parent::applyCustomExternalTransaction($object, $xaction); return parent::applyCustomExternalTransaction($object, $xaction);
} }
protected function validateTransaction(
PhabricatorLiskDAO $object,
$type,
array $xactions) {
$errors = parent::validateTransaction($object, $type, $xactions);
$actor = $this->getActor();
$menu_item = $object->getMenuItem();
$menu_item->setViewer($actor);
switch ($type) {
case PhabricatorProfileMenuItemConfigurationTransaction::TYPE_PROPERTY:
$key_map = array();
foreach ($xactions as $xaction) {
$xaction_key = $xaction->getMetadataValue('property.key');
$old = $this->getCustomTransactionOldValue($object, $xaction);
$new = $xaction->getNewValue();
$key_map[$xaction_key][] = array(
'xaction' => $xaction,
'old' => $old,
'new' => $new,
);
}
foreach ($object->validateTransactions($key_map) as $error) {
$errors[] = $error;
}
break;
}
return $errors;
}
} }

View file

@ -5,6 +5,8 @@ final class PhabricatorApplicationProfileMenuItem
const MENUITEMKEY = 'application'; const MENUITEMKEY = 'application';
const FIELD_APPLICATION = 'application';
public function getMenuItemTypeIcon() { public function getMenuItemTypeIcon() {
return 'fa-globe'; return 'fa-globe';
} }
@ -32,10 +34,11 @@ final class PhabricatorApplicationProfileMenuItem
PhabricatorProfileMenuItemConfiguration $config) { PhabricatorProfileMenuItemConfiguration $config) {
return array( return array(
id(new PhabricatorDatasourceEditField()) id(new PhabricatorDatasourceEditField())
->setKey('application') ->setKey(self::FIELD_APPLICATION)
->setLabel(pht('Application')) ->setLabel(pht('Application'))
->setIsRequired(true) ->setIsRequired(true)
->setDatasource(new PhabricatorApplicationDatasource()) ->setDatasource(new PhabricatorApplicationDatasource())
->setIsRequired(true)
->setSingleValue($config->getMenuItemProperty('application')), ->setSingleValue($config->getMenuItemProperty('application')),
); );
} }
@ -44,6 +47,7 @@ final class PhabricatorApplicationProfileMenuItem
PhabricatorProfileMenuItemConfiguration $config) { PhabricatorProfileMenuItemConfiguration $config) {
$viewer = $this->getViewer(); $viewer = $this->getViewer();
$phid = $config->getMenuItemProperty('application'); $phid = $config->getMenuItemProperty('application');
$app = id(new PhabricatorApplicationQuery()) $app = id(new PhabricatorApplicationQuery())
->setViewer($viewer) ->setViewer($viewer)
->withPHIDs(array($phid)) ->withPHIDs(array($phid))
@ -77,4 +81,49 @@ final class PhabricatorApplicationProfileMenuItem
); );
} }
public function validateTransactions(
PhabricatorProfileMenuItemConfiguration $config,
$field_key,
$value,
array $xactions) {
$viewer = $this->getViewer();
$errors = array();
if ($field_key == self::FIELD_APPLICATION) {
if ($this->isEmptyTransaction($value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('You must choose an application.'),
$field_key);
}
foreach ($xactions as $xaction) {
$new = $xaction['new'];
if (!$new) {
continue;
}
if ($new === $value) {
continue;
}
$applications = id(new PhabricatorApplicationQuery())
->setViewer($viewer)
->withPHIDs(array($new))
->execute();
if (!$applications) {
$errors[] = $this->newInvalidError(
pht(
'Application "%s" is not a valid application which you have '.
'permission to see.',
$new),
$xaction['xaction']);
}
}
}
return $errors;
}
} }

View file

@ -5,6 +5,8 @@ final class PhabricatorDashboardProfileMenuItem
const MENUITEMKEY = 'dashboard'; const MENUITEMKEY = 'dashboard';
const FIELD_DASHBOARD = 'dashboardPHID';
private $dashboard; private $dashboard;
public function getMenuItemTypeIcon() { public function getMenuItemTypeIcon() {
@ -75,7 +77,7 @@ final class PhabricatorDashboardProfileMenuItem
->setLabel(pht('Name')) ->setLabel(pht('Name'))
->setValue($this->getName($config)), ->setValue($this->getName($config)),
id(new PhabricatorDatasourceEditField()) id(new PhabricatorDatasourceEditField())
->setKey('dashboardPHID') ->setKey(self::FIELD_DASHBOARD)
->setLabel(pht('Dashboard')) ->setLabel(pht('Dashboard'))
->setIsRequired(true) ->setIsRequired(true)
->setDatasource(new PhabricatorDashboardDatasource()) ->setDatasource(new PhabricatorDashboardDatasource())
@ -110,4 +112,49 @@ final class PhabricatorDashboardProfileMenuItem
); );
} }
public function validateTransactions(
PhabricatorProfileMenuItemConfiguration $config,
$field_key,
$value,
array $xactions) {
$viewer = $this->getViewer();
$errors = array();
if ($field_key == self::FIELD_DASHBOARD) {
if ($this->isEmptyTransaction($value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('You must choose a dashboard.'),
$field_key);
}
foreach ($xactions as $xaction) {
$new = $xaction['new'];
if (!$new) {
continue;
}
if ($new === $value) {
continue;
}
$dashboards = id(new PhabricatorDashboardQuery())
->setViewer($viewer)
->withPHIDs(array($new))
->execute();
if (!$dashboards) {
$errors[] = $this->newInvalidError(
pht(
'Dashboard "%s" is not a valid dashboard which you have '.
'permission to see.',
$new),
$xaction['xaction']);
}
}
}
return $errors;
}
} }

View file

@ -5,6 +5,8 @@ final class PhabricatorEditEngineProfileMenuItem
const MENUITEMKEY = 'editengine'; const MENUITEMKEY = 'editengine';
const FIELD_FORM = 'formKey';
private $form; private $form;
public function getMenuItemTypeIcon() { public function getMenuItemTypeIcon() {
@ -51,7 +53,8 @@ final class PhabricatorEditEngineProfileMenuItem
foreach ($items as $item) { foreach ($items as $item) {
$key = $item->getMenuItemProperty('formKey'); $key = $item->getMenuItemProperty('formKey');
list($engine_key, $form_key) = explode('/', $key); list($engine_key, $form_key) = PhabricatorEditEngine::splitFullKey($key);
if (is_numeric($form_key)) { if (is_numeric($form_key)) {
$form = idx($form_ids, $form_key, null); $form = idx($form_ids, $form_key, null);
$item->getMenuItem()->attachForm($form); $item->getMenuItem()->attachForm($form);
@ -83,7 +86,7 @@ final class PhabricatorEditEngineProfileMenuItem
->setLabel(pht('Name')) ->setLabel(pht('Name'))
->setValue($this->getName($config)), ->setValue($this->getName($config)),
id(new PhabricatorDatasourceEditField()) id(new PhabricatorDatasourceEditField())
->setKey('formKey') ->setKey(self::FIELD_FORM)
->setLabel(pht('Form')) ->setLabel(pht('Form'))
->setIsRequired(true) ->setIsRequired(true)
->setDatasource(new PhabricatorEditEngineDatasource()) ->setDatasource(new PhabricatorEditEngineDatasource())
@ -120,4 +123,53 @@ final class PhabricatorEditEngineProfileMenuItem
); );
} }
public function validateTransactions(
PhabricatorProfileMenuItemConfiguration $config,
$field_key,
$value,
array $xactions) {
$viewer = $this->getViewer();
$errors = array();
if ($field_key == self::FIELD_FORM) {
if ($this->isEmptyTransaction($value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('You must choose a form.'),
$field_key);
}
foreach ($xactions as $xaction) {
$new = $xaction['new'];
if (!$new) {
continue;
}
if ($new === $value) {
continue;
}
list($engine_key, $form_key) = PhabricatorEditEngine::splitFullKey(
$new);
$forms = id(new PhabricatorEditEngineConfigurationQuery())
->setViewer($viewer)
->withEngineKeys(array($engine_key))
->withIdentifiers(array($form_key))
->execute();
if (!$forms) {
$errors[] = $this->newInvalidError(
pht(
'Form "%s" is not a valid form which you have permission to '.
'see.',
$new),
$xaction['xaction']);
}
}
}
return $errors;
}
} }

View file

@ -5,6 +5,9 @@ final class PhabricatorLinkProfileMenuItem
const MENUITEMKEY = 'link'; const MENUITEMKEY = 'link';
const FIELD_URI = 'uri';
const FIELD_NAME = 'name';
public function getMenuItemTypeIcon() { public function getMenuItemTypeIcon() {
return 'fa-link'; return 'fa-link';
} }
@ -26,12 +29,12 @@ final class PhabricatorLinkProfileMenuItem
PhabricatorProfileMenuItemConfiguration $config) { PhabricatorProfileMenuItemConfiguration $config) {
return array( return array(
id(new PhabricatorTextEditField()) id(new PhabricatorTextEditField())
->setKey('name') ->setKey(self::FIELD_NAME)
->setLabel(pht('Name')) ->setLabel(pht('Name'))
->setIsRequired(true) ->setIsRequired(true)
->setValue($this->getLinkName($config)), ->setValue($this->getLinkName($config)),
id(new PhabricatorTextEditField()) id(new PhabricatorTextEditField())
->setKey('uri') ->setKey(self::FIELD_URI)
->setLabel(pht('URI')) ->setLabel(pht('URI'))
->setIsRequired(true) ->setIsRequired(true)
->setValue($this->getLinkURI($config)), ->setValue($this->getLinkURI($config)),
@ -91,4 +94,53 @@ final class PhabricatorLinkProfileMenuItem
); );
} }
public function validateTransactions(
PhabricatorProfileMenuItemConfiguration $config,
$field_key,
$value,
array $xactions) {
$viewer = $this->getViewer();
$errors = array();
if ($field_key == self::FIELD_NAME) {
if ($this->isEmptyTransaction($value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('You must choose a link name.'),
$field_key);
}
}
if ($field_key == self::FIELD_URI) {
if ($this->isEmptyTransaction($value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('You must choose a URI to link to.'),
$field_key);
}
foreach ($xactions as $xaction) {
$new = $xaction['new'];
if (!$new) {
continue;
}
if ($new === $value) {
continue;
}
if (!$this->isValidLinkURI($new)) {
$errors[] = $this->newInvalidError(
pht(
'URI "%s" is not a valid link URI. It should be a full, valid '.
'URI beginning with a protocol like "%s".',
$new,
'https://'),
$xaction['xaction']);
}
}
}
return $errors;
}
} }

View file

@ -70,4 +70,41 @@ abstract class PhabricatorProfileMenuItem extends Phobject {
return new PHUIListItemView(); return new PHUIListItemView();
} }
public function valdateTransactions(
PhabricatorProfileMenuItemConfiguration $config,
$field_key,
$value,
array $xactions) {
return array();
}
final protected function isEmptyTransaction($value, array $xactions) {
$result = $value;
foreach ($xactions as $xaction) {
$result = $xaction['new'];
}
return !strlen($result);
}
final protected function newError($title, $message, $xaction = null) {
return new PhabricatorApplicationTransactionValidationError(
PhabricatorProfileMenuItemConfigurationTransaction::TYPE_PROPERTY,
$title,
$message,
$xaction);
}
final protected function newRequiredError($message, $type) {
$xaction = id(new PhabricatorProfileMenuItemConfigurationTransaction())
->setMetadataValue('property.key', $type);
return $this->newError(pht('Required'), $message, $xaction)
->setIsMissingFieldError(true);
}
final protected function newInvalidError($message, $xaction = null) {
return $this->newError(pht('Invalid'), $message, $xaction);
}
} }

View file

@ -4,6 +4,7 @@ final class PhabricatorProjectProfileMenuItem
extends PhabricatorProfileMenuItem { extends PhabricatorProfileMenuItem {
const MENUITEMKEY = 'project'; const MENUITEMKEY = 'project';
const FIELD_PROJECT = 'project';
private $project; private $project;
@ -76,7 +77,7 @@ final class PhabricatorProjectProfileMenuItem
->setLabel(pht('Name')) ->setLabel(pht('Name'))
->setValue($this->getName($config)), ->setValue($this->getName($config)),
id(new PhabricatorDatasourceEditField()) id(new PhabricatorDatasourceEditField())
->setKey('project') ->setKey(self::FIELD_PROJECT)
->setLabel(pht('Project')) ->setLabel(pht('Project'))
->setIsRequired(true) ->setIsRequired(true)
->setDatasource(new PhabricatorProjectDatasource()) ->setDatasource(new PhabricatorProjectDatasource())
@ -111,4 +112,49 @@ final class PhabricatorProjectProfileMenuItem
); );
} }
public function validateTransactions(
PhabricatorProfileMenuItemConfiguration $config,
$field_key,
$value,
array $xactions) {
$viewer = $this->getViewer();
$errors = array();
if ($field_key == self::FIELD_PROJECT) {
if ($this->isEmptyTransaction($value, $xactions)) {
$errors[] = $this->newRequiredError(
pht('You must choose a project.'),
$field_key);
}
foreach ($xactions as $xaction) {
$new = $xaction['new'];
if (!$new) {
continue;
}
if ($new === $value) {
continue;
}
$projects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withPHIDs(array($new))
->execute();
if (!$projects) {
$errors[] = $this->newInvalidError(
pht(
'Project "%s" is not a valid project which you have '.
'permission to see.',
$new),
$xaction['xaction']);
}
}
}
return $errors;
}
} }

View file

@ -126,6 +126,30 @@ final class PhabricatorProfileMenuItemConfiguration
return $this->getMenuItem()->willBuildNavigationItems($items); return $this->getMenuItem()->willBuildNavigationItems($items);
} }
public function validateTransactions(array $map) {
$item = $this->getMenuItem();
$fields = $item->buildEditEngineFields($this);
$errors = array();
foreach ($fields as $field) {
$field_key = $field->getKey();
$xactions = idx($map, $field_key, array());
$value = $this->getMenuItemProperty($field_key);
$field_errors = $item->validateTransactions(
$this,
$field_key,
$value,
$xactions);
foreach ($field_errors as $error) {
$errors[] = $error;
}
}
return $errors;
}
public function getSortVector() { public function getSortVector() {
// Sort custom items above global items. // Sort custom items above global items.
if ($this->getCustomPHID()) { if ($this->getCustomPHID()) {

View file

@ -95,6 +95,10 @@ abstract class PhabricatorEditEngine
return $keys; return $keys;
} }
public static function splitFullKey($full_key) {
return explode('/', $full_key, 2);
}
public function getQuickCreateOrderVector() { public function getQuickCreateOrderVector() {
return id(new PhutilSortVector()) return id(new PhutilSortVector())
->addString($this->getObjectCreateShortText()); ->addString($this->getObjectCreateShortText());