diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index 8b339c0032..484f7015c4 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -1,1117 +1,1130 @@ viewer = $viewer; return $this; } final public function getViewer() { return $this->viewer; } final public function setController(PhabricatorController $controller) { $this->controller = $controller; $this->setViewer($controller->getViewer()); return $this; } final public function getController() { return $this->controller; } final public function getEngineKey() { return $this->getPhobjectClassConstant('ENGINECONST', 64); } final public function getApplication() { $app_class = $this->getEngineApplicationClass(); return PhabricatorApplication::getByClass($app_class); } /* -( Managing Fields )---------------------------------------------------- */ abstract public function getEngineApplicationClass(); abstract protected function buildCustomEditFields($object); public function getFieldsForConfig( PhabricatorEditEngineConfiguration $config) { $object = $this->newEditableObject(); $this->editEngineConfiguration = $config; // This is mostly making sure that we fill in default values. $this->setIsCreate(true); return $this->buildEditFields($object); } final protected function buildEditFields($object) { $viewer = $this->getViewer(); $fields = $this->buildCustomEditFields($object); $extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions(); foreach ($extensions as $extension) { $extension->setViewer($viewer); if (!$extension->supportsObject($this, $object)) { continue; } $extension_fields = $extension->buildCustomEditFields($this, $object); // TODO: Validate this in more detail with a more tailored error. assert_instances_of($extension_fields, 'PhabricatorEditField'); foreach ($extension_fields as $field) { $fields[] = $field; } } $config = $this->getEditEngineConfiguration(); $fields = $config->applyConfigurationToFields($this, $fields); foreach ($fields as $field) { $field ->setViewer($viewer) ->setObject($object); } return $fields; } /* -( Display Text )------------------------------------------------------- */ /** * @task text */ abstract public function getEngineName(); /** * @task text */ abstract protected function getObjectCreateTitleText($object); /** * @task text */ protected function getFormHeaderText($object) { $config = $this->getEditEngineConfiguration(); return $config->getName(); } /** * @task text */ abstract protected function getObjectEditTitleText($object); /** * @task text */ abstract protected function getObjectCreateShortText(); /** * @task text */ abstract protected function getObjectEditShortText($object); /** * @task text */ protected function getObjectCreateButtonText($object) { return $this->getObjectCreateTitleText($object); } /** * @task text */ protected function getObjectEditButtonText($object) { return pht('Save Changes'); } /* -( Edit Engine Configuration )------------------------------------------ */ protected function supportsEditEngineConfiguration() { return true; } final protected function getEditEngineConfiguration() { return $this->editEngineConfiguration; } private function loadEditEngineConfiguration($key) { if ($key === null) { $key = self::EDITENGINECONFIG_DEFAULT; } $config = id(new PhabricatorEditEngineConfigurationQuery()) ->setViewer($this->getViewer()) ->withEngineKeys(array($this->getEngineKey())) ->withIdentifiers(array($key)) ->executeOne(); if (!$config) { return null; } $this->editEngineConfiguration = $config; return $config; } final public function getBuiltinEngineConfigurations() { $configurations = $this->newBuiltinEngineConfigurations(); if (!$configurations) { throw new Exception( pht( 'EditEngine ("%s") returned no builtin engine configurations, but '. 'an edit engine must have at least one configuration.', get_class($this))); } assert_instances_of($configurations, 'PhabricatorEditEngineConfiguration'); $has_default = false; foreach ($configurations as $config) { if ($config->getBuiltinKey() == self::EDITENGINECONFIG_DEFAULT) { $has_default = true; } } if (!$has_default) { $first = head($configurations); if (!$first->getBuiltinKey()) { $first ->setBuiltinKey(self::EDITENGINECONFIG_DEFAULT) ->setIsDefault(true); if (!strlen($first->getName())) { $first->setName($this->getObjectCreateShortText()); } } else { throw new Exception( pht( 'EditEngine ("%s") returned builtin engine configurations, '. 'but none are marked as default and the first configuration has '. 'a different builtin key already. Mark a builtin as default or '. 'omit the key from the first configuration', get_class($this))); } } $builtins = array(); foreach ($configurations as $key => $config) { $builtin_key = $config->getBuiltinKey(); if ($builtin_key === null) { throw new Exception( pht( 'EditEngine ("%s") returned builtin engine configurations, '. 'but one (with key "%s") is missing a builtin key. Provide a '. 'builtin key for each configuration (you can omit it from the '. 'first configuration in the list to automatically assign the '. 'default key).', get_class($this), $key)); } if (isset($builtins[$builtin_key])) { throw new Exception( pht( 'EditEngine ("%s") returned builtin engine configurations, '. 'but at least two specify the same builtin key ("%s"). Engines '. 'must have unique builtin keys.', get_class($this), $builtin_key)); } $builtins[$builtin_key] = $config; } return $builtins; } protected function newBuiltinEngineConfigurations() { return array( $this->newConfiguration(), ); } final protected function newConfiguration() { return PhabricatorEditEngineConfiguration::initializeNewConfiguration( $this->getViewer(), $this); } /* -( Managing URIs )------------------------------------------------------ */ /** * @task uri */ abstract protected function getObjectViewURI($object); /** * @task uri */ protected function getObjectCreateCancelURI($object) { return $this->getApplication()->getApplicationURI(); } /** * @task uri */ protected function getEditorURI() { return $this->getApplication()->getApplicationURI('edit/'); } /** * @task uri */ protected function getObjectEditCancelURI($object) { return $this->getObjectViewURI($object); } /** * @task uri */ public function getEditURI($object = null, $path = null) { $parts = array(); $parts[] = $this->getEditorURI(); if ($object && $object->getID()) { $parts[] = $object->getID().'/'; } if ($path !== null) { $parts[] = $path; } return implode('', $parts); } /* -( Creating and Loading Objects )--------------------------------------- */ /** * Initialize a new object for creation. * * @return object Newly initialized object. * @task load */ abstract protected function newEditableObject(); /** * Build an empty query for objects. * * @return PhabricatorPolicyAwareQuery Query. * @task load */ abstract protected function newObjectQuery(); /** * Test if this workflow is creating a new object or editing an existing one. * * @return bool True if a new object is being created. * @task load */ final public function getIsCreate() { return $this->isCreate; } /** * Flag this workflow as a create or edit. * * @param bool True if this is a create workflow. * @return this * @task load */ private function setIsCreate($is_create) { $this->isCreate = $is_create; return $this; } /** * Try to load an object by ID, PHID, or monogram. This is done primarily * to make Conduit a little easier to use. * * @param wild ID, PHID, or monogram. * @return object Corresponding editable object. * @task load */ private function newObjectFromIdentifier($identifier) { if (is_int($identifier) || ctype_digit($identifier)) { $object = $this->newObjectFromID($identifier); if (!$object) { throw new Exception( pht( 'No object exists with ID "%s".', $identifier)); } return $object; } $type_unknown = PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN; if (phid_get_type($identifier) != $type_unknown) { $object = $this->newObjectFromPHID($identifier); if (!$object) { throw new Exception( pht( 'No object exists with PHID "%s".', $identifier)); } return $object; } $target = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withNames(array($identifier)) ->executeOne(); if (!$target) { throw new Exception( pht( 'Monogram "%s" does not identify a valid object.', $identifier)); } $expect = $this->newEditableObject(); $expect_class = get_class($expect); $target_class = get_class($target); if ($expect_class !== $target_class) { throw new Exception( pht( 'Monogram "%s" identifies an object of the wrong type. Loaded '. 'object has class "%s", but this editor operates on objects of '. 'type "%s".', $identifier, $target_class, $expect_class)); } // Load the object by PHID using this engine's standard query. This makes // sure it's really valid, goes through standard policy check logic, and // picks up any `need...()` clauses we want it to load with. $object = $this->newObjectFromPHID($target->getPHID()); if (!$object) { throw new Exception( pht( 'Failed to reload object identified by monogram "%s" when '. 'querying by PHID.', $identifier)); } return $object; } /** * Load an object by ID. * * @param int Object ID. * @return object|null Object, or null if no such object exists. * @task load */ private function newObjectFromID($id) { $query = $this->newObjectQuery() ->withIDs(array($id)); return $this->newObjectFromQuery($query); } /** * Load an object by PHID. * * @param phid Object PHID. * @return object|null Object, or null if no such object exists. * @task load */ private function newObjectFromPHID($phid) { $query = $this->newObjectQuery() ->withPHIDs(array($phid)); return $this->newObjectFromQuery($query); } /** * Load an object given a configured query. * * @param PhabricatorPolicyAwareQuery Configured query. * @return object|null Object, or null if no such object exists. * @task load */ private function newObjectFromQuery(PhabricatorPolicyAwareQuery $query) { $viewer = $this->getViewer(); $object = $query ->setViewer($viewer) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$object) { return null; } return $object; } /** * Verify that an object is appropriate for editing. * * @param wild Loaded value. * @return void * @task load */ private function validateObject($object) { if (!$object || !is_object($object)) { throw new Exception( pht( 'EditEngine "%s" created or loaded an invalid object: object must '. 'actually be an object, but is of some other type ("%s").', get_class($this), gettype($object))); } if (!($object instanceof PhabricatorApplicationTransactionInterface)) { throw new Exception( pht( 'EditEngine "%s" created or loaded an invalid object: object (of '. 'class "%s") must implement "%s", but does not.', get_class($this), get_class($object), 'PhabricatorApplicationTransactionInterface')); } } /* -( Responding to Web Requests )----------------------------------------- */ final public function buildResponse() { $viewer = $this->getViewer(); $controller = $this->getController(); $request = $controller->getRequest(); $form_key = $request->getURIData('formKey'); $config = $this->loadEditEngineConfiguration($form_key); if (!$config) { return new Aphront404Response(); } $id = $request->getURIData('id'); if ($id) { $this->setIsCreate(false); $object = $this->newObjectFromID($id); if (!$object) { return new Aphront404Response(); } } else { $this->setIsCreate(true); $object = $this->newEditableObject(); } $this->validateObject($object); $action = $request->getURIData('editAction'); switch ($action) { case 'parameters': return $this->buildParametersResponse($object); case 'nodefault': return $this->buildNoDefaultResponse($object); default: return $this->buildEditResponse($object); } } private function buildCrumbs($object, $final = false) { $controller = $this->getcontroller(); $crumbs = $controller->buildApplicationCrumbsForEditEngine(); if ($this->getIsCreate()) { $create_text = $this->getObjectCreateShortText(); if ($final) { $crumbs->addTextCrumb($create_text); } else { $edit_uri = $this->getEditURI($object); $crumbs->addTextCrumb($create_text, $edit_uri); } } else { $crumbs->addTextCrumb( $this->getObjectEditShortText($object), $this->getObjectViewURI($object)); $edit_text = pht('Edit'); if ($final) { $crumbs->addTextCrumb($edit_text); } else { $edit_uri = $this->getEditURI($object); $crumbs->addTextCrumb($edit_text, $edit_uri); } } return $crumbs; } private function buildEditResponse($object) { $viewer = $this->getViewer(); $controller = $this->getController(); $request = $controller->getRequest(); $fields = $this->buildEditFields($object); $template = $object->getApplicationTransactionTemplate(); $validation_exception = null; if ($request->isFormPost()) { foreach ($fields as $field) { $field->setIsSubmittedForm(true); if ($field->getIsLocked() || $field->getIsHidden()) { continue; } $field->readValueFromSubmit($request); } $xactions = array(); foreach ($fields as $field) { - $xaction = $field->generateTransaction(clone $template); + $types = $field->getWebEditTypes(); + foreach ($types as $type) { + $type_xactions = $type->generateTransactions( + clone $template, + array( + 'value' => $field->getValueForTransaction(), + )); + + if (!$type_xactions) { + continue; + } - if (!$xaction) { - continue; + foreach ($type_xactions as $type_xaction) { + $xactions[] = $type_xaction; + } } - - $xactions[] = $xaction; } $editor = $object->getApplicationTransactionEditor() ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true); try { $editor->applyTransactions($object, $xactions); return id(new AphrontRedirectResponse()) ->setURI($this->getObjectViewURI($object)); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; foreach ($fields as $field) { $xaction_type = $field->getTransactionType(); if ($xaction_type === null) { continue; } $message = $ex->getShortMessage($xaction_type); if ($message === null) { continue; } $field->setControlError($message); } } } else { if ($this->getIsCreate()) { foreach ($fields as $field) { if ($field->getIsLocked() || $field->getIsHidden()) { continue; } $field->readValueFromRequest($request); } } else { foreach ($fields as $field) { $field->readValueFromObject($object); } } } $action_button = $this->buildEditFormActionButton($object); if ($this->getIsCreate()) { $header_text = $this->getFormHeaderText($object); } else { $header_text = $this->getObjectEditTitleText($object); } $header = id(new PHUIHeaderView()) ->setHeader($header_text) ->addActionLink($action_button); $crumbs = $this->buildCrumbs($object, $final = true); $form = $this->buildEditForm($object, $fields); $box = id(new PHUIObjectBoxView()) ->setUser($viewer) ->setHeader($header) ->setValidationException($validation_exception) ->appendChild($form); return $controller->newPage() ->setTitle($header_text) ->setCrumbs($crumbs) ->appendChild($box); } private function buildEditForm($object, array $fields) { $viewer = $this->getViewer(); $form = id(new AphrontFormView()) ->setUser($viewer); foreach ($fields as $field) { $field->appendToForm($form); } if ($this->getIsCreate()) { $cancel_uri = $this->getObjectCreateCancelURI($object); $submit_button = $this->getObjectCreateButtonText($object); } else { $cancel_uri = $this->getObjectEditCancelURI($object); $submit_button = $this->getObjectEditButtonText($object); } $form->appendControl( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_button)); return $form; } private function buildEditFormActionButton($object) { $viewer = $this->getViewer(); $action_view = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($this->buildEditFormActions($object) as $action) { $action_view->addAction($action); } $action_button = id(new PHUIButtonView()) ->setTag('a') ->setText(pht('Actions')) ->setHref('#') ->setIconFont('fa-bars') ->setDropdownMenu($action_view); return $action_button; } private function buildEditFormActions($object) { $actions = array(); if ($this->supportsEditEngineConfiguration()) { $engine_key = $this->getEngineKey(); $config = $this->getEditEngineConfiguration(); $actions[] = id(new PhabricatorActionView()) ->setName(pht('Manage Form Configurations')) ->setIcon('fa-list-ul') ->setHref("/transactions/editengine/{$engine_key}/"); $actions[] = id(new PhabricatorActionView()) ->setName(pht('Edit Form Configuration')) ->setIcon('fa-pencil') ->setHref($config->getURI()); } $actions[] = id(new PhabricatorActionView()) ->setName(pht('Show HTTP Parameters')) ->setIcon('fa-crosshairs') ->setHref($this->getEditURI($object, 'parameters/')); return $actions; } final public function addActionToCrumbs(PHUICrumbsView $crumbs) { $viewer = $this->getViewer(); $configs = id(new PhabricatorEditEngineConfigurationQuery()) ->setViewer($viewer) ->withEngineKeys(array($this->getEngineKey())) ->withIsDefault(true) ->withIsDisabled(false) ->execute(); $dropdown = null; $disabled = false; $workflow = false; $menu_icon = 'fa-plus-square'; if (!$configs) { if ($viewer->isLoggedIn()) { $disabled = true; } else { // If the viewer isn't logged in, assume they'll get hit with a login // dialog and are likely able to create objects after they log in. $disabled = false; } $workflow = true; $create_uri = $this->getEditURI(null, 'nodefault/'); } else { $config = head($configs); $form_key = $config->getIdentifier(); $create_uri = $this->getEditURI(null, "form/{$form_key}/"); if (count($configs) > 1) { $configs = msort($configs, 'getDisplayName'); $menu_icon = 'fa-caret-square-o-down'; $dropdown = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($configs as $config) { $form_key = $config->getIdentifier(); $config_uri = $this->getEditURI(null, "form/{$form_key}/"); $item_icon = 'fa-plus'; $dropdown->addAction( id(new PhabricatorActionView()) ->setName($config->getDisplayName()) ->setIcon($item_icon) ->setHref($config_uri)); } } } $action = id(new PHUIListItemView()) ->setName($this->getObjectCreateShortText()) ->setHref($create_uri) ->setIcon($menu_icon) ->setWorkflow($workflow) ->setDisabled($disabled); if ($dropdown) { $action->setDropdownMenu($dropdown); } $crumbs->addAction($action); } /* -( Responding to HTTP Parameter Requests )------------------------------ */ /** * Respond to a request for documentation on HTTP parameters. * * @param object Editable object. * @return AphrontResponse Response object. * @task http */ private function buildParametersResponse($object) { $controller = $this->getController(); $viewer = $this->getViewer(); $request = $controller->getRequest(); $fields = $this->buildEditFields($object); $crumbs = $this->buildCrumbs($object); $crumbs->addTextCrumb(pht('HTTP Parameters')); $crumbs->setBorder(true); $header_text = pht( 'HTTP Parameters: %s', $this->getObjectCreateShortText()); $header = id(new PHUIHeaderView()) ->setHeader($header_text); $help_view = id(new PhabricatorApplicationEditHTTPParameterHelpView()) ->setUser($viewer) ->setFields($fields); $document = id(new PHUIDocumentViewPro()) ->setUser($viewer) ->setHeader($header) ->appendChild($help_view); return $controller->newPage() ->setTitle(pht('HTTP Parameters')) ->setCrumbs($crumbs) ->appendChild($document); } private function buildNoDefaultResponse($object) { $cancel_uri = $this->getObjectCreateCancelURI($object); return $this->getController() ->newDialog() ->setTitle(pht('No Default Create Forms')) ->appendParagraph( pht( 'This application is not configured with any visible, enabled '. 'forms for creating objects.')) ->addCancelButton($cancel_uri); } /* -( Conduit )------------------------------------------------------------ */ /** * Respond to a Conduit edit request. * * This method accepts a list of transactions to apply to an object, and * either edits an existing object or creates a new one. * * @task conduit */ final public function buildConduitResponse(ConduitAPIRequest $request) { $viewer = $this->getViewer(); $config = $this->loadEditEngineConfiguration(null); if (!$config) { throw new Exception( pht( 'Unable to load configuration for this EditEngine ("%s").', get_class($this))); } $identifier = $request->getValue('objectIdentifier'); if ($identifier) { $this->setIsCreate(false); $object = $this->newObjectFromIdentifier($identifier); } else { $this->setIsCreate(true); $object = $this->newEditableObject(); } $this->validateObject($object); $fields = $this->buildEditFields($object); - $types = $this->getAllEditTypesFromFields($fields); + $types = $this->getConduitEditTypesFromFields($fields); $template = $object->getApplicationTransactionTemplate(); $xactions = $this->getConduitTransactions($request, $types, $template); $editor = $object->getApplicationTransactionEditor() ->setActor($viewer) ->setContentSourceFromConduitRequest($request) ->setContinueOnNoEffect(true); $xactions = $editor->applyTransactions($object, $xactions); $xactions_struct = array(); foreach ($xactions as $xaction) { $xactions_struct[] = array( 'phid' => $xaction->getPHID(), ); } return array( 'object' => array( 'id' => $object->getID(), 'phid' => $object->getPHID(), ), 'transactions' => $xactions_struct, ); } /** * Generate transactions which can be applied from edit actions in a Conduit * request. * * @param ConduitAPIRequest The request. * @param list Supported edit types. * @param PhabricatorApplicationTransaction Template transaction. * @return list Generated transactions. * @task conduit */ private function getConduitTransactions( ConduitAPIRequest $request, array $types, PhabricatorApplicationTransaction $template) { $transactions_key = 'transactions'; $xactions = $request->getValue($transactions_key); if (!is_array($xactions)) { throw new Exception( pht( 'Parameter "%s" is not a list of transactions.', $transactions_key)); } foreach ($xactions as $key => $xaction) { if (!is_array($xaction)) { throw new Exception( pht( 'Parameter "%s" must contain a list of transaction descriptions, '. 'but item with key "%s" is not a dictionary.', $transactions_key, $key)); } if (!array_key_exists('type', $xaction)) { throw new Exception( pht( 'Parameter "%s" must contain a list of transaction descriptions, '. 'but item with key "%s" is missing a "type" field. Each '. 'transaction must have a type field.', $transactions_key, $key)); } $type = $xaction['type']; if (empty($types[$type])) { throw new Exception( pht( 'Transaction with key "%s" has invalid type "%s". This type is '. 'not recognized. Valid types are: %s.', $key, $type, implode(', ', array_keys($types)))); } } $results = array(); foreach ($xactions as $xaction) { $type = $types[$xaction['type']]; - $results[] = $type->generateTransaction( + $type_xactions = $type->generateTransactions( clone $template, $xaction); + + foreach ($type_xactions as $type_xaction) { + $results[] = $type_xaction; + } } return $results; } /** * @return map * @task conduit */ - private function getAllEditTypesFromFields(array $fields) { + private function getConduitEditTypesFromFields(array $fields) { $types = array(); foreach ($fields as $field) { - $field_types = $field->getEditTransactionTypes(); + $field_types = $field->getConduitEditTypes(); if ($field_types === null) { continue; } foreach ($field_types as $field_type) { $field_type->setField($field); $types[$field_type->getEditType()] = $field_type; } } return $types; } - public function getAllEditTypes() { + public function getConduitEditTypes() { $config = $this->loadEditEngineConfiguration(null); if (!$config) { return array(); } $object = $this->newEditableObject(); $fields = $this->buildEditFields($object); - return $this->getAllEditTypesFromFields($fields); + return $this->getConduitEditTypesFromFields($fields); } final public static function getAllEditEngines() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getEngineKey') ->execute(); } final public static function getByKey(PhabricatorUser $viewer, $key) { return id(new PhabricatorEditEngineQuery()) ->setViewer($viewer) ->withEngineKeys(array($key)) ->executeOne(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getPHID() { return get_class($this); } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return PhabricatorPolicies::getMostOpenPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } } diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php b/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php index 8358352675..dff4d1b62f 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php @@ -1,198 +1,198 @@ newEditEngine(); $class = $engine->getEngineApplicationClass(); return PhabricatorApplication::getByClass($class); } public function getMethodStatus() { return self::METHOD_STATUS_UNSTABLE; } public function getMethodStatusDescription() { return pht('ApplicationEditor methods are highly unstable.'); } final protected function defineParamTypes() { return array( 'transactions' => 'list>', 'objectIdentifier' => 'optional id|phid|string', ); } final protected function defineReturnType() { return 'map'; } final protected function execute(ConduitAPIRequest $request) { $engine = $this->newEditEngine() ->setViewer($request->getUser()); return $engine->buildConduitResponse($request); } final public function getMethodDescription() { // TODO: We don't currently have a real viewer in this method. $viewer = PhabricatorUser::getOmnipotentUser(); $engine = $this->newEditEngine() ->setViewer($viewer); - $types = $engine->getAllEditTypes(); + $types = $engine->getConduitEditTypes(); $out = array(); $out[] = pht(<<getEditType(); $edit_summary = $type->getSummary(); $table[] = "| `{$edit_type}` | {$edit_summary} |"; } $out[] = implode("\n", $table); foreach ($types as $type) { $section = array(); $section[] = pht('Edit Type: %s', $type->getEditType()); $section[] = '---------'; $section[] = null; $section[] = $type->getDescription(); $section[] = null; $section[] = pht( 'This edit generates transactions of type `%s` internally.', $type->getTransactionType()); $section[] = null; $type_description = pht( 'Use `%s` to select this edit type.', $type->getEditType()); $value_type = $type->getValueType(); $value_description = $type->getValueDescription(); $table = array(); $table[] = "| {$key} | {$head_type} | {$description} |"; $table[] = '|--------|--------------|----------------|'; $table[] = "| `type` | `const` | {$type_description} |"; $table[] = "| `value` | `{$value_type}` | {$value_description} |"; $section[] = implode("\n", $table); $out[] = implode("\n", $section); } $out = implode("\n\n", $out); return $out; } } diff --git a/src/applications/transactions/editfield/PhabricatorCommentEditField.php b/src/applications/transactions/editfield/PhabricatorCommentEditField.php index 6080b9fa73..7a7e671e8c 100644 --- a/src/applications/transactions/editfield/PhabricatorCommentEditField.php +++ b/src/applications/transactions/editfield/PhabricatorCommentEditField.php @@ -1,25 +1,14 @@ $this->getValueForTransaction(), - ); - - return head($this->getEditTransactionTypes()) - ->generateTransaction($xaction, $spec); - } - } diff --git a/src/applications/transactions/editfield/PhabricatorEditField.php b/src/applications/transactions/editfield/PhabricatorEditField.php index 031fcc270b..c4f3227e9e 100644 --- a/src/applications/transactions/editfield/PhabricatorEditField.php +++ b/src/applications/transactions/editfield/PhabricatorEditField.php @@ -1,478 +1,470 @@ key = $key; return $this; } public function getKey() { return $this->key; } public function setLabel($label) { $this->label = $label; return $this; } public function getLabel() { return $this->label; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setAliases(array $aliases) { $this->aliases = $aliases; return $this; } public function getAliases() { return $this->aliases; } public function setObject($object) { $this->object = $object; return $this; } public function getObject() { return $this->object; } public function setDescription($description) { $this->description = $description; return $this; } public function getDescription() { return $this->description; } public function setIsLocked($is_locked) { $this->isLocked = $is_locked; return $this; } public function getIsLocked() { return $this->isLocked; } public function setIsPreview($preview) { $this->isPreview = $preview; return $this; } public function getIsPreview() { return $this->isPreview; } public function setIsReorderable($is_reorderable) { $this->isReorderable = $is_reorderable; return $this; } public function getIsReorderable() { return $this->isReorderable; } public function setIsEditDefaults($is_edit_defaults) { $this->isEditDefaults = $is_edit_defaults; return $this; } public function getIsEditDefaults() { return $this->isEditDefaults; } public function setIsDefaultable($is_defaultable) { $this->isDefaultable = $is_defaultable; return $this; } public function getIsDefaultable() { return $this->isDefaultable; } public function setIsLockable($is_lockable) { $this->isLockable = $is_lockable; return $this; } public function getIsLockable() { return $this->isLockable; } public function setIsHidden($is_hidden) { $this->isHidden = $is_hidden; return $this; } public function getIsHidden() { return $this->isHidden; } protected function newControl() { throw new PhutilMethodNotImplementedException(); } public function setIsSubmittedForm($is_submitted) { $this->isSubmittedForm = $is_submitted; return $this; } public function getIsSubmittedForm() { return $this->isSubmittedForm; } public function setIsRequired($is_required) { $this->isRequired = $is_required; return $this; } public function getIsRequired() { return $this->isRequired; } public function setControlError($control_error) { $this->controlError = $control_error; return $this; } public function getControlError() { return $this->controlError; } protected function renderControl() { $control = $this->newControl(); if ($control === null) { return null; } $control ->setValue($this->getValueForControl()) ->setName($this->getKey()); if (!$control->getLabel()) { $control->setLabel($this->getLabel()); } if ($this->getIsPreview()) { $disabled = true; $hidden = false; } else if ($this->getIsEditDefaults()) { $disabled = false; $hidden = false; } else { $disabled = $this->getIsLocked(); $hidden = $this->getIsHidden(); } if ($hidden) { return null; } $control->setDisabled($disabled); if ($this->getIsSubmittedForm()) { $error = $this->getControlError(); if ($error !== null) { $control->setError($error); } } else if ($this->getIsRequired()) { $control->setError(true); } return $control; } public function appendToForm(AphrontFormView $form) { $control = $this->renderControl(); if ($control !== null) { if ($this->getIsPreview()) { if ($this->getIsHidden()) { $control ->addClass('aphront-form-preview-hidden') ->setError(pht('Hidden')); } else if ($this->getIsLocked()) { $control ->setError(pht('Locked')); } } $form->appendControl($control); } return $this; } protected function getValueForControl() { return $this->getValue(); } public function getValueForDefaults() { $value = $this->getValue(); // By default, just treat the empty string like `null` since they're // equivalent for almost all fields and this reduces the number of // meaningless transactions we generate when adjusting defaults. if ($value === '') { return null; } return $value; } protected function getValue() { return $this->value; } public function setValue($value) { $this->hasValue = true; $this->initialValue = $value; $this->value = $value; return $this; } - public function generateTransaction( - PhabricatorApplicationTransaction $xaction) { - - if (!$this->getTransactionType()) { - return null; - } - - $xaction - ->setTransactionType($this->getTransactionType()) - ->setNewValue($this->getValueForTransaction()); - - foreach ($this->metadata as $key => $value) { - $xaction->setMetadataValue($key, $value); - } - - return $xaction; - } - public function setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function getMetadata() { return $this->metadata; } - protected function getValueForTransaction() { + public function getValueForTransaction() { return $this->getValue(); } public function getTransactionType() { return $this->transactionType; } public function setTransactionType($type) { $this->transactionType = $type; return $this; } public function readValueFromRequest(AphrontRequest $request) { $check = array_merge(array($this->getKey()), $this->getAliases()); foreach ($check as $key) { if (!$this->getValueExistsInRequest($request, $key)) { continue; } $this->value = $this->getValueFromRequest($request, $key); return; } $this->readValueFromObject($this->getObject()); return $this; } public function readValueFromObject($object) { $this->value = $this->getValueFromObject($object); return $this; } public function readDefaultValueFromConfiguration($value) { $this->value = $this->getDefaultValueFromConfiguration($value); return $this; } protected function getDefaultValueFromConfiguration($value) { return $value; } protected function getValueFromObject($object) { if ($this->hasValue) { return $this->value; } else { return $this->getDefaultValue(); } } protected function getValueExistsInRequest(AphrontRequest $request, $key) { return $this->getValueExistsInSubmit($request, $key); } protected function getValueFromRequest(AphrontRequest $request, $key) { return $this->getValueFromSubmit($request, $key); } /** * Read and return the value the object had when the user first loaded the * form. * * This is the initial value from the user's point of view when they started * the edit process, and used primarily to prevent race conditions for fields * like "Projects" and "Subscribers" that use tokenizers and support edge * transactions. * * Most fields do not need to store these values or deal with initial value * handling. * * @param AphrontRequest Request to read from. * @param string Key to read. * @return wild Value read from request. */ protected function getInitialValueFromSubmit(AphrontRequest $request, $key) { return null; } public function getInitialValue() { return $this->initialValue; } public function readValueFromSubmit(AphrontRequest $request) { $key = $this->getKey(); if ($this->getValueExistsInSubmit($request, $key)) { $value = $this->getValueFromSubmit($request, $key); } else { $value = $this->getDefaultValue(); } $this->value = $value; $initial_value = $this->getInitialValueFromSubmit($request, $key); $this->initialValue = $initial_value; return $this; } protected function getValueExistsInSubmit(AphrontRequest $request, $key) { $type = $this->getHTTPParameterType(); if ($type) { return $type->getExists($request, $key); } return false; } protected function getValueFromSubmit(AphrontRequest $request, $key) { return $this->getHTTPParameterType()->getValue($request, $key); } protected function getDefaultValue() { $type = $this->getHTTPParameterType(); if ($type) { return $type->getDefaultValue(); } return null; } final public function getHTTPParameterType() { $type = $this->newHTTPParameterType(); if ($type) { $type->setViewer($this->getViewer()); } return $type; } protected function newHTTPParameterType() { return new AphrontStringHTTPParameterType(); } public function setEditTypeKey($edit_type_key) { $this->editTypeKey = $edit_type_key; return $this; } public function getEditTypeKey() { if ($this->editTypeKey === null) { return $this->getKey(); } return $this->editTypeKey; } protected function newEditType() { return id(new PhabricatorSimpleEditType()) ->setValueType($this->getHTTPParameterType()->getTypeName()); } - protected function getEditTransactionType() { + protected function getEditType() { $transaction_type = $this->getTransactionType(); if ($transaction_type === null) { return null; } $type_key = $this->getEditTypeKey(); return $this->newEditType() ->setEditType($type_key) ->setTransactionType($transaction_type) ->setDescription($this->getDescription()) ->setMetadata($this->getMetadata()); } - public function getEditTransactionTypes() { - $edit_type = $this->getEditTransactionType(); + public function getConduitEditTypes() { + $edit_type = $this->getEditType(); + + if ($edit_type === null) { + return null; + } + + return array($edit_type); + } + + public function getWebEditTypes() { + $edit_type = $this->getEditType(); if ($edit_type === null) { return null; } return array($edit_type); } } diff --git a/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php b/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php index 4bf22317a8..b1ce19a1e0 100644 --- a/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php +++ b/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php @@ -1,114 +1,114 @@ useEdgeTransactions = $use_edge_transactions; return $this; } public function getUseEdgeTransactions() { return $this->useEdgeTransactions; } public function setEdgeTransactionDescriptions($add, $rem, $set) { $this->transactionDescriptions = array( '+' => $add, '-' => $rem, '=' => $set, ); return $this; } protected function newHTTPParameterType() { return new AphrontPHIDListHTTPParameterType(); } - protected function getValueForTransaction() { + public function getValueForTransaction() { $new = parent::getValueForTransaction(); if (!$this->getUseEdgeTransactions()) { return $new; } $old = $this->getInitialValue(); if ($old === null) { return array( '=' => array_fuse($new), ); } // If we're building an edge transaction and the request has data about the // original value the user saw when they loaded the form, interpret the // edit as a mixture of "+" and "-" operations instead of a single "=" // operation. This limits our exposure to race conditions by making most // concurrent edits merge correctly. $add = array_diff($new, $old); $rem = array_diff($old, $new); $value = array(); if ($add) { $value['+'] = array_fuse($add); } if ($rem) { $value['-'] = array_fuse($rem); } return $value; } protected function newEditType() { if ($this->getUseEdgeTransactions()) { return new PhabricatorEdgeEditType(); } return parent::newEditType(); } - public function getEditTransactionTypes() { + public function getConduitEditTypes() { if (!$this->getUseEdgeTransactions()) { - return parent::getEditTransactionTypes(); + return parent::getConduitEditTypes(); } $transaction_type = $this->getTransactionType(); if ($transaction_type === null) { return array(); } $type_key = $this->getEditTypeKey(); $strings = $this->transactionDescriptions; - $base = $this->getEditTransactionType(); + $base = $this->getEditType(); $add = id(clone $base) ->setEditType($type_key.'.add') ->setEdgeOperation('+') ->setDescription(idx($strings, '+')) ->setValueDescription(pht('List of PHIDs to add.')); $rem = id(clone $base) ->setEditType($type_key.'.remove') ->setEdgeOperation('-') ->setDescription(idx($strings, '-')) ->setValueDescription(pht('List of PHIDs to remove.')); $set = id(clone $base) ->setEditType($type_key.'.set') ->setEdgeOperation('=') ->setDescription(idx($strings, '=')) ->setValueDescription(pht('List of PHIDs to set.')); return array( $add, $rem, $set, ); } } diff --git a/src/applications/transactions/edittype/PhabricatorCommentEditType.php b/src/applications/transactions/edittype/PhabricatorCommentEditType.php index 5e4df3942f..ee3d840015 100644 --- a/src/applications/transactions/edittype/PhabricatorCommentEditType.php +++ b/src/applications/transactions/edittype/PhabricatorCommentEditType.php @@ -1,31 +1,26 @@ getTypeName(); } - public function generateTransaction( + public function generateTransactions( PhabricatorApplicationTransaction $template, array $spec) { $comment = $template->getApplicationTransactionCommentObject() ->setContent(idx($spec, 'value')); - $template - ->setTransactionType($this->getTransactionType()) + $xaction = $this->newTransaction($template) ->attachComment($comment); - foreach ($this->getMetadata() as $key => $value) { - $template->setMetadataValue($key, $value); - } - - return $template; + return array($xaction); } public function getValueDescription() { return pht('Comment to add, formated as remarkup.'); } } diff --git a/src/applications/transactions/edittype/PhabricatorEdgeEditType.php b/src/applications/transactions/edittype/PhabricatorEdgeEditType.php index 17d1441a80..1ee89fc5f6 100644 --- a/src/applications/transactions/edittype/PhabricatorEdgeEditType.php +++ b/src/applications/transactions/edittype/PhabricatorEdgeEditType.php @@ -1,51 +1,49 @@ edgeOperation = $edge_operation; return $this; } public function getEdgeOperation() { return $this->edgeOperation; } public function getValueType() { return 'list'; } - public function generateTransaction( + public function generateTransactions( PhabricatorApplicationTransaction $template, array $spec) { $value = idx($spec, 'value'); - $value = array_fuse($value); - $value = array( - $this->getEdgeOperation() => $value, - ); - $template - ->setTransactionType($this->getTransactionType()) - ->setNewValue($value); - - foreach ($this->getMetadata() as $key => $value) { - $template->setMetadataValue($key, $value); + if ($this->getEdgeOperation() !== null) { + $value = array_fuse($value); + $value = array( + $this->getEdgeOperation() => $value, + ); } - return $template; + $xaction = $this->newTransaction($template) + ->setNewValue($value); + + return array($xaction); } public function setValueDescription($value_description) { $this->valueDescription = $value_description; return $this; } public function getValueDescription() { return $this->valueDescription; } } diff --git a/src/applications/transactions/edittype/PhabricatorEditType.php b/src/applications/transactions/edittype/PhabricatorEditType.php index a1e580f6fd..ae4a05c059 100644 --- a/src/applications/transactions/edittype/PhabricatorEditType.php +++ b/src/applications/transactions/edittype/PhabricatorEditType.php @@ -1,76 +1,89 @@ description = $description; return $this; } public function getDescription() { return $this->description; } public function setSummary($summary) { $this->summary = $summary; return $this; } public function getSummary() { if ($this->summary === null) { return $this->getDescription(); } return $this->summary; } public function setField(PhabricatorEditField $field) { $this->field = $field; return $this; } public function getField() { return $this->field; } public function setEditType($edit_type) { $this->editType = $edit_type; return $this; } public function getEditType() { return $this->editType; } public function setMetadata($metadata) { $this->metadata = $metadata; return $this; } public function getMetadata() { return $this->metadata; } public function setTransactionType($transaction_type) { $this->transactionType = $transaction_type; return $this; } public function getTransactionType() { return $this->transactionType; } - abstract public function generateTransaction( + abstract public function generateTransactions( PhabricatorApplicationTransaction $template, array $spec); abstract public function getValueType(); abstract public function getValueDescription(); + protected function newTransaction( + PhabricatorApplicationTransaction $template) { + + $xaction = id(clone $template) + ->setTransactionType($this->getTransactionType()); + + foreach ($this->getMetadata() as $key => $value) { + $xaction->setMetadataValue($key, $value); + } + + return $xaction; + } + } diff --git a/src/applications/transactions/edittype/PhabricatorSimpleEditType.php b/src/applications/transactions/edittype/PhabricatorSimpleEditType.php index 3aa0575579..c1a1a98730 100644 --- a/src/applications/transactions/edittype/PhabricatorSimpleEditType.php +++ b/src/applications/transactions/edittype/PhabricatorSimpleEditType.php @@ -1,41 +1,36 @@ valueType = $value_type; return $this; } public function getValueType() { return $this->valueType; } - public function generateTransaction( + public function generateTransactions( PhabricatorApplicationTransaction $template, array $spec) { - $template - ->setTransactionType($this->getTransactionType()) + $edit = $this->newTransaction($template) ->setNewValue(idx($spec, 'value')); - foreach ($this->getMetadata() as $key => $value) { - $template->setMetadataValue($key, $value); - } - - return $template; + return array($edit); } public function setValueDescription($value_description) { $this->valueDescription = $value_description; return $this; } public function getValueDescription() { return $this->valueDescription; } }