diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index b57d8a47d7..a872c52dfa 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -1,182 +1,185 @@ getViewer()); } protected function newObjectQuery() { return id(new ManiphestTaskQuery()); } protected function getObjectCreateTitleText($object) { return pht('Create New Task'); } protected function getObjectEditTitleText($object) { return pht('Edit %s %s', $object->getMonogram(), $object->getTitle()); } protected function getObjectEditShortText($object) { return $object->getMonogram(); } protected function getObjectCreateShortText() { return pht('Create Task'); } protected function getCommentViewHeaderText($object) { $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); if (!$is_serious) { return pht('Weigh In'); } return parent::getCommentViewHeaderText($object); } protected function getObjectViewURI($object) { return '/'.$object->getMonogram(); } protected function buildCustomEditFields($object) { $status_map = $this->getTaskStatusMap($object); $priority_map = $this->getTaskPriorityMap($object); if ($object->isClosed()) { $priority_label = null; $default_status = ManiphestTaskStatus::getDefaultStatus(); } else { $priority_label = pht('Change Priority'); $default_status = ManiphestTaskStatus::getDefaultClosedStatus(); } if ($object->getOwnerPHID()) { $owner_value = array($object->getOwnerPHID()); } else { $owner_value = array($this->getViewer()->getPHID()); } return array( id(new PhabricatorTextEditField()) ->setKey('title') ->setLabel(pht('Title')) ->setDescription(pht('Name of the task.')) ->setTransactionType(ManiphestTransaction::TYPE_TITLE) ->setIsRequired(true) ->setValue($object->getTitle()), id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) ->setDescription(pht('Status of the task.')) ->setTransactionType(ManiphestTransaction::TYPE_STATUS) + ->setIsCopyable(true) ->setValue($object->getStatus()) ->setOptions($status_map) ->setCommentActionLabel(pht('Change Status')) ->setCommentActionDefaultValue($default_status), id(new PhabricatorUsersEditField()) ->setKey('owner') ->setAliases(array('ownerPHID', 'assign', 'assigned')) ->setLabel(pht('Assigned To')) ->setDescription(pht('User who is responsible for the task.')) ->setTransactionType(ManiphestTransaction::TYPE_OWNER) + ->setIsCopyable(true) ->setSingleValue($object->getOwnerPHID()) ->setCommentActionLabel(pht('Assign / Claim')) ->setCommentActionDefaultValue($owner_value), id(new PhabricatorSelectEditField()) ->setKey('priority') ->setLabel(pht('Priority')) ->setDescription(pht('Priority of the task.')) ->setTransactionType(ManiphestTransaction::TYPE_PRIORITY) + ->setIsCopyable(true) ->setValue($object->getPriority()) ->setOptions($priority_map) ->setCommentActionLabel($priority_label), id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) ->setDescription(pht('Task description.')) ->setTransactionType(ManiphestTransaction::TYPE_DESCRIPTION) ->setValue($object->getDescription()), ); } protected function getEditorURI() { // TODO: Remove when cutting over. return $this->getApplication()->getApplicationURI('editpro/'); } private function getTaskStatusMap(ManiphestTask $task) { $status_map = ManiphestTaskStatus::getTaskStatusMap(); $current_status = $task->getStatus(); // If the current status is something we don't recognize (maybe an older // status which was deleted), put a dummy entry in the status map so that // saving the form doesn't destroy any data by accident. if (idx($status_map, $current_status) === null) { $status_map[$current_status] = pht('', $current_status); } $dup_status = ManiphestTaskStatus::getDuplicateStatus(); foreach ($status_map as $status => $status_name) { // Always keep the task's current status. if ($status == $current_status) { continue; } // Don't allow tasks to be changed directly into "Closed, Duplicate" // status. Instead, you have to merge them. See T4819. if ($status == $dup_status) { unset($status_map[$status]); continue; } // Don't let new or existing tasks be moved into a disabled status. if (ManiphestTaskStatus::isDisabledStatus($status)) { unset($status_map[$status]); continue; } } return $status_map; } private function getTaskPriorityMap(ManiphestTask $task) { $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); $current_priority = $task->getPriority(); // If the current value isn't a legitimate one, put it in the dropdown // anyway so saving the form doesn't cause a side effects. if (idx($priority_map, $current_priority) === null) { $priority_map[$current_priority] = pht( '', $current_priority); } foreach ($priority_map as $priority => $priority_name) { // Always keep the current priority. if ($priority == $current_priority) { continue; } if (ManiphestTaskPriority::isDisabledPriority($priority)) { unset($priority_map[$priority]); continue; } } return $priority_map; } } diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php index c753556e89..e17c59a2a2 100644 --- a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php +++ b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php @@ -1,93 +1,95 @@ getViewer()); } protected function newObjectQuery() { return id(new PhabricatorOwnersPackageQuery()) ->needOwners(true); } protected function getObjectCreateTitleText($object) { return pht('Create New Package'); } protected function getObjectEditTitleText($object) { return pht('Edit Package %s', $object->getName()); } protected function getObjectEditShortText($object) { return pht('Package %d', $object->getID()); } protected function getObjectCreateShortText() { return pht('Create Package'); } protected function getObjectViewURI($object) { $id = $object->getID(); return "/owners/package/{$id}/"; } protected function buildCustomEditFields($object) { return array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the package.')) ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_NAME) ->setIsRequired(true) ->setValue($object->getName()), id(new PhabricatorDatasourceEditField()) ->setKey('owners') ->setLabel(pht('Owners')) ->setDescription(pht('Users and projects which own the package.')) ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_OWNERS) ->setDatasource(new PhabricatorProjectOrUserDatasource()) + ->setIsCopyable(true) ->setValue($object->getOwnerPHIDs()), id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) ->setDescription(pht('Archive or enable the package.')) ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_STATUS) ->setValue($object->getStatus()) ->setOptions($object->getStatusNameMap()), id(new PhabricatorSelectEditField()) ->setKey('auditing') ->setLabel(pht('Auditing')) ->setDescription( pht( 'Automatically trigger audits for commits affecting files in '. 'this package.')) ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_AUDITING) + ->setIsCopyable(true) ->setValue($object->getAuditingEnabled()) ->setOptions( array( '' => pht('Disabled'), '1' => pht('Enabled'), )), id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) ->setDescription(pht('Human-readable description of the package.')) ->setTransactionType( PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION) ->setValue($object->getDescription()), ); } } diff --git a/src/applications/paste/editor/PhabricatorPasteEditEngine.php b/src/applications/paste/editor/PhabricatorPasteEditEngine.php index 2a8ebb4798..e3b00bf4db 100644 --- a/src/applications/paste/editor/PhabricatorPasteEditEngine.php +++ b/src/applications/paste/editor/PhabricatorPasteEditEngine.php @@ -1,96 +1,97 @@ getViewer()); } protected function newObjectQuery() { return id(new PhabricatorPasteQuery()) ->needRawContent(true); } protected function getObjectCreateTitleText($object) { return pht('Create New Paste'); } protected function getObjectEditTitleText($object) { return pht('Edit %s %s', $object->getMonogram(), $object->getTitle()); } protected function getObjectEditShortText($object) { return $object->getMonogram(); } protected function getObjectCreateShortText() { return pht('Create Paste'); } protected function getCommentViewHeaderText($object) { $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); if (!$is_serious) { return pht('Eat Paste'); } return parent::getCommentViewHeaderText($object); } protected function getObjectViewURI($object) { return '/P'.$object->getID(); } protected function buildCustomEditFields($object) { $langs = array( '' => pht('(Detect From Filename in Title)'), ) + PhabricatorEnv::getEnvConfig('pygments.dropdown-choices'); return array( id(new PhabricatorTextEditField()) ->setKey('title') ->setLabel(pht('Title')) ->setDescription(pht('Name of the paste.')) ->setTransactionType(PhabricatorPasteTransaction::TYPE_TITLE) ->setValue($object->getTitle()), id(new PhabricatorSelectEditField()) ->setKey('language') ->setLabel(pht('Language')) ->setDescription( pht( 'Programming language to interpret the paste as for syntax '. 'highlighting. By default, the language is inferred from the '. 'title.')) ->setAliases(array('lang')) ->setTransactionType(PhabricatorPasteTransaction::TYPE_LANGUAGE) + ->setIsCopyable(true) ->setValue($object->getLanguage()) ->setOptions($langs), id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) ->setDescription(pht('Archive the paste.')) ->setTransactionType(PhabricatorPasteTransaction::TYPE_STATUS) ->setValue($object->getStatus()) ->setOptions(PhabricatorPaste::getStatusNameMap()), id(new PhabricatorTextAreaEditField()) ->setKey('text') ->setLabel(pht('Text')) ->setDescription(pht('The main body text of the paste.')) ->setTransactionType(PhabricatorPasteTransaction::TYPE_CONTENT) ->setMonospaced(true) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setValue($object->getRawContent()), ); } } diff --git a/src/applications/policy/editor/PhabricatorPolicyEditEngineExtension.php b/src/applications/policy/editor/PhabricatorPolicyEditEngineExtension.php index 21ea34ca28..d7fe2e4d6f 100644 --- a/src/applications/policy/editor/PhabricatorPolicyEditEngineExtension.php +++ b/src/applications/policy/editor/PhabricatorPolicyEditEngineExtension.php @@ -1,118 +1,120 @@ getViewer(); $editor = $object->getApplicationTransactionEditor(); $types = $editor->getTransactionTypesForObject($object); $types = array_fuse($types); $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($object) ->execute(); $map = array( PhabricatorTransactions::TYPE_VIEW_POLICY => array( 'key' => 'policy.view', 'aliases' => array('view'), 'capability' => PhabricatorPolicyCapability::CAN_VIEW, 'label' => pht('View Policy'), 'description' => pht('Controls who can view the object.'), 'edit' => 'view', ), PhabricatorTransactions::TYPE_EDIT_POLICY => array( 'key' => 'policy.edit', 'aliases' => array('edit'), 'capability' => PhabricatorPolicyCapability::CAN_EDIT, 'label' => pht('Edit Policy'), 'description' => pht('Controls who can edit the object.'), 'edit' => 'edit', ), PhabricatorTransactions::TYPE_JOIN_POLICY => array( 'key' => 'policy.join', 'aliases' => array('join'), 'capability' => PhabricatorPolicyCapability::CAN_JOIN, 'label' => pht('Join Policy'), 'description' => pht('Controls who can join the object.'), 'edit' => 'join', ), ); $fields = array(); foreach ($map as $type => $spec) { if (empty($types[$type])) { continue; } $capability = $spec['capability']; $key = $spec['key']; $aliases = $spec['aliases']; $label = $spec['label']; $description = $spec['description']; $edit = $spec['edit']; $policy_field = id(new PhabricatorPolicyEditField()) ->setKey($key) ->setLabel($label) ->setDescription($description) ->setAliases($aliases) + ->setIsCopyable(true) ->setCapability($capability) ->setPolicies($policies) ->setTransactionType($type) ->setEditTypeKey($edit) ->setValue($object->getPolicy($capability)); $fields[] = $policy_field; if (!($object instanceof PhabricatorSpacesInterface)) { if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { $type_space = PhabricatorTransactions::TYPE_SPACE; if (isset($types[$type_space])) { $space_field = id(new PhabricatorSpaceEditField()) ->setKey('spacePHID') ->setLabel(pht('Space')) ->setEditTypeKey('space') ->setDescription( pht('Shifts the object in the Spaces application.')) + ->setIsCopyable(true) ->setIsReorderable(false) ->setAliases(array('space', 'policy.space')) ->setTransactionType($type_space) ->setValue($object->getSpacePHID()); $fields[] = $space_field; $policy_field->setSpaceField($space_field); } } } } return $fields; } } diff --git a/src/applications/project/editor/PhabricatorProjectsEditEngineExtension.php b/src/applications/project/editor/PhabricatorProjectsEditEngineExtension.php index b8cd651a11..4aadf75e5e 100644 --- a/src/applications/project/editor/PhabricatorProjectsEditEngineExtension.php +++ b/src/applications/project/editor/PhabricatorProjectsEditEngineExtension.php @@ -1,66 +1,67 @@ getPHID(); if ($object_phid) { $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $object_phid, $project_edge_type); $project_phids = array_reverse($project_phids); } else { $project_phids = array(); } $projects_field = id(new PhabricatorProjectsEditField()) ->setKey('projectPHIDs') ->setLabel(pht('Projects')) ->setEditTypeKey('projects') ->setDescription(pht('Add or remove associated projects.')) ->setAliases(array('project', 'projects')) + ->setIsCopyable(true) ->setUseEdgeTransactions(true) ->setEdgeTransactionDescriptions( pht('Add projects.'), pht('Remove projects.'), pht('Set associated projects, overwriting current value.')) ->setCommentActionLabel(pht('Add Projects')) ->setTransactionType($edge_type) ->setMetadataValue('edge:type', $project_edge_type) ->setValue($project_phids); return array( $projects_field, ); } } diff --git a/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditEngineExtension.php b/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditEngineExtension.php index 2023fbee09..228c1a8cc1 100644 --- a/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditEngineExtension.php +++ b/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditEngineExtension.php @@ -1,62 +1,63 @@ getPHID(); if ($object_phid) { $sub_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( $object_phid); } else { // TODO: Allow applications to provide default subscribers? Maniphest // does this at a minimum. $sub_phids = array(); } $subscribers_field = id(new PhabricatorSubscribersEditField()) ->setKey('subscriberPHIDs') ->setLabel(pht('Subscribers')) ->setEditTypeKey('subscribers') ->setDescription(pht('Manage subscribers.')) ->setAliases(array('subscriber', 'subscribers')) + ->setIsCopyable(true) ->setUseEdgeTransactions(true) ->setEdgeTransactionDescriptions( pht('Add subscribers.'), pht('Remove subscribers.'), pht('Set subscribers, overwriting current value.')) ->setCommentActionLabel(pht('Add Subscribers')) ->setTransactionType($subscribers_type) ->setValue($sub_phids); return array( $subscribers_field, ); } } diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index a19ddf6400..ff8407c1d3 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -1,1468 +1,1509 @@ 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); + $fields = $config->applyConfigurationToFields($this, $object, $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'); } /** * @task text */ protected function getCommentViewHeaderText($object) { return pht('Add Comment'); } /** * @task text */ protected function getCommentViewButtonText($object) { return pht('Add Comment'); } /** * @task text */ protected function getQuickCreateMenuHeaderText() { return $this->getObjectCreateShortText(); } /* -( Edit Engine Configuration )------------------------------------------ */ protected function supportsEditEngineConfiguration() { return true; } final protected function getEditEngineConfiguration() { return $this->editEngineConfiguration; } /** * Load the default configuration, ignoring customization in the database * (which means we implicitly ignore policies). * * This is used from places like Conduit, where the fields available in the * API should not be affected by configuration changes. * * @return PhabricatorEditEngineConfiguration Default configuration, ignoring * customization. */ private function loadDefaultEditEngineConfiguration() { return $this->loadEditEngineConfigurationWithOptions( self::EDITENGINECONFIG_DEFAULT, true); } /** * Load a named configuration, respecting database customization and policies. * * @param string Configuration key, or null to load the default. * @return PhabricatorEditEngineConfiguration Default configuration, * respecting customization. */ private function loadEditEngineConfiguration($key) { if (!strlen($key)) { $key = self::EDITENGINECONFIG_DEFAULT; } return $this->loadEditEngineConfigurationWithOptions( $key, false); } private function loadEditEngineConfigurationWithOptions( $key, $ignore_database) { $viewer = $this->getViewer(); $config = id(new PhabricatorEditEngineConfigurationQuery()) ->setViewer($viewer) ->withEngineKeys(array($this->getEngineKey())) ->withIdentifiers(array($key)) ->withIgnoreDatabaseConfigurations($ignore_database) ->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. + * @param list List of required capability constants, or omit for + * defaults. * @return object Corresponding editable object. * @task load */ - private function newObjectFromIdentifier($identifier) { + private function newObjectFromIdentifier( + $identifier, + array $capabilities = array()) { if (is_int($identifier) || ctype_digit($identifier)) { - $object = $this->newObjectFromID($identifier); + $object = $this->newObjectFromID($identifier, $capabilities); 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); + $object = $this->newObjectFromPHID($identifier, $capabilities); 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()); + $object = $this->newObjectFromPHID($target->getPHID(), $capabilities); 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. * @param list List of required capability constants, or omit for * defaults. * @return object|null Object, or null if no such object exists. * @task load */ private function newObjectFromID($id, array $capabilities = array()) { $query = $this->newObjectQuery() ->withIDs(array($id)); return $this->newObjectFromQuery($query, $capabilities); } /** * Load an object by PHID. * * @param phid Object PHID. + * @param list List of required capability constants, or omit for + * defaults. * @return object|null Object, or null if no such object exists. * @task load */ - private function newObjectFromPHID($phid) { + private function newObjectFromPHID($phid, array $capabilities = array()) { $query = $this->newObjectQuery() ->withPHIDs(array($phid)); - return $this->newObjectFromQuery($query); + return $this->newObjectFromQuery($query, $capabilities); } /** * Load an object given a configured query. * * @param PhabricatorPolicyAwareQuery Configured query. * @param list List of required capabilitiy constants, or omit for * defaults. * @return object|null Object, or null if no such object exists. * @task load */ private function newObjectFromQuery( PhabricatorPolicyAwareQuery $query, array $capabilities = array()) { $viewer = $this->getViewer(); if (!$capabilities) { $capabilities = array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } $object = $query ->setViewer($viewer) ->requireCapabilities($capabilities) ->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(); $action = $request->getURIData('editAction'); $capabilities = array(); $use_default = false; switch ($action) { case 'comment': $capabilities = array( PhabricatorPolicyCapability::CAN_VIEW, ); $use_default = true; break; + case 'parameters': + $use_default = true; + break; default: break; } if ($use_default) { $config = $this->loadDefaultEditEngineConfiguration(); } else { $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, $capabilities); if (!$object) { return new Aphront404Response(); } } else { $this->setIsCreate(true); $object = $this->newEditableObject(); } $this->validateObject($object); switch ($action) { case 'parameters': return $this->buildParametersResponse($object); case 'nodefault': return $this->buildNoDefaultResponse($object); case 'comment': return $this->buildCommentResponse($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) { $types = $field->getWebEditTypes(); foreach ($types as $type) { $type_xactions = $type->generateTransactions( clone $template, array( 'value' => $field->getValueForTransaction(), )); if (!$type_xactions) { continue; } foreach ($type_xactions as $type_xaction) { $xactions[] = $type_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()) { + $template = $request->getStr('template'); + + if (strlen($template)) { + $template_object = $this->newObjectFromIdentifier( + $template, + array( + PhabricatorPolicyCapability::CAN_VIEW, + )); + if (!$template_object) { + return new Aphront404Response(); + } + } else { + $template_object = null; + } + + if ($template_object) { + $copy_fields = $this->buildEditFields($template_object); + $copy_fields = mpull($copy_fields, null, 'getKey'); + foreach ($copy_fields as $copy_key => $copy_field) { + if (!$copy_field->getIsCopyable()) { + unset($copy_fields[$copy_key]); + } + } + } else { + $copy_fields = array(); + } + foreach ($fields as $field) { if ($field->getIsLocked() || $field->getIsHidden()) { continue; } + $field_key = $field->getKey(); + if (isset($copy_fields[$field_key])) { + $field->readValueFromField($copy_fields[$field_key]); + } + $field->readValueFromRequest($request); } } } $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 = $this->loadUsableConfigurationsForCreate(); $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); } final public function buildEditEngineCommentView($object) { $config = $this->loadDefaultEditEngineConfiguration(); $viewer = $this->getViewer(); $object_phid = $object->getPHID(); $header_text = $this->getCommentViewHeaderText($object); $button_text = $this->getCommentViewButtonText($object); $comment_uri = $this->getEditURI($object, 'comment/'); $view = id(new PhabricatorApplicationTransactionCommentView()) ->setUser($viewer) ->setObjectPHID($object_phid) ->setHeaderText($header_text) ->setAction($comment_uri) ->setSubmitButtonName($button_text); $draft = PhabricatorVersionedDraft::loadDraft( $object_phid, $viewer->getPHID()); if ($draft) { $view->setVersionedDraft($draft); } $view->setCurrentVersion($this->loadDraftVersion($object)); $fields = $this->buildEditFields($object); $all_types = array(); foreach ($fields as $field) { // TODO: Load draft stuff. $types = $field->getCommentEditTypes(); foreach ($types as $type) { $all_types[] = $type; } } $view->setEditTypes($all_types); return $view; } protected function loadDraftVersion($object) { $viewer = $this->getViewer(); if (!$viewer->isLoggedIn()) { return null; } $template = $object->getApplicationTransactionTemplate(); $conn_r = $template->establishConnection('r'); // Find the most recent transaction the user has written. We'll use this // as a version number to make sure that out-of-date drafts get discarded. $result = queryfx_one( $conn_r, 'SELECT id AS version FROM %T WHERE objectPHID = %s AND authorPHID = %s ORDER BY id DESC LIMIT 1', $template->getTableName(), $object->getPHID(), $viewer->getPHID()); if ($result) { return (int)$result['version']; } else { return null; } } /* -( 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); } private function buildCommentResponse($object) { $viewer = $this->getViewer(); if ($this->getIsCreate()) { return new Aphront404Response(); } $controller = $this->getController(); $request = $controller->getRequest(); if (!$request->isFormPost()) { return new Aphront400Response(); } $config = $this->loadDefaultEditEngineConfiguration(); $fields = $this->buildEditFields($object); $is_preview = $request->isPreviewRequest(); $view_uri = $this->getObjectViewURI($object); $template = $object->getApplicationTransactionTemplate(); $comment_template = $template->getApplicationTransactionCommentObject(); $comment_text = $request->getStr('comment'); $actions = $request->getStr('editengine.actions'); if ($actions) { $actions = phutil_json_decode($actions); } if ($is_preview) { $version_key = PhabricatorVersionedDraft::KEY_VERSION; $request_version = $request->getInt($version_key); $current_version = $this->loadDraftVersion($object); if ($request_version >= $current_version) { $draft = PhabricatorVersionedDraft::loadOrCreateDraft( $object->getPHID(), $viewer->getPHID(), $current_version); // TODO: This is just a proof of concept. $draft ->setProperty('temporary.comment', $comment_text) ->setProperty('actions', $actions) ->save(); } } $xactions = array(); if ($actions) { $type_map = array(); foreach ($fields as $field) { $types = $field->getCommentEditTypes(); foreach ($types as $type) { $type_map[$type->getEditType()] = array( 'type' => $type, 'field' => $field, ); } } foreach ($actions as $action) { $type = idx($action, 'type'); if (!$type) { continue; } $spec = idx($type_map, $type); if (!$spec) { continue; } $edit_type = $spec['type']; $field = $spec['field']; $field->readValueFromComment($action); $type_xactions = $edit_type->generateTransactions( $template, array( 'value' => $field->getValueForTransaction(), )); foreach ($type_xactions as $type_xaction) { $xactions[] = $type_xaction; } } } if (strlen($comment_text) || !$xactions) { $xactions[] = id(clone $template) ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT) ->attachComment( id(clone $comment_template) ->setContent($comment_text)); } $editor = $object->getApplicationTransactionEditor() ->setActor($viewer) ->setContinueOnNoEffect($request->isContinueRequest()) ->setContentSourceFromRequest($request) ->setIsPreview($is_preview); try { $xactions = $editor->applyTransactions($object, $xactions); } catch (PhabricatorApplicationTransactionNoEffectException $ex) { return id(new PhabricatorApplicationTransactionNoEffectResponse()) ->setCancelURI($view_uri) ->setException($ex); } if (!$is_preview) { PhabricatorVersionedDraft::purgeDrafts( $object->getPHID(), $viewer->getPHID(), $this->loadDraftVersion($object)); } if ($request->isAjax() && $is_preview) { return id(new PhabricatorApplicationTransactionResponse()) ->setViewer($viewer) ->setTransactions($xactions) ->setIsPreview($is_preview); } else { return id(new AphrontRedirectResponse()) ->setURI($view_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->loadDefaultEditEngineConfiguration(); 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->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']]; $type_xactions = $type->generateTransactions( clone $template, $xaction); foreach ($type_xactions as $type_xaction) { $results[] = $type_xaction; } } return $results; } /** * @return map * @task conduit */ private function getConduitEditTypesFromFields(array $fields) { $types = array(); foreach ($fields as $field) { $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 getConduitEditTypes() { $config = $this->loadDefaultEditEngineConfiguration(); if (!$config) { return array(); } $object = $this->newEditableObject(); $fields = $this->buildEditFields($object); 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(); } public function getIcon() { $application = $this->getApplication(); return $application->getFontIcon(); } public function loadQuickCreateItems() { $configs = $this->loadUsableConfigurationsForCreate(); $items = array(); if (!$configs) { // No items to add. } else if (count($configs) == 1) { $config = head($configs); $items[] = $this->newQuickCreateItem($config); } else { $group_name = $this->getQuickCreateMenuHeaderText(); $items[] = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_LABEL) ->setName($group_name); foreach ($configs as $config) { $items[] = $this->newQuickCreateItem($config); } } return $items; } private function loadUsableConfigurationsForCreate() { $viewer = $this->getViewer(); return id(new PhabricatorEditEngineConfigurationQuery()) ->setViewer($viewer) ->withEngineKeys(array($this->getEngineKey())) ->withIsDefault(true) ->withIsDisabled(false) ->execute(); } private function newQuickCreateItem( PhabricatorEditEngineConfiguration $config) { $item_name = $config->getName(); $item_icon = $config->getIcon(); $form_key = $config->getIdentifier(); $item_uri = $this->getEditURI(null, "form/{$form_key}/"); return id(new PHUIListItemView()) ->setName($item_name) ->setIcon($item_icon) ->setHref($item_uri); } /* -( 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/editfield/PhabricatorEditField.php b/src/applications/transactions/editfield/PhabricatorEditField.php index 788ee526f3..3cc2189ccb 100644 --- a/src/applications/transactions/editfield/PhabricatorEditField.php +++ b/src/applications/transactions/editfield/PhabricatorEditField.php @@ -1,520 +1,539 @@ 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; } + public function setIsCopyable($is_copyable) { + $this->isCopyable = $is_copyable; + return $this; + } + + public function getIsCopyable() { + return $this->isCopyable; + } + 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; } public function setCommentActionLabel($label) { $this->commentActionLabel = $label; return $this; } public function getCommentActionLabel() { return $this->commentActionLabel; } protected function newControl() { throw new PhutilMethodNotImplementedException(); } protected function buildControl() { $control = $this->newControl(); if ($control === null) { return null; } $control ->setValue($this->getValueForControl()) ->setName($this->getKey()); if (!$control->getLabel()) { $control->setLabel($this->getLabel()); } if ($this->getIsSubmittedForm()) { $error = $this->getControlError(); if ($error !== null) { $control->setError($error); } } else if ($this->getIsRequired()) { $control->setError(true); } return $control; } protected function renderControl() { $control = $this->buildControl(); if ($control === null) { return null; } 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); 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 setMetadataValue($key, $value) { $this->metadata[$key] = $value; return $this; } public function getMetadata() { return $this->metadata; } 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 = $this->getAllReadValueFromRequestKeys(); foreach ($check as $key) { if (!$this->getValueExistsInRequest($request, $key)) { continue; } $this->value = $this->getValueFromRequest($request, $key); break; } return $this; } public function readValueFromComment($action) { $this->value = $this->getValueFromComment(idx($action, 'value')); return $this; } protected function getValueFromComment($value) { return $value; } public function getAllReadValueFromRequestKeys() { $keys = array(); $keys[] = $this->getKey(); foreach ($this->getAliases() as $alias) { $keys[] = $alias; } return $keys; } 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->getHTTPParameterValueExists($request, $key); } protected function getValueFromRequest(AphrontRequest $request, $key) { return $this->getHTTPParameterValue($request, $key); } + public function readValueFromField(PhabricatorEditField $other) { + $this->value = $this->getValueFromField($other); + return $this; + } + + protected function getValueFromField(PhabricatorEditField $other) { + return $other->getValue(); + } + /** * 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) { return $this->getHTTPParameterValueExists($request, $key); } protected function getValueFromSubmit(AphrontRequest $request, $key) { return $this->getHTTPParameterValue($request, $key); } protected function getHTTPParameterValueExists( AphrontRequest $request, $key) { $type = $this->getHTTPParameterType(); if ($type) { return $type->getExists($request, $key); } return false; } protected function getHTTPParameterValue($request, $key) { $type = $this->getHTTPParameterType(); if ($type) { return $type->getValue($request, $key); } return null; } 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 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 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); } public function getCommentEditTypes() { return array(); } } diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php index 57bf585304..5d80d95c5e 100644 --- a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php +++ b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php @@ -1,267 +1,270 @@ setEngineKey($engine->getEngineKey()) ->attachEngine($engine) ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->setEditPolicy($edit_policy); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorEditEngineConfigurationPHIDType::TYPECONST); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'engineKey' => 'text64', 'builtinKey' => 'text64?', 'name' => 'text255', 'isDisabled' => 'bool', 'isDefault' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_engine' => array( 'columns' => array('engineKey', 'builtinKey'), 'unique' => true, ), 'key_default' => array( 'columns' => array('engineKey', 'isDefault', 'isDisabled'), ), ), ) + parent::getConfiguration(); } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function attachEngine(PhabricatorEditEngine $engine) { $this->engine = $engine; return $this; } public function getEngine() { return $this->assertAttached($this->engine); } public function applyConfigurationToFields( PhabricatorEditEngine $engine, + $object, array $fields) { $fields = mpull($fields, null, 'getKey'); + $is_new = !$object->getID(); + $values = $this->getProperty('defaults', array()); foreach ($fields as $key => $field) { - if ($engine->getIsCreate()) { + if ($is_new) { if (array_key_exists($key, $values)) { $field->readDefaultValueFromConfiguration($values[$key]); } } } $locks = $this->getFieldLocks(); foreach ($fields as $field) { $key = $field->getKey(); switch (idx($locks, $key)) { case self::LOCK_LOCKED: $field->setIsHidden(false); $field->setIsLocked(true); break; case self::LOCK_HIDDEN: $field->setIsHidden(true); $field->setIsLocked(false); break; case self::LOCK_VISIBLE: $field->setIsHidden(false); $field->setIsLocked(false); break; default: // If we don't have an explicit value, don't make any adjustments. break; } } $fields = $this->reorderFields($fields); $preamble = $this->getPreamble(); if (strlen($preamble)) { $fields = array( 'config.preamble' => id(new PhabricatorInstructionsEditField()) ->setKey('config.preamble') ->setIsReorderable(false) ->setIsDefaultable(false) ->setIsLockable(false) ->setValue($preamble), ) + $fields; } return $fields; } private function reorderFields(array $fields) { $keys = $this->getFieldOrder(); $fields = array_select_keys($fields, $keys) + $fields; return $fields; } public function getURI() { $engine_key = $this->getEngineKey(); $key = $this->getIdentifier(); return "/transactions/editengine/{$engine_key}/view/{$key}/"; } public function getIdentifier() { $key = $this->getID(); if (!$key) { $key = $this->getBuiltinKey(); } return $key; } public function getDisplayName() { $name = $this->getName(); if (strlen($name)) { return $name; } $builtin = $this->getBuiltinKey(); if ($builtin !== null) { return pht('Builtin Form "%s"', $builtin); } return pht('Untitled Form'); } public function getPreamble() { return $this->getProperty('preamble'); } public function setPreamble($preamble) { return $this->setProperty('preamble', $preamble); } public function setFieldOrder(array $field_order) { return $this->setProperty('order', $field_order); } public function getFieldOrder() { return $this->getProperty('order', array()); } public function setFieldLocks(array $field_locks) { return $this->setProperty('locks', $field_locks); } public function getFieldLocks() { return $this->getProperty('locks', array()); } public function getFieldDefault($key) { $defaults = $this->getProperty('defaults', array()); return idx($defaults, $key); } public function setFieldDefault($key, $value) { $defaults = $this->getProperty('defaults', array()); $defaults[$key] = $value; return $this->setProperty('defaults', $defaults); } public function getIcon() { return $this->getEngine()->getIcon(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new PhabricatorEditEngineConfigurationEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new PhabricatorEditEngineConfigurationTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } } diff --git a/src/applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php b/src/applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php index 9200bdcf1f..bd6ffaa807 100644 --- a/src/applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php +++ b/src/applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php @@ -1,258 +1,313 @@ object = $object; return $this; } public function getObject() { return $this->object; } public function setFields(array $fields) { $this->fields = $fields; return $this; } public function getFields() { return $this->fields; } public function render() { $object = $this->getObject(); $fields = $this->getFields(); $uri = 'https://your.install.com/application/edit/'; // Remove fields which do not expose an HTTP parameter type. $types = array(); foreach ($fields as $key => $field) { $type = $field->getHTTPParameterType(); if ($type === null) { unset($fields[$key]); continue; } $types[$type->getTypeName()] = $type; } $intro = pht(<<getLabel(), head($field->getAllReadValueFromRequestKeys()), $field->getHTTPParameterType()->getTypeName(), $field->getDescription(), ); } $main_table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Label'), pht('Key'), pht('Type'), pht('Description'), )) ->setColumnClasses( array( 'pri', null, null, 'wide', )); $aliases_text = pht(<<getAllReadValueFromRequestKeys(), 1); if (!$aliases) { continue; } $rows[] = array( $field->getLabel(), $field->getKey(), implode(', ', $aliases), ); } $alias_table = id(new AphrontTableView($rows)) ->setNoDataString(pht('This object has no fields with aliases.')) ->setHeaders( array( pht('Label'), pht('Key'), pht('Aliases'), )) ->setColumnClasses( array( 'pri', null, 'wide', )); + $template_text = pht(<<setIconFont('fa-check-circle green'); + $no = id(new PHUIIconView())->setIconFont('fa-times grey'); + + $rows = array(); + foreach ($fields as $field) { + $rows[] = array( + $field->getLabel(), + $field->getIsCopyable() ? $yes : $no, + ); + } + + $template_table = id(new AphrontTableView($rows)) + ->setNoDataString( + pht('None of the fields on this object support templating.')) + ->setHeaders( + array( + pht('Field'), + pht('Will Copy'), + )) + ->setColumnClasses( + array( + 'pri', + 'wide', + )); + $select_text = pht(<<getOptions(); $label = $field->getLabel(); foreach ($options as $option_key => $option_value) { if (strlen($option_key)) { $option_display = $option_key; } else { $option_display = phutil_tag('em', array(), pht('')); } $rows[] = array( $label, $option_display, $option_value, ); $label = null; } } $select_table = id(new AphrontTableView($rows)) ->setNoDataString(pht('This object has no select fields.')) ->setHeaders( array( pht('Field'), pht('Value'), pht('Label'), )) ->setColumnClasses( array( 'pri', null, 'wide', )); $types_text = pht(<<setHTTPParameterTypes($types); return array( $this->renderInstructions($intro), $main_table, $this->renderInstructions($aliases_text), $alias_table, + $this->renderInstructions($template_text), + $template_table, $this->renderInstructions($select_text), $select_table, $this->renderInstructions($types_text), $types_table, ); } protected function renderInstructions($corpus) { $viewer = $this->getUser(); return new PHUIRemarkupView($viewer, $corpus); } } diff --git a/src/docs/user/configuration/custom_fields.diviner b/src/docs/user/configuration/custom_fields.diviner index 9bd1bb0e28..ecb7382648 100644 --- a/src/docs/user/configuration/custom_fields.diviner +++ b/src/docs/user/configuration/custom_fields.diviner @@ -1,219 +1,212 @@ @title Configuring Custom Fields @group config How to add custom fields to applications which support them. = Overview = Several Phabricator applications allow the configuration of custom fields. These fields allow you to add more information to objects, and in some cases reorder or remove builtin fields. For example, you could use custom fields to add an "Estimated Hours" field to tasks, a "Lead" field to projects, or a "T-Shirt Size" field to users. These applications currently support custom fields: | Application | Support | |-------------|---------| | Differential | Partial Support | | Diffusion | Limited Support | | Maniphest | Full Support | | Owners | Full Support | | People | Full Support | | Projects | Full Support | Custom fields can appear in many interfaces and support search, editing, and other features. = Basic Custom Fields = To get started with custom fields, you can use configuration to select and reorder fields and to add new simple fields. If you don't need complicated display controls or sophisticated validation, these simple fields should cover most use cases. They allow you to attach things like strings, numbers, and dropdown menus to objects. The relevant configuration settings are: | Application | Add Fields | Select Fields | |-------------|------------|---------------| | Differential | Planned | `differential.fields` | | Diffusion | Planned | Planned | | Maniphest | `maniphest.custom-field-definitions` | `maniphest.fields` | | Owners | `owners.custom-field-definitions` | `owners.fields` | | People | `user.custom-field-definitions` | `user.fields` | | Projects | `projects.custom-field-definitions` | `projects.fields` | When adding fields, you'll specify a JSON blob like this (for example, as the value of `maniphest.custom-field-definitions`): { "mycompany:estimated-hours": { "name": "Estimated Hours", "type": "int", "caption": "Estimated number of hours this will take.", "required": true }, "mycompany:actual-hours": { "name": "Actual Hours", "type": "int", "caption": "Actual number of hours this took." }, "mycompany:company-jobs": { "name": "Job Role", "type": "select", "options": { "mycompany:engineer": "Engineer", "mycompany:nonengineer": "Other" } }, "mycompany:favorite-dinosaur": { "name": "Favorite Dinosaur", "type": "text" } } The fields will then appear in the other config option for the application (for example, in `maniphest.fields`) and you can enable, disable, or reorder them. For details on how to define a field, see the next section. = Custom Field Configuration = When defining custom fields using a configuration option like `maniphest.custom-field-definitions`, these options are available: - **name**: Display label for the field on the edit and detail interfaces. - **description**: Optional text shown when managing the field. - **type**: Field type. The supported field types are: - **int**: An integer, rendered as a text field. - **text**: A string, rendered as a text field. - **bool**: A boolean value, rendered as a checkbox. - **select**: Allows the user to select from several options as defined by **options**, rendered as a dropdown. - **remarkup**: A text area which allows the user to enter markup. - **users**: A typeahead which allows multiple users to be input. - **date**: A date/time picker. - **header**: Renders a visual divider which you can use to group fields. - **link**: A text field which allows the user to enter a link. - **edit**: Show this field on the application's edit interface (this defaults to `true`). - **view**: Show this field on the application's view interface (this defaults to `true`). (Note: Empty fields are not shown.) - **search**: Show this field on the application's search interface, allowing users to filter objects by the field value. - **fulltext**: Index the text in this field as part of the object's global full-text index. This allows users to find the object by searching for the field's contents using global search. - **caption**: A caption to display underneath the field (optional). - **required**: True if the user should be required to provide a value. - **options**: If type is set to **select**, provide options for the dropdown as a dictionary. - **default**: Default field value. - **strings**: Allows you to override specific strings based on the field type. See below. - **instructions**: Optional block of remarkup text which will appear above the control when rendered on the edit view. - **placeholder**: A placeholder text that appears on text boxes. Only supported in text, int and remarkup fields (optional). + - **copy**: If true, this field's value will be copied when an object is + created using another object as a template. The `strings` value supports different strings per control type. They are: - **bool** - **edit.checkbox** Text for the edit interface, no default. - **view.yes** Text for the view interface, defaults to "Yes". - **search.default** Text for the search interface, defaults to "(Any)". - **search.require** Text for the search interface, defaults to "Require". -Some applications have specific options which only work in that application. - -In **Maniphest**: - - - **copy**: When a user creates a task, the UI gives them an option to - "Create Another Similar Task". Some fields from the original task are copied - into the new task, while others are not; by default, fields are not copied. - If you want this field to be copied, specify `true` for the `copy` property. - Internally, Phabricator implements some additional custom field types and options. These are not intended for general use and are subject to abrupt change, but are documented here for completeness: - **Credentials**: Controls with type `credential` allow selection of a Passphrase credential which provides `credential.provides`, and creation of credentials of `credential.type`. - **Datasource**: Controls with type `datasource` allow selection of tokens from an arbitrary datasource, controlled with `datasource.class` and `datasource.parameters`. = Advanced Custom Fields = If you want custom fields to have advanced behaviors (sophisticated rendering, advanced validation, complicated controls, interaction with other systems, etc), you can write a custom field as an extension and add it to Phabricator. NOTE: This API is somewhat new and fairly large. You should expect that there will be occasional changes to the API requiring minor updates in your code. To do this, extend the appropriate `CustomField` class for the application you want to add a field to: | Application | Extend | |-------------|---------| | Differential | @{class:DifferentialCustomField} | | Diffusion | @{class:PhabricatorCommitCustomField} | | Maniphest | @{class:ManiphestCustomField} | | Owners | @{class:PhabricatorOwnersCustomField} | | People | @{class:PhabricatorUserCustomField} | | Projects | @{class:PhabricatorProjectCustomField} | The easiest way to get started is to drop your subclass into `phabricator/src/extensions/`. If Phabricator is configured in development mode, the class should immediately be available in the UI. If not, you can restart Phabricator (for help, see @{article:Restarting Phabricator}). For example, this is a simple template which adds a custom field to Maniphest: name=ExampleManiphestCustomField.php 'color: #ff00ff', ), pht('It worked!')); } } Broadly, you can then add features by overriding more methods and implementing them. Many of the native fields are implemented on the custom field architecture, and it may be useful to look at them. For details on available integrations, see the base class for your application and @{class:PhabricatorCustomField}. = Next Steps = Continue by: - learning more about extending Phabricator with custom code in @{article@phabcontrib:Adding New Classes}; - or returning to the @{article: Configuration Guide}. diff --git a/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditEngineExtension.php b/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditEngineExtension.php index 07618aab67..b9427905b8 100644 --- a/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditEngineExtension.php +++ b/src/infrastructure/customfield/editor/PhabricatorCustomFieldEditEngineExtension.php @@ -1,53 +1,53 @@ getViewer(); $field_list = PhabricatorCustomField::getObjectFields( $object, PhabricatorCustomField::ROLE_EDIT); $field_list->setViewer($viewer); - if (!$engine->getIsCreate()) { + if ($object->getID()) { $field_list->readFieldsFromStorage($object); } $results = array(); foreach ($field_list->getFields() as $field) { $edit_fields = $field->getEditEngineFields($engine); foreach ($edit_fields as $edit_field) { $results[] = $edit_field; } } return $results; } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php index 1e081f5cfe..37c22b8ca3 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php @@ -1,440 +1,454 @@ setAncestorClass(__CLASS__) ->setUniqueMethod('getFieldType') ->execute(); $fields = array(); foreach ($config as $key => $value) { $type = idx($value, 'type', 'text'); if (empty($types[$type])) { // TODO: We should have better typechecking somewhere, and then make // this more serious. continue; } $namespace = $template->getStandardCustomFieldNamespace(); $full_key = "std:{$namespace}:{$key}"; $template = clone $template; $standard = id(clone $types[$type]) ->setRawStandardFieldKey($key) ->setFieldKey($full_key) ->setFieldConfig($value) ->setApplicationField($template); $field = $template->setProxy($standard); $fields[] = $field; } return $fields; } public function setApplicationField( PhabricatorStandardCustomFieldInterface $application_field) { $this->applicationField = $application_field; return $this; } public function getApplicationField() { return $this->applicationField; } public function setFieldName($name) { $this->fieldName = $name; return $this; } public function getFieldValue() { return $this->fieldValue; } public function setFieldValue($value) { $this->fieldValue = $value; return $this; } public function setCaption($caption) { $this->caption = $caption; return $this; } public function getCaption() { return $this->caption; } public function setFieldDescription($description) { $this->fieldDescription = $description; return $this; } public function setFieldConfig(array $config) { foreach ($config as $key => $value) { switch ($key) { case 'name': $this->setFieldName($value); break; case 'description': $this->setFieldDescription($value); break; case 'strings': $this->setStrings($value); break; case 'caption': $this->setCaption($value); break; case 'required': if ($value) { $this->setRequired($value); $this->setFieldError(true); } break; case 'default': $this->setFieldValue($value); break; + case 'copy': + $this->setIsCopyable($value); + break; case 'type': // We set this earlier on. break; } } $this->fieldConfig = $config; return $this; } public function getFieldConfigValue($key, $default = null) { return idx($this->fieldConfig, $key, $default); } public function setFieldError($field_error) { $this->fieldError = $field_error; return $this; } public function getFieldError() { return $this->fieldError; } public function setRequired($required) { $this->required = $required; return $this; } public function getRequired() { return $this->required; } public function setRawStandardFieldKey($raw_key) { $this->rawKey = $raw_key; return $this; } public function getRawStandardFieldKey() { return $this->rawKey; } /* -( PhabricatorCustomField )--------------------------------------------- */ public function setFieldKey($field_key) { $this->fieldKey = $field_key; return $this; } public function getFieldKey() { return $this->fieldKey; } public function getFieldName() { return coalesce($this->fieldName, parent::getFieldName()); } public function getFieldDescription() { return coalesce($this->fieldDescription, parent::getFieldDescription()); } public function setStrings(array $strings) { $this->strings = $strings; return; } public function getString($key, $default = null) { return idx($this->strings, $key, $default); } + public function setIsCopyable($is_copyable) { + $this->isCopyable = $is_copyable; + return $this; + } + + public function getIsCopyable() { + return $this->isCopyable; + } + public function shouldUseStorage() { try { $object = $this->newStorageObject(); return true; } catch (PhabricatorCustomFieldImplementationIncompleteException $ex) { return false; } } public function getValueForStorage() { return $this->getFieldValue(); } public function setValueFromStorage($value) { return $this->setFieldValue($value); } public function shouldAppearInApplicationTransactions() { return true; } public function shouldAppearInEditView() { return $this->getFieldConfigValue('edit', true); } public function readValueFromRequest(AphrontRequest $request) { $value = $request->getStr($this->getFieldKey()); if (!strlen($value)) { $value = null; } $this->setFieldValue($value); } public function getInstructionsForEdit() { return $this->getFieldConfigValue('instructions'); } public function getPlaceholder() { return $this->getFieldConfigValue('placeholder', null); } public function renderEditControl(array $handles) { return id(new AphrontFormTextControl()) ->setName($this->getFieldKey()) ->setCaption($this->getCaption()) ->setValue($this->getFieldValue()) ->setError($this->getFieldError()) ->setLabel($this->getFieldName()) ->setPlaceholder($this->getPlaceholder()); } public function newStorageObject() { return $this->getApplicationField()->newStorageObject(); } public function shouldAppearInPropertyView() { return $this->getFieldConfigValue('view', true); } public function renderPropertyViewValue(array $handles) { if (!strlen($this->getFieldValue())) { return null; } return $this->getFieldValue(); } public function shouldAppearInApplicationSearch() { return $this->getFieldConfigValue('search', false); } protected function newStringIndexStorage() { return $this->getApplicationField()->newStringIndexStorage(); } protected function newNumericIndexStorage() { return $this->getApplicationField()->newNumericIndexStorage(); } public function buildFieldIndexes() { return array(); } public function buildOrderIndex() { return null; } public function readApplicationSearchValueFromRequest( PhabricatorApplicationSearchEngine $engine, AphrontRequest $request) { return; } public function applyApplicationSearchConstraintToQuery( PhabricatorApplicationSearchEngine $engine, PhabricatorCursorPagedPolicyAwareQuery $query, $value) { return; } public function appendToApplicationSearchForm( PhabricatorApplicationSearchEngine $engine, AphrontFormView $form, $value) { return; } public function validateApplicationTransactions( PhabricatorApplicationTransactionEditor $editor, $type, array $xactions) { $this->setFieldError(null); $errors = parent::validateApplicationTransactions( $editor, $type, $xactions); if ($this->getRequired()) { $value = $this->getOldValueForApplicationTransactions(); $transaction = null; foreach ($xactions as $xaction) { $value = $xaction->getNewValue(); if (!$this->isValueEmpty($value)) { $transaction = $xaction; break; } } if ($this->isValueEmpty($value)) { $error = new PhabricatorApplicationTransactionValidationError( $type, pht('Required'), pht('%s is required.', $this->getFieldName()), $transaction); $error->setIsMissingFieldError(true); $errors[] = $error; $this->setFieldError(pht('Required')); } } return $errors; } protected function isValueEmpty($value) { if (is_array($value)) { return empty($value); } return !strlen($value); } public function getApplicationTransactionTitle( PhabricatorApplicationTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); if (!$old) { return pht( '%s set %s to %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $new); } else if (!$new) { return pht( '%s removed %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName()); } else { return pht( '%s changed %s from %s to %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $old, $new); } } public function getApplicationTransactionTitleForFeed( PhabricatorApplicationTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $object_phid = $xaction->getObjectPHID(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); if (!$old) { return pht( '%s set %s to %s on %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $new, $xaction->renderHandleLink($object_phid)); } else if (!$new) { return pht( '%s removed %s on %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $xaction->renderHandleLink($object_phid)); } else { return pht( '%s changed %s from %s to %s on %s.', $xaction->renderHandleLink($author_phid), $this->getFieldName(), $old, $new, $xaction->renderHandleLink($object_phid)); } } public function getHeraldFieldValue() { return $this->getFieldValue(); } public function getFieldControlID($key = null) { $key = coalesce($key, $this->getRawStandardFieldKey()); return 'std:control:'.$key; } public function shouldAppearInGlobalSearch() { return $this->getFieldConfigValue('fulltext', false); } public function updateAbstractDocument( PhabricatorSearchAbstractDocument $document) { $field_key = $this->getFieldConfigValue('fulltext'); // If the caller or configuration didn't specify a valid field key, // generate one automatically from the field index. if (!is_string($field_key) || (strlen($field_key) != 4)) { $field_key = '!'.substr($this->getFieldIndex(), 0, 3); } $field_value = $this->getFieldValue(); if (strlen($field_value)) { $document->addField($field_key, $field_value); } } protected function newStandardEditField() { $short = 'custom.'.$this->getRawStandardFieldKey(); return parent::newStandardEditField() - ->setEditTypeKey($short); + ->setEditTypeKey($short) + ->setIsCopyable($this->getIsCopyable()); } public function shouldAppearInConduitTransactions() { return true; } }