@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Implement custom fields in Projects

Summary:
Ref T4379. Ref T3794. Fixes T4010. This brings CustomFields to projects.

My primary goal is to get rid of the special casing around project profiles and profile editing, so all edits are ApplicationTransactions. Particularly, I want to make the "blurb/description" field a custom field which goes through builtin infrastructure.

A distant secondary goal is that this is a feature which users like/want because users like/want features.

Test Plan: Added a custom field and examined it in the edit, view, and search interfaces.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T3794, T4010, T4379

Differential Revision: https://secure.phabricator.com/D8180

+213 -44
+11
src/__phutil_library_map__.php
··· 1830 1830 'PhabricatorProjectBoardEditController' => 'applications/project/controller/PhabricatorProjectBoardEditController.php', 1831 1831 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 1832 1832 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', 1833 + 'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php', 1834 + 'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php', 1833 1835 'PhabricatorProjectConstants' => 'applications/project/constants/PhabricatorProjectConstants.php', 1834 1836 'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php', 1835 1837 'PhabricatorProjectCreateController' => 'applications/project/controller/PhabricatorProjectCreateController.php', 1838 + 'PhabricatorProjectCustomField' => 'applications/project/customfield/PhabricatorProjectCustomField.php', 1836 1839 'PhabricatorProjectCustomFieldNumericIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldNumericIndex.php', 1837 1840 'PhabricatorProjectCustomFieldStorage' => 'applications/project/storage/PhabricatorProjectCustomFieldStorage.php', 1838 1841 'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php', ··· 4557 4560 1 => 'PhabricatorFlaggableInterface', 4558 4561 2 => 'PhabricatorPolicyInterface', 4559 4562 3 => 'PhabricatorSubscribableInterface', 4563 + 4 => 'PhabricatorCustomFieldInterface', 4560 4564 ), 4561 4565 'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', 4562 4566 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', ··· 4567 4571 1 => 'PhabricatorPolicyInterface', 4568 4572 ), 4569 4573 'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 4574 + 'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions', 4575 + 'PhabricatorProjectConfiguredCustomField' => 4576 + array( 4577 + 0 => 'PhabricatorProjectCustomField', 4578 + 1 => 'PhabricatorStandardCustomFieldInterface', 4579 + ), 4570 4580 'PhabricatorProjectController' => 'PhabricatorController', 4571 4581 'PhabricatorProjectCreateController' => 'PhabricatorProjectController', 4582 + 'PhabricatorProjectCustomField' => 'PhabricatorCustomField', 4572 4583 'PhabricatorProjectCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 4573 4584 'PhabricatorProjectCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 4574 4585 'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage',
+43
src/applications/project/config/PhabricatorProjectConfigOptions.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectConfigOptions 4 + extends PhabricatorApplicationConfigOptions { 5 + 6 + public function getName() { 7 + return pht("Projects"); 8 + } 9 + 10 + public function getDescription() { 11 + return pht("Configure Projects."); 12 + } 13 + 14 + public function getOptions() { 15 + // This is intentionally blank for now, until we can move more Project 16 + // logic to custom fields. 17 + $default_fields = array(); 18 + 19 + foreach ($default_fields as $key => $enabled) { 20 + $default_fields[$key] = array( 21 + 'disabled' => !$enabled, 22 + ); 23 + } 24 + 25 + $custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType'; 26 + 27 + return array( 28 + $this->newOption('projects.custom-field-definitions', 'wild', array()) 29 + ->setSummary(pht('Custom Projects fields.')) 30 + ->setDescription( 31 + pht( 32 + "Array of custom fields for Projects.")) 33 + ->addExample( 34 + '{"mycompany:motto": {"name": "Project Motto", '. 35 + '"type": "string"}}', 36 + pht('Valid Setting')), 37 + $this->newOption('projects.fields', $custom_field_type, $default_fields) 38 + ->setCustomData(id(new PhabricatorProject())->getCustomFieldBaseClass()) 39 + ->setDescription(pht("Select and reorder project fields.")), 40 + ); 41 + } 42 + 43 + }
+5
src/applications/project/controller/PhabricatorProjectProfileController.php
··· 266 266 ? $this->renderHandlesForPHIDs($project->getMemberPHIDs(), ',') 267 267 : phutil_tag('em', array(), pht('None'))); 268 268 269 + $field_list = PhabricatorCustomField::getObjectFields( 270 + $project, 271 + PhabricatorCustomField::ROLE_VIEW); 272 + $field_list->appendFieldsToPropertyList($project, $viewer, $view); 273 + 269 274 $view->addSectionHeader(pht('Description')); 270 275 $view->addTextContent( 271 276 PhabricatorMarkupEngine::renderOneObject(
+72 -39
src/applications/project/controller/PhabricatorProjectProfileEditController.php
··· 10 10 } 11 11 12 12 public function processRequest() { 13 - 14 13 $request = $this->getRequest(); 15 - $user = $request->getUser(); 14 + $viewer = $request->getUser(); 16 15 17 16 $project = id(new PhabricatorProjectQuery()) 18 - ->setViewer($user) 17 + ->setViewer($viewer) 19 18 ->withIDs(array($this->id)) 20 19 ->requireCapabilities( 21 20 array( ··· 29 28 } 30 29 31 30 $profile = $project->getProfile(); 31 + 32 + $field_list = PhabricatorCustomField::getObjectFields( 33 + $project, 34 + PhabricatorCustomField::ROLE_EDIT); 35 + $field_list 36 + ->setViewer($viewer) 37 + ->readFieldsFromStorage($project); 38 + 39 + $view_uri = $this->getApplicationURI('view/'.$project->getID().'/'); 32 40 33 41 $e_name = true; 42 + $e_edit = null; 43 + 44 + $v_name = $project->getName(); 45 + $v_desc = $profile->getBlurb(); 34 46 35 - $errors = array(); 47 + $validation_exception = null; 48 + 36 49 if ($request->isFormPost()) { 37 - $xactions = array(); 50 + $e_name = null; 51 + 52 + $v_name = $request->getStr('name'); 53 + $v_desc = $request->getStr('blurb'); 54 + $v_view = $request->getStr('can_view'); 55 + $v_edit = $request->getStr('can_edit'); 56 + $v_join = $request->getStr('can_join'); 57 + 58 + $xactions = $field_list->buildFieldTransactionsFromRequest( 59 + new PhabricatorProjectTransaction(), 60 + $request); 61 + 62 + $type_name = PhabricatorProjectTransaction::TYPE_NAME; 63 + $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; 38 64 39 65 $xactions[] = id(new PhabricatorProjectTransaction()) 40 - ->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME) 66 + ->setTransactionType($type_name) 41 67 ->setNewValue($request->getStr('name')); 42 68 43 69 $xactions[] = id(new PhabricatorProjectTransaction()) 44 70 ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) 45 - ->setNewValue($request->getStr('can_view')); 71 + ->setNewValue($v_view); 46 72 47 73 $xactions[] = id(new PhabricatorProjectTransaction()) 48 - ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) 49 - ->setNewValue($request->getStr('can_edit')); 74 + ->setTransactionType($type_edit) 75 + ->setNewValue($v_edit); 50 76 51 77 $xactions[] = id(new PhabricatorProjectTransaction()) 52 78 ->setTransactionType(PhabricatorTransactions::TYPE_JOIN_POLICY) 53 - ->setNewValue($request->getStr('can_join')); 79 + ->setNewValue($v_join); 54 80 55 81 $editor = id(new PhabricatorProjectTransactionEditor()) 56 - ->setActor($user) 82 + ->setActor($viewer) 57 83 ->setContentSourceFromRequest($request) 58 - ->setContinueOnNoEffect(true) 59 - ->applyTransactions($project, $xactions); 84 + ->setContinueOnNoEffect(true); 85 + 86 + try { 87 + $editor->applyTransactions($project, $xactions); 88 + 89 + // TODO: Move this into a custom field. 90 + $profile->setBlurb($request->getStr('blurb')); 91 + if (!$profile->getProjectPHID()) { 92 + $profile->setProjectPHID($project->getPHID()); 93 + } 94 + $profile->save(); 60 95 61 - $profile->setBlurb($request->getStr('blurb')); 96 + return id(new AphrontRedirectResponse())->setURI($view_uri); 97 + } catch (PhabricatorApplicationTransactionValidationException $ex) { 98 + $validation_exception = $ex; 62 99 63 - if (!strlen($project->getName())) { 64 - $e_name = pht('Required'); 65 - $errors[] = pht('Project name is required.'); 66 - } else { 67 - $e_name = null; 68 - } 100 + $e_name = $ex->getShortMessage($type_name); 101 + $e_edit = $ex->getShortMessage($type_edit); 69 102 70 - if (!$errors) { 71 - $project->save(); 72 - $profile->setProjectPHID($project->getPHID()); 73 - $profile->save(); 74 - return id(new AphrontRedirectResponse()) 75 - ->setURI('/project/view/'.$project->getID().'/'); 103 + $project->setViewPolicy($v_view); 104 + $project->setEditPolicy($v_edit); 105 + $project->setJoinPolicy($v_join); 76 106 } 77 107 } 78 108 ··· 81 111 $action = '/project/edit/'.$project->getID().'/'; 82 112 83 113 $policies = id(new PhabricatorPolicyQuery()) 84 - ->setViewer($user) 114 + ->setViewer($viewer) 85 115 ->setObject($project) 86 116 ->execute(); 87 117 88 118 $form = new AphrontFormView(); 89 119 $form 90 120 ->setID('project-edit-form') 91 - ->setUser($user) 121 + ->setUser($viewer) 92 122 ->setAction($action) 93 123 ->appendChild( 94 124 id(new AphrontFormTextControl()) 95 125 ->setLabel(pht('Name')) 96 126 ->setName('name') 97 - ->setValue($project->getName()) 127 + ->setValue($v_name) 98 128 ->setError($e_name)) 99 129 ->appendChild( 100 130 id(new PhabricatorRemarkupControl()) 101 131 ->setLabel(pht('Description')) 102 132 ->setName('blurb') 103 - ->setValue($profile->getBlurb())) 133 + ->setValue($v_desc)); 134 + 135 + $field_list->appendFieldsToForm($form); 136 + 137 + $form 104 138 ->appendChild( 105 139 id(new AphrontFormPolicyControl()) 106 - ->setUser($user) 140 + ->setUser($viewer) 107 141 ->setName('can_view') 108 142 ->setCaption(pht('Members can always view a project.')) 109 143 ->setPolicyObject($project) ··· 111 145 ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) 112 146 ->appendChild( 113 147 id(new AphrontFormPolicyControl()) 114 - ->setUser($user) 148 + ->setUser($viewer) 115 149 ->setName('can_edit') 116 150 ->setPolicyObject($project) 117 151 ->setPolicies($policies) 118 - ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)) 152 + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) 153 + ->setError($e_edit)) 119 154 ->appendChild( 120 155 id(new AphrontFormPolicyControl()) 121 - ->setUser($user) 156 + ->setUser($viewer) 122 157 ->setName('can_join') 123 158 ->setCaption( 124 159 pht('Users who can edit a project can always join a project.')) ··· 127 162 ->setCapability(PhabricatorPolicyCapability::CAN_JOIN)) 128 163 ->appendChild( 129 164 id(new AphrontFormSubmitControl()) 130 - ->addCancelButton('/project/view/'.$project->getID().'/') 165 + ->addCancelButton($view_uri) 131 166 ->setValue(pht('Save'))); 132 167 133 168 $form_box = id(new PHUIObjectBoxView()) 134 169 ->setHeaderText($title) 135 - ->setFormErrors($errors) 170 + ->setValidationException($validation_exception) 136 171 ->setForm($form); 137 172 138 173 $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()) 139 - ->addTextCrumb( 140 - $project->getName(), 141 - '/project/view/'.$project->getID().'/') 174 + ->addTextCrumb($project->getName(), $view_uri) 142 175 ->addTextCrumb(pht('Edit Project'), $this->getApplicationURI()); 143 176 144 177 return $this->buildApplicationPage(
+31
src/applications/project/customfield/PhabricatorProjectConfiguredCustomField.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectConfiguredCustomField 4 + extends PhabricatorProjectCustomField 5 + implements PhabricatorStandardCustomFieldInterface { 6 + 7 + public function getStandardCustomFieldNamespace() { 8 + return 'project'; 9 + } 10 + 11 + public function createFields() { 12 + return PhabricatorStandardCustomField::buildStandardFields( 13 + $this, 14 + PhabricatorEnv::getEnvConfig( 15 + 'projects.custom-field-definitions', 16 + array())); 17 + } 18 + 19 + public function newStorageObject() { 20 + return new PhabricatorProjectCustomFieldStorage(); 21 + } 22 + 23 + protected function newStringIndexStorage() { 24 + return new PhabricatorProjectCustomFieldStringIndex(); 25 + } 26 + 27 + protected function newNumericIndexStorage() { 28 + return new PhabricatorProjectCustomFieldNumericIndex(); 29 + } 30 + 31 + }
+7
src/applications/project/customfield/PhabricatorProjectCustomField.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorProjectCustomField 4 + extends PhabricatorCustomField { 5 + 6 + 7 + }
+7 -1
src/applications/project/query/PhabricatorProjectQuery.php
··· 246 246 if ($this->memberPHIDs) { 247 247 return 'GROUP BY p.id'; 248 248 } else { 249 - return ''; 249 + return $this->buildApplicationSearchGroupClause($conn_r); 250 250 } 251 251 } 252 252 ··· 270 270 PhabricatorEdgeConfig::TYPE_PROJ_MEMBER); 271 271 } 272 272 273 + $joins[] = $this->buildApplicationSearchJoinClause($conn_r); 274 + 273 275 return implode(' ', $joins); 274 276 } 275 277 276 278 277 279 public function getQueryApplicationClass() { 278 280 return 'PhabricatorApplicationProject'; 281 + } 282 + 283 + protected function getApplicationSearchObjectPHIDColumn() { 284 + return 'p.phid'; 279 285 } 280 286 281 287 }
+13 -3
src/applications/project/query/PhabricatorProjectSearchEngine.php
··· 3 3 final class PhabricatorProjectSearchEngine 4 4 extends PhabricatorApplicationSearchEngine { 5 5 6 + public function getCustomFieldObject() { 7 + return new PhabricatorProject(); 8 + } 9 + 6 10 public function buildSavedQueryFromRequest(AphrontRequest $request) { 7 11 $saved = new PhabricatorSavedQuery(); 8 12 ··· 10 14 'memberPHIDs', 11 15 $this->readUsersFromRequest($request, 'members')); 12 16 $saved->setParameter('status', $request->getStr('status')); 17 + 18 + $this->readCustomFieldsFromRequest($request, $saved); 13 19 14 20 return $saved; 15 21 } ··· 27 33 if ($status) { 28 34 $query->withStatus($status); 29 35 } 36 + 37 + $this->applyCustomFieldsToQuery($query, $saved); 30 38 31 39 return $query; 32 40 } 33 41 34 42 public function buildSearchForm( 35 43 AphrontFormView $form, 36 - PhabricatorSavedQuery $saved_query) { 44 + PhabricatorSavedQuery $saved) { 37 45 38 - $phids = $saved_query->getParameter('memberPHIDs', array()); 46 + $phids = $saved->getParameter('memberPHIDs', array()); 39 47 $member_handles = id(new PhabricatorHandleQuery()) 40 48 ->setViewer($this->requireViewer()) 41 49 ->withPHIDs($phids) 42 50 ->execute(); 43 51 44 - $status = $saved_query->getParameter('status'); 52 + $status = $saved->getParameter('status'); 45 53 46 54 $form 47 55 ->appendChild( ··· 56 64 ->setName('status') 57 65 ->setOptions($this->getStatusOptions()) 58 66 ->setValue($status)); 67 + 68 + $this->appendCustomFieldsToForm($form, $saved); 59 69 } 60 70 61 71 protected function getURI($path) {
+24 -1
src/applications/project/storage/PhabricatorProject.php
··· 4 4 implements 5 5 PhabricatorFlaggableInterface, 6 6 PhabricatorPolicyInterface, 7 - PhabricatorSubscribableInterface { 7 + PhabricatorSubscribableInterface, 8 + PhabricatorCustomFieldInterface { 8 9 9 10 protected $name; 10 11 protected $status = PhabricatorProjectStatus::STATUS_ACTIVE; ··· 19 20 private $memberPHIDs = self::ATTACHABLE; 20 21 private $sparseMembers = self::ATTACHABLE; 21 22 private $profile = self::ATTACHABLE; 23 + private $customFields = self::ATTACHABLE; 22 24 23 25 public static function initializeNewProject(PhabricatorUser $actor) { 24 26 return id(new PhabricatorProject()) ··· 165 167 166 168 public function shouldAllowSubscription($phid) { 167 169 return false; 170 + } 171 + 172 + 173 + /* -( PhabricatorCustomFieldInterface )------------------------------------ */ 174 + 175 + 176 + public function getCustomFieldSpecificationForRole($role) { 177 + return PhabricatorEnv::getEnvConfig('projects.fields'); 178 + } 179 + 180 + public function getCustomFieldBaseClass() { 181 + return 'PhabricatorProjectCustomField'; 182 + } 183 + 184 + public function getCustomFields() { 185 + return $this->assertAttached($this->customFields); 186 + } 187 + 188 + public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { 189 + $this->customFields = $fields; 190 + return $this; 168 191 } 169 192 170 193