diff --git a/src/applications/search/editor/PhabricatorProfileMenuEditEngine.php b/src/applications/search/editor/PhabricatorProfileMenuEditEngine.php --- a/src/applications/search/editor/PhabricatorProfileMenuEditEngine.php +++ b/src/applications/search/editor/PhabricatorProfileMenuEditEngine.php @@ -149,4 +149,47 @@ 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); + } + } diff --git a/src/applications/search/editor/PhabricatorProfileMenuEditor.php b/src/applications/search/editor/PhabricatorProfileMenuEditor.php --- a/src/applications/search/editor/PhabricatorProfileMenuEditor.php +++ b/src/applications/search/editor/PhabricatorProfileMenuEditor.php @@ -87,4 +87,39 @@ 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; + } + + } diff --git a/src/applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php --- a/src/applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php @@ -5,6 +5,8 @@ const MENUITEMKEY = 'application'; + const FIELD_APPLICATION = 'application'; + public function getMenuItemTypeIcon() { return 'fa-globe'; } @@ -32,10 +34,11 @@ PhabricatorProfileMenuItemConfiguration $config) { return array( id(new PhabricatorDatasourceEditField()) - ->setKey('application') + ->setKey(self::FIELD_APPLICATION) ->setLabel(pht('Application')) ->setIsRequired(true) ->setDatasource(new PhabricatorApplicationDatasource()) + ->setIsRequired(true) ->setSingleValue($config->getMenuItemProperty('application')), ); } @@ -44,6 +47,7 @@ PhabricatorProfileMenuItemConfiguration $config) { $viewer = $this->getViewer(); $phid = $config->getMenuItemProperty('application'); + $app = id(new PhabricatorApplicationQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) @@ -77,4 +81,49 @@ ); } + 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; + } + } diff --git a/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php --- a/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php @@ -5,6 +5,8 @@ const MENUITEMKEY = 'dashboard'; + const FIELD_DASHBOARD = 'dashboardPHID'; + private $dashboard; public function getMenuItemTypeIcon() { @@ -75,7 +77,7 @@ ->setLabel(pht('Name')) ->setValue($this->getName($config)), id(new PhabricatorDatasourceEditField()) - ->setKey('dashboardPHID') + ->setKey(self::FIELD_DASHBOARD) ->setLabel(pht('Dashboard')) ->setIsRequired(true) ->setDatasource(new PhabricatorDashboardDatasource()) @@ -110,4 +112,49 @@ ); } + 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; + } + } diff --git a/src/applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php --- a/src/applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php @@ -5,6 +5,8 @@ const MENUITEMKEY = 'editengine'; + const FIELD_FORM = 'formKey'; + private $form; public function getMenuItemTypeIcon() { @@ -51,7 +53,8 @@ foreach ($items as $item) { $key = $item->getMenuItemProperty('formKey'); - list($engine_key, $form_key) = explode('/', $key); + list($engine_key, $form_key) = PhabricatorEditEngine::splitFullKey($key); + if (is_numeric($form_key)) { $form = idx($form_ids, $form_key, null); $item->getMenuItem()->attachForm($form); @@ -83,7 +86,7 @@ ->setLabel(pht('Name')) ->setValue($this->getName($config)), id(new PhabricatorDatasourceEditField()) - ->setKey('formKey') + ->setKey(self::FIELD_FORM) ->setLabel(pht('Form')) ->setIsRequired(true) ->setDatasource(new PhabricatorEditEngineDatasource()) @@ -120,4 +123,53 @@ ); } + 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; + } + } diff --git a/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php --- a/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php @@ -5,6 +5,9 @@ const MENUITEMKEY = 'link'; + const FIELD_URI = 'uri'; + const FIELD_NAME = 'name'; + public function getMenuItemTypeIcon() { return 'fa-link'; } @@ -26,12 +29,12 @@ PhabricatorProfileMenuItemConfiguration $config) { return array( id(new PhabricatorTextEditField()) - ->setKey('name') + ->setKey(self::FIELD_NAME) ->setLabel(pht('Name')) ->setIsRequired(true) ->setValue($this->getLinkName($config)), id(new PhabricatorTextEditField()) - ->setKey('uri') + ->setKey(self::FIELD_URI) ->setLabel(pht('URI')) ->setIsRequired(true) ->setValue($this->getLinkURI($config)), @@ -91,4 +94,53 @@ ); } + 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; + } } diff --git a/src/applications/search/menuitem/PhabricatorProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorProfileMenuItem.php --- a/src/applications/search/menuitem/PhabricatorProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorProfileMenuItem.php @@ -70,4 +70,41 @@ 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); + } + } diff --git a/src/applications/search/menuitem/PhabricatorProjectProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorProjectProfileMenuItem.php --- a/src/applications/search/menuitem/PhabricatorProjectProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorProjectProfileMenuItem.php @@ -4,6 +4,7 @@ extends PhabricatorProfileMenuItem { const MENUITEMKEY = 'project'; + const FIELD_PROJECT = 'project'; private $project; @@ -76,7 +77,7 @@ ->setLabel(pht('Name')) ->setValue($this->getName($config)), id(new PhabricatorDatasourceEditField()) - ->setKey('project') + ->setKey(self::FIELD_PROJECT) ->setLabel(pht('Project')) ->setIsRequired(true) ->setDatasource(new PhabricatorProjectDatasource()) @@ -111,4 +112,49 @@ ); } + 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; + } + } diff --git a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php --- a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php +++ b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php @@ -126,6 +126,30 @@ 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() { // Sort custom items above global items. if ($this->getCustomPHID()) { diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -95,6 +95,10 @@ return $keys; } + public static function splitFullKey($full_key) { + return explode('/', $full_key, 2); + } + public function getQuickCreateOrderVector() { return id(new PhutilSortVector()) ->addString($this->getObjectCreateShortText());