diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 317241b701..bf08517a9e 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -34,6 +34,7 @@ return array( 'rsrc/css/aphront/transaction.css' => '5d0cae25', 'rsrc/css/aphront/two-column.css' => '16ab3ad2', 'rsrc/css/aphront/typeahead.css' => 'a989b5b3', + 'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af', 'rsrc/css/application/auth/auth.css' => '1e655982', 'rsrc/css/application/base/main-menu-view.css' => '33e5f2f6', 'rsrc/css/application/base/notification-menu.css' => '6aa0a74b', @@ -497,6 +498,7 @@ return array( 'rsrc/swf/aphlict.swf' => 'f19daffb', ), 'symbols' => array( + 'almanac-css' => 'dbb9b3af', 'aphront-bars' => '231ac33c', 'aphront-contextbar-view-css' => '1c3b0529', 'aphront-dark-console-css' => '6378ef3d', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index bf5b3dfd2e..21697c274b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -67,6 +67,7 @@ phutil_register_library_map(array( 'AlmanacNetworkViewController' => 'applications/almanac/controller/AlmanacNetworkViewController.php', 'AlmanacProperty' => 'applications/almanac/storage/AlmanacProperty.php', 'AlmanacPropertyController' => 'applications/almanac/controller/AlmanacPropertyController.php', + 'AlmanacPropertyDeleteController' => 'applications/almanac/controller/AlmanacPropertyDeleteController.php', 'AlmanacPropertyEditController' => 'applications/almanac/controller/AlmanacPropertyEditController.php', 'AlmanacPropertyInterface' => 'applications/almanac/property/AlmanacPropertyInterface.php', 'AlmanacPropertyQuery' => 'applications/almanac/query/AlmanacPropertyQuery.php', @@ -3092,6 +3093,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', ), 'AlmanacPropertyController' => 'AlmanacController', + 'AlmanacPropertyDeleteController' => 'AlmanacDeviceController', 'AlmanacPropertyEditController' => 'AlmanacDeviceController', 'AlmanacPropertyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'AlmanacQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', diff --git a/src/applications/almanac/application/PhabricatorAlmanacApplication.php b/src/applications/almanac/application/PhabricatorAlmanacApplication.php index 2936b2323a..c8cfe8e929 100644 --- a/src/applications/almanac/application/PhabricatorAlmanacApplication.php +++ b/src/applications/almanac/application/PhabricatorAlmanacApplication.php @@ -57,7 +57,8 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { '(?P\d+)/' => 'AlmanacNetworkViewController', ), 'property/' => array( - 'edit/(?:(?P\d+)/)?' => 'AlmanacPropertyEditController', + 'edit/' => 'AlmanacPropertyEditController', + 'delete/' => 'AlmanacPropertyDeleteController', ), ), ); diff --git a/src/applications/almanac/controller/AlmanacController.php b/src/applications/almanac/controller/AlmanacController.php index 1c2d62ef59..8537556c19 100644 --- a/src/applications/almanac/controller/AlmanacController.php +++ b/src/applications/almanac/controller/AlmanacController.php @@ -9,13 +9,129 @@ abstract class AlmanacController $viewer = $this->getViewer(); $properties = $object->getAlmanacProperties(); + $this->requireResource('almanac-css'); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $object, + PhabricatorPolicyCapability::CAN_EDIT); + + $field_list = PhabricatorCustomField::getObjectFields( + $object, + PhabricatorCustomField::ROLE_DEFAULT); + + // Before reading values from the object, read defaults. + $defaults = mpull( + $field_list->getFields(), + 'getValueForStorage', + 'getFieldKey'); + + $field_list + ->setViewer($viewer) + ->readFieldsFromStorage($object); + + Javelin::initBehavior('phabricator-tooltips', array()); + + $icon_builtin = id(new PHUIIconView()) + ->setIconFont('fa-circle') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => pht('Builtin Property'), + 'align' => 'E', + )); + + $icon_custom = id(new PHUIIconView()) + ->setIconFont('fa-circle-o grey') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => pht('Custom Property'), + 'align' => 'E', + )); + + $builtins = $object->getAlmanacPropertyFieldSpecifications(); + + // Sort fields so builtin fields appear first, then fields are ordered + // alphabetically. + $fields = $field_list->getFields(); + $fields = msort($fields, 'getFieldKey'); + + $head = array(); + $tail = array(); + foreach ($fields as $field) { + $key = $field->getFieldKey(); + if (isset($builtins[$key])) { + $head[$key] = $field; + } else { + $tail[$key] = $field; + } + } + + $fields = $head + $tail; + $rows = array(); - foreach ($properties as $property) { - $value = $property->getFieldValue(); + foreach ($fields as $key => $field) { + $value = $field->getValueForStorage(); + + $is_builtin = isset($builtins[$key]); + + $delete_uri = $this->getApplicationURI('property/delete/'); + $delete_uri = id(new PhutilURI($delete_uri)) + ->setQueryParams( + array( + 'objectPHID' => $object->getPHID(), + 'key' => $key, + )); + + $edit_uri = $this->getApplicationURI('property/edit/'); + $edit_uri = id(new PhutilURI($edit_uri)) + ->setQueryParams( + array( + 'objectPHID' => $object->getPHID(), + 'key' => $key, + )); + + $delete = javelin_tag( + 'a', + array( + 'class' => ($can_edit + ? 'button grey small' + : 'button grey small disabled'), + 'sigil' => 'workflow', + 'href' => $delete_uri, + ), + $is_builtin ? pht('Reset') : pht('Delete')); + + $default = idx($defaults, $key); + $is_default = ($default !== null && $default === $value); + + $display_value = PhabricatorConfigJSON::prettyPrintJSON($value); + if ($is_default) { + $display_value = phutil_tag( + 'span', + array( + 'class' => 'almanac-default-property-value', + ), + $display_value); + } + + $display_key = $key; + if ($can_edit) { + $display_key = javelin_tag( + 'a', + array( + 'href' => $edit_uri, + 'sigil' => 'workflow', + ), + $display_key); + } $rows[] = array( - $property->getFieldName(), - PhabricatorConfigJSON::prettyPrintJSON($value), + ($is_builtin ? $icon_builtin : $icon_custom), + $display_key, + $display_value, + $delete, ); } @@ -23,13 +139,17 @@ abstract class AlmanacController ->setNoDataString(pht('No properties.')) ->setHeaders( array( + null, pht('Name'), pht('Value'), + null, )) ->setColumnClasses( array( + null, null, 'wide', + 'action', )); $phid = $object->getPHID(); diff --git a/src/applications/almanac/controller/AlmanacNetworkViewController.php b/src/applications/almanac/controller/AlmanacNetworkViewController.php index 49000b0dc2..294039dae7 100644 --- a/src/applications/almanac/controller/AlmanacNetworkViewController.php +++ b/src/applications/almanac/controller/AlmanacNetworkViewController.php @@ -11,7 +11,6 @@ final class AlmanacNetworkViewController $viewer = $request->getViewer(); $id = $request->getURIData('id'); - $network = id(new AlmanacNetworkQuery()) ->setViewer($viewer) ->withIDs(array($id)) diff --git a/src/applications/almanac/controller/AlmanacPropertyDeleteController.php b/src/applications/almanac/controller/AlmanacPropertyDeleteController.php new file mode 100644 index 0000000000..0b328affcd --- /dev/null +++ b/src/applications/almanac/controller/AlmanacPropertyDeleteController.php @@ -0,0 +1,109 @@ +getViewer(); + + $object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($request->getStr('objectPHID'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$object) { + return new Aphront404Response(); + } + + if (!($object instanceof AlmanacPropertyInterface)) { + return new Aphront404Response(); + } + + $key = $request->getStr('key'); + if (!strlen($key)) { + return new Aphront404Response(); + } + + $cancel_uri = $object->getURI(); + + $builtins = $object->getAlmanacPropertyFieldSpecifications(); + $is_builtin = isset($builtins[$key]); + + if ($is_builtin) { + // This is a builtin property, so we're going to reset it to the + // default value. + $field_list = PhabricatorCustomField::getObjectFields( + $object, + PhabricatorCustomField::ROLE_DEFAULT); + + // Note that we're NOT loading field values from the object: we just want + // to get the field's default value so we can reset it. + + $fields = $field_list->getFields(); + $field = $fields[$key]; + + $is_delete = false; + $new_value = $field->getValueForStorage(); + + // Now, load the field to get the old value. + + $field_list + ->setViewer($viewer) + ->readFieldsFromStorage($object); + + $old_value = $field->getValueForStorage(); + + $title = pht('Reset Property'); + $body = pht('Reset this property to its default value?'); + $submit_text = pht('Reset'); + } else { + // This is a custom property, so we're going to delete it outright. + $is_delete = true; + $old_value = $object->getAlmanacPropertyValue($key); + $new_value = null; + + $title = pht('Delete Property'); + $body = pht('Delete this property? TODO: DOES NOT WORK YET'); + $submit_text = pht('Delete'); + } + + $validation_exception = null; + if ($request->isFormPost()) { + $xaction = $object->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorTransactions::TYPE_CUSTOMFIELD) + ->setMetadataValue('customfield:key', $key) + ->setOldValue($old_value) + ->setNewValue($new_value); + + // TODO: We aren't really deleting properties that we claim to delete + // yet, but that needs to be specialized a little bit. + + $editor = $object->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + try { + $editor->applyTransactions($object, array($xaction)); + return id(new AphrontRedirectResponse())->setURI($cancel_uri); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + } + } + + return $this->newDialog() + ->setTitle($title) + ->setValidationException($validation_exception) + ->addHiddenInput('objectPHID', $object->getPHID()) + ->addHiddenInput('key', $key) + ->appendParagraph($body) + ->addCancelButton($cancel_uri) + ->addSubmitButton($submit_text); + } + +} diff --git a/src/applications/almanac/controller/AlmanacPropertyEditController.php b/src/applications/almanac/controller/AlmanacPropertyEditController.php index 0a4e443ce3..9a881bc046 100644 --- a/src/applications/almanac/controller/AlmanacPropertyEditController.php +++ b/src/applications/almanac/controller/AlmanacPropertyEditController.php @@ -6,43 +6,17 @@ final class AlmanacPropertyEditController public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); - $id = $request->getURIData('id'); - if ($id) { - $property = id(new AlmanacPropertyQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$property) { - return new Aphront404Response(); - } - - $object = $property->getObject(); - - $is_new = false; - $title = pht('Edit Property'); - $save_button = pht('Save Changes'); - } else { - $object = id(new PhabricatorObjectQuery()) - ->setViewer($viewer) - ->withPHIDs(array($request->getStr('objectPHID'))) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$object) { - return new Aphront404Response(); - } - - $is_new = true; - $title = pht('Add Property'); - $save_button = pht('Add Property'); + $object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($request->getStr('objectPHID'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$object) { + return new Aphront404Response(); } if (!($object instanceof AlmanacPropertyInterface)) { @@ -51,6 +25,21 @@ final class AlmanacPropertyEditController $cancel_uri = $object->getURI(); + $key = $request->getStr('key'); + if ($key) { + $property_key = $key; + + $is_new = false; + $title = pht('Edit Property'); + $save_button = pht('Save Changes'); + } else { + $property_key = null; + + $is_new = true; + $title = pht('Add Property'); + $save_button = pht('Add Property'); + } + if ($is_new) { $errors = array(); $property = null; @@ -77,25 +66,11 @@ final class AlmanacPropertyEditController } if (!$errors) { - $property = id(new AlmanacPropertyQuery()) - ->setViewer($viewer) - ->withObjectPHIDs(array($object->getPHID())) - ->withNames(array($name)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$property) { - $property = id(new AlmanacProperty()) - ->setObjectPHID($object->getPHID()) - ->setFieldName($name); - } + $property_key = $name; } } - if (!$property) { + if ($property_key === null) { $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( @@ -115,17 +90,29 @@ final class AlmanacPropertyEditController } } - $v_name = $property->getFieldName(); - $e_name = true; + // Make sure property key is appropriate. + // TODO: It would be cleaner to put this safety check in the Editor. + AlmanacNames::validateServiceOrDeviceName($property_key); - $v_value = $property->getFieldValue(); - $e_value = null; + // If we're adding a new property, put a placeholder on the object so + // that we can build a CustomField for it. + if (!$object->hasAlmanacProperty($property_key)) { + $temporary_property = id(new AlmanacProperty()) + ->setObjectPHID($object->getPHID()) + ->setFieldName($property_key); - $object->attachAlmanacProperties(array($property)); + $object->attachAlmanacProperties(array($temporary_property)); + } $field_list = PhabricatorCustomField::getObjectFields( $object, - PhabricatorCustomField::ROLE_EDIT); + PhabricatorCustomField::ROLE_DEFAULT); + + // Select only the field being edited. + $fields = $field_list->getFields(); + $fields = array_select_keys($fields, array($property_key)); + $field_list = new PhabricatorCustomFieldList($fields); + $field_list ->setViewer($viewer) ->readFieldsFromStorage($object); @@ -153,7 +140,8 @@ final class AlmanacPropertyEditController $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('objectPHID', $request->getStr('objectPHID')) - ->addHiddenInput('name', $request->getStr('name')) + ->addHiddenInput('key', $request->getStr('key')) + ->addHiddenInput('name', $property_key) ->addHiddenInput('isValueEdit', true); $field_list->appendFieldsToForm($form); diff --git a/src/applications/almanac/customfield/AlmanacCoreCustomField.php b/src/applications/almanac/customfield/AlmanacCoreCustomField.php index 6e3fb6b6ff..c1df52321f 100644 --- a/src/applications/almanac/customfield/AlmanacCoreCustomField.php +++ b/src/applications/almanac/customfield/AlmanacCoreCustomField.php @@ -8,17 +8,29 @@ final class AlmanacCoreCustomField return 'almanac:core'; } - public function createFields($object) { - $specs = array(); + public function getFieldKey() { + return $this->getProxy()->getRawStandardFieldKey(); + } + public function getFieldName() { + return $this->getFieldKey(); + } + + public function createFields($object) { + + $specs = $object->getAlmanacPropertyFieldSpecifications(); + + $default_specs = array(); foreach ($object->getAlmanacProperties() as $property) { - $specs[$property->getFieldName()] = array( + $default_specs[$property->getFieldName()] = array( 'name' => $property->getFieldName(), 'type' => 'text', ); } - return PhabricatorStandardCustomField::buildStandardFields($this, $specs); + return PhabricatorStandardCustomField::buildStandardFields( + $this, + $specs + $default_specs); } public function shouldUseStorage() { @@ -26,8 +38,11 @@ final class AlmanacCoreCustomField } public function readValueFromObject(PhabricatorCustomFieldInterface $object) { - $key = $this->getProxy()->getRawStandardFieldKey(); - $this->setValueFromStorage($object->getAlmanacPropertyValue($key)); + $key = $this->getFieldKey(); + + if ($object->hasAlmanacProperty($key)) { + $this->setValueFromStorage($object->getAlmanacPropertyValue($key)); + } } public function applyApplicationTransactionInternalEffects( @@ -40,7 +55,7 @@ final class AlmanacCoreCustomField $object = $this->getObject(); $phid = $object->getPHID(); - $key = $this->getProxy()->getRawStandardFieldKey(); + $key = $this->getFieldKey(); $property = id(new AlmanacPropertyQuery()) ->setViewer($this->getViewer()) diff --git a/src/applications/almanac/property/AlmanacPropertyInterface.php b/src/applications/almanac/property/AlmanacPropertyInterface.php index 89ec271af6..9a2bf11455 100644 --- a/src/applications/almanac/property/AlmanacPropertyInterface.php +++ b/src/applications/almanac/property/AlmanacPropertyInterface.php @@ -7,5 +7,6 @@ interface AlmanacPropertyInterface { public function hasAlmanacProperty($key); public function getAlmanacProperty($key); public function getAlmanacPropertyValue($key, $default = null); + public function getAlmanacPropertyFieldSpecifications(); } diff --git a/src/applications/almanac/query/AlmanacPropertyQuery.php b/src/applications/almanac/query/AlmanacPropertyQuery.php index 1c66b5b53f..470ea62618 100644 --- a/src/applications/almanac/query/AlmanacPropertyQuery.php +++ b/src/applications/almanac/query/AlmanacPropertyQuery.php @@ -14,7 +14,7 @@ final class AlmanacPropertyQuery } public function withObjectPHIDs(array $phids) { - $this->phids = $phids; + $this->objectPHIDs = $phids; return $this; } diff --git a/src/applications/almanac/query/AlmanacQuery.php b/src/applications/almanac/query/AlmanacQuery.php index 7c0ad75ef8..54274dfe72 100644 --- a/src/applications/almanac/query/AlmanacQuery.php +++ b/src/applications/almanac/query/AlmanacQuery.php @@ -12,7 +12,7 @@ abstract class AlmanacQuery $property_query = id(new AlmanacPropertyQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) - ->withObjectPHIDs(mpull($objects, null, 'getPHID')); + ->withObjectPHIDs(mpull($objects, 'getPHID')); // NOTE: We disable policy filtering and object attachment to avoid // a cyclic dependency where objects need their properties and properties @@ -21,6 +21,7 @@ abstract class AlmanacQuery $property_query->setDisablePolicyFilteringAndAttachment(true); $properties = $property_query->execute(); + $properties = mgroup($properties, 'getObjectPHID'); foreach ($objects as $object) { $object_properties = idx($properties, $object->getPHID(), array()); diff --git a/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php b/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php index fdc0482069..e4dc3c8afc 100644 --- a/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacClusterRepositoryServiceType.php @@ -16,4 +16,19 @@ final class AlmanacClusterRepositoryServiceType 'Defines a repository service for use in a Phabricator cluster.'); } + public function getFieldSpecifications() { + return array( + 'closed' => array( + 'type' => 'bool', + 'name' => pht('Closed'), + 'default' => false, + 'strings' => array( + 'edit.checkbox' => pht( + 'Prevent new repositories from being allocated on this '. + 'service.'), + ), + ), + ); + } + } diff --git a/src/applications/almanac/servicetype/AlmanacServiceType.php b/src/applications/almanac/servicetype/AlmanacServiceType.php index aa4f7275cc..4e8d9d02e8 100644 --- a/src/applications/almanac/servicetype/AlmanacServiceType.php +++ b/src/applications/almanac/servicetype/AlmanacServiceType.php @@ -47,6 +47,14 @@ abstract class AlmanacServiceType extends Phobject { } + public function getDefaultPropertyMap() { + return array(); + } + + public function getFieldSpecifications() { + return array(); + } + /** * List all available service type implementations. * diff --git a/src/applications/almanac/storage/AlmanacBinding.php b/src/applications/almanac/storage/AlmanacBinding.php index 0cca45e961..70afa094c7 100644 --- a/src/applications/almanac/storage/AlmanacBinding.php +++ b/src/applications/almanac/storage/AlmanacBinding.php @@ -119,6 +119,10 @@ final class AlmanacBinding } } + public function getAlmanacPropertyFieldSpecifications() { + return array(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index 692206f0dd..f0133b3504 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -97,6 +97,10 @@ final class AlmanacDevice } } + public function getAlmanacPropertyFieldSpecifications() { + return array(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php index 663f815e02..fc49fe7823 100644 --- a/src/applications/almanac/storage/AlmanacNetwork.php +++ b/src/applications/almanac/storage/AlmanacNetwork.php @@ -40,7 +40,7 @@ final class AlmanacNetwork } public function getURI() { - return '/almanac/network/view/'.$this->getName().'/'; + return '/almanac/network/'.$this->getID().'/'; } diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index ef9c9890b3..507f90c888 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -121,6 +121,10 @@ final class AlmanacService } } + public function getAlmanacPropertyFieldSpecifications() { + return $this->getServiceType()->getFieldSpecifications(); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php index a066dcfe34..8ac1ce715e 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php @@ -22,9 +22,13 @@ final class PhabricatorStandardCustomFieldBool return $this->newNumericIndex(0); } + public function readValueFromRequest(AphrontRequest $request) { + $this->setFieldValue((bool)$request->getBool($this->getFieldKey())); + } + public function getValueForStorage() { $value = $this->getFieldValue(); - if (strlen($value)) { + if ($value !== null) { return (int)$value; } else { return null; diff --git a/webroot/rsrc/css/application/almanac/almanac.css b/webroot/rsrc/css/application/almanac/almanac.css new file mode 100644 index 0000000000..9091aff761 --- /dev/null +++ b/webroot/rsrc/css/application/almanac/almanac.css @@ -0,0 +1,7 @@ +/** + * @provides almanac-css + */ + +.almanac-default-property-value { + color: {$lightgreytext}; +}