diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f78f3dd849..9a70610392 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1830,9 +1830,12 @@ phutil_register_library_map(array( 'PhabricatorProjectBoardEditController' => 'applications/project/controller/PhabricatorProjectBoardEditController.php', 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', + 'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php', + 'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php', 'PhabricatorProjectConstants' => 'applications/project/constants/PhabricatorProjectConstants.php', 'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php', 'PhabricatorProjectCreateController' => 'applications/project/controller/PhabricatorProjectCreateController.php', + 'PhabricatorProjectCustomField' => 'applications/project/customfield/PhabricatorProjectCustomField.php', 'PhabricatorProjectCustomFieldNumericIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldNumericIndex.php', 'PhabricatorProjectCustomFieldStorage' => 'applications/project/storage/PhabricatorProjectCustomFieldStorage.php', 'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php', @@ -4557,6 +4560,7 @@ phutil_register_library_map(array( 1 => 'PhabricatorFlaggableInterface', 2 => 'PhabricatorPolicyInterface', 3 => 'PhabricatorSubscribableInterface', + 4 => 'PhabricatorCustomFieldInterface', ), 'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', @@ -4567,8 +4571,15 @@ phutil_register_library_map(array( 1 => 'PhabricatorPolicyInterface', ), 'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions', + 'PhabricatorProjectConfiguredCustomField' => + array( + 0 => 'PhabricatorProjectCustomField', + 1 => 'PhabricatorStandardCustomFieldInterface', + ), 'PhabricatorProjectController' => 'PhabricatorController', 'PhabricatorProjectCreateController' => 'PhabricatorProjectController', + 'PhabricatorProjectCustomField' => 'PhabricatorCustomField', 'PhabricatorProjectCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'PhabricatorProjectCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', diff --git a/src/applications/project/config/PhabricatorProjectConfigOptions.php b/src/applications/project/config/PhabricatorProjectConfigOptions.php new file mode 100644 index 0000000000..4d48cdc9aa --- /dev/null +++ b/src/applications/project/config/PhabricatorProjectConfigOptions.php @@ -0,0 +1,43 @@ + $enabled) { + $default_fields[$key] = array( + 'disabled' => !$enabled, + ); + } + + $custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType'; + + return array( + $this->newOption('projects.custom-field-definitions', 'wild', array()) + ->setSummary(pht('Custom Projects fields.')) + ->setDescription( + pht( + "Array of custom fields for Projects.")) + ->addExample( + '{"mycompany:motto": {"name": "Project Motto", '. + '"type": "string"}}', + pht('Valid Setting')), + $this->newOption('projects.fields', $custom_field_type, $default_fields) + ->setCustomData(id(new PhabricatorProject())->getCustomFieldBaseClass()) + ->setDescription(pht("Select and reorder project fields.")), + ); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 6ba5f666d4..e88ee1f044 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -266,6 +266,11 @@ final class PhabricatorProjectProfileController ? $this->renderHandlesForPHIDs($project->getMemberPHIDs(), ',') : phutil_tag('em', array(), pht('None'))); + $field_list = PhabricatorCustomField::getObjectFields( + $project, + PhabricatorCustomField::ROLE_VIEW); + $field_list->appendFieldsToPropertyList($project, $viewer, $view); + $view->addSectionHeader(pht('Description')); $view->addTextContent( PhabricatorMarkupEngine::renderOneObject( diff --git a/src/applications/project/controller/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/PhabricatorProjectProfileEditController.php index dc31632bec..638401e9f8 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileEditController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileEditController.php @@ -10,12 +10,11 @@ final class PhabricatorProjectProfileEditController } public function processRequest() { - $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); $project = id(new PhabricatorProjectQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIDs(array($this->id)) ->requireCapabilities( array( @@ -30,49 +29,80 @@ final class PhabricatorProjectProfileEditController $profile = $project->getProfile(); - $e_name = true; + $field_list = PhabricatorCustomField::getObjectFields( + $project, + PhabricatorCustomField::ROLE_EDIT); + $field_list + ->setViewer($viewer) + ->readFieldsFromStorage($project); + + $view_uri = $this->getApplicationURI('view/'.$project->getID().'/'); + + $e_name = true; + $e_edit = null; + + $v_name = $project->getName(); + $v_desc = $profile->getBlurb(); + + $validation_exception = null; - $errors = array(); if ($request->isFormPost()) { - $xactions = array(); + $e_name = null; + + $v_name = $request->getStr('name'); + $v_desc = $request->getStr('blurb'); + $v_view = $request->getStr('can_view'); + $v_edit = $request->getStr('can_edit'); + $v_join = $request->getStr('can_join'); + + $xactions = $field_list->buildFieldTransactionsFromRequest( + new PhabricatorProjectTransaction(), + $request); + + $type_name = PhabricatorProjectTransaction::TYPE_NAME; + $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; $xactions[] = id(new PhabricatorProjectTransaction()) - ->setTransactionType(PhabricatorProjectTransaction::TYPE_NAME) + ->setTransactionType($type_name) ->setNewValue($request->getStr('name')); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) - ->setNewValue($request->getStr('can_view')); + ->setNewValue($v_view); $xactions[] = id(new PhabricatorProjectTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) - ->setNewValue($request->getStr('can_edit')); + ->setTransactionType($type_edit) + ->setNewValue($v_edit); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_JOIN_POLICY) - ->setNewValue($request->getStr('can_join')); + ->setNewValue($v_join); $editor = id(new PhabricatorProjectTransactionEditor()) - ->setActor($user) + ->setActor($viewer) ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true) - ->applyTransactions($project, $xactions); + ->setContinueOnNoEffect(true); - $profile->setBlurb($request->getStr('blurb')); + try { + $editor->applyTransactions($project, $xactions); - if (!strlen($project->getName())) { - $e_name = pht('Required'); - $errors[] = pht('Project name is required.'); - } else { - $e_name = null; - } - - if (!$errors) { - $project->save(); - $profile->setProjectPHID($project->getPHID()); + // TODO: Move this into a custom field. + $profile->setBlurb($request->getStr('blurb')); + if (!$profile->getProjectPHID()) { + $profile->setProjectPHID($project->getPHID()); + } $profile->save(); - return id(new AphrontRedirectResponse()) - ->setURI('/project/view/'.$project->getID().'/'); + + return id(new AphrontRedirectResponse())->setURI($view_uri); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + + $e_name = $ex->getShortMessage($type_name); + $e_edit = $ex->getShortMessage($type_edit); + + $project->setViewPolicy($v_view); + $project->setEditPolicy($v_edit); + $project->setJoinPolicy($v_join); } } @@ -81,29 +111,33 @@ final class PhabricatorProjectProfileEditController $action = '/project/edit/'.$project->getID().'/'; $policies = id(new PhabricatorPolicyQuery()) - ->setViewer($user) + ->setViewer($viewer) ->setObject($project) ->execute(); $form = new AphrontFormView(); $form ->setID('project-edit-form') - ->setUser($user) + ->setUser($viewer) ->setAction($action) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Name')) ->setName('name') - ->setValue($project->getName()) + ->setValue($v_name) ->setError($e_name)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel(pht('Description')) ->setName('blurb') - ->setValue($profile->getBlurb())) + ->setValue($v_desc)); + + $field_list->appendFieldsToForm($form); + + $form ->appendChild( id(new AphrontFormPolicyControl()) - ->setUser($user) + ->setUser($viewer) ->setName('can_view') ->setCaption(pht('Members can always view a project.')) ->setPolicyObject($project) @@ -111,14 +145,15 @@ final class PhabricatorProjectProfileEditController ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) ->appendChild( id(new AphrontFormPolicyControl()) - ->setUser($user) + ->setUser($viewer) ->setName('can_edit') ->setPolicyObject($project) ->setPolicies($policies) - ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) + ->setError($e_edit)) ->appendChild( id(new AphrontFormPolicyControl()) - ->setUser($user) + ->setUser($viewer) ->setName('can_join') ->setCaption( pht('Users who can edit a project can always join a project.')) @@ -127,18 +162,16 @@ final class PhabricatorProjectProfileEditController ->setCapability(PhabricatorPolicyCapability::CAN_JOIN)) ->appendChild( id(new AphrontFormSubmitControl()) - ->addCancelButton('/project/view/'.$project->getID().'/') + ->addCancelButton($view_uri) ->setValue(pht('Save'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) - ->setFormErrors($errors) + ->setValidationException($validation_exception) ->setForm($form); $crumbs = $this->buildApplicationCrumbs($this->buildSideNavView()) - ->addTextCrumb( - $project->getName(), - '/project/view/'.$project->getID().'/') + ->addTextCrumb($project->getName(), $view_uri) ->addTextCrumb(pht('Edit Project'), $this->getApplicationURI()); return $this->buildApplicationPage( diff --git a/src/applications/project/customfield/PhabricatorProjectConfiguredCustomField.php b/src/applications/project/customfield/PhabricatorProjectConfiguredCustomField.php new file mode 100644 index 0000000000..b4aec137ac --- /dev/null +++ b/src/applications/project/customfield/PhabricatorProjectConfiguredCustomField.php @@ -0,0 +1,31 @@ +memberPHIDs) { return 'GROUP BY p.id'; } else { - return ''; + return $this->buildApplicationSearchGroupClause($conn_r); } } @@ -270,6 +270,8 @@ final class PhabricatorProjectQuery PhabricatorEdgeConfig::TYPE_PROJ_MEMBER); } + $joins[] = $this->buildApplicationSearchJoinClause($conn_r); + return implode(' ', $joins); } @@ -278,4 +280,8 @@ final class PhabricatorProjectQuery return 'PhabricatorApplicationProject'; } + protected function getApplicationSearchObjectPHIDColumn() { + return 'p.phid'; + } + } diff --git a/src/applications/project/query/PhabricatorProjectSearchEngine.php b/src/applications/project/query/PhabricatorProjectSearchEngine.php index f10129bb29..8d485b2931 100644 --- a/src/applications/project/query/PhabricatorProjectSearchEngine.php +++ b/src/applications/project/query/PhabricatorProjectSearchEngine.php @@ -3,6 +3,10 @@ final class PhabricatorProjectSearchEngine extends PhabricatorApplicationSearchEngine { + public function getCustomFieldObject() { + return new PhabricatorProject(); + } + public function buildSavedQueryFromRequest(AphrontRequest $request) { $saved = new PhabricatorSavedQuery(); @@ -11,6 +15,8 @@ final class PhabricatorProjectSearchEngine $this->readUsersFromRequest($request, 'members')); $saved->setParameter('status', $request->getStr('status')); + $this->readCustomFieldsFromRequest($request, $saved); + return $saved; } @@ -28,20 +34,22 @@ final class PhabricatorProjectSearchEngine $query->withStatus($status); } + $this->applyCustomFieldsToQuery($query, $saved); + return $query; } public function buildSearchForm( AphrontFormView $form, - PhabricatorSavedQuery $saved_query) { + PhabricatorSavedQuery $saved) { - $phids = $saved_query->getParameter('memberPHIDs', array()); + $phids = $saved->getParameter('memberPHIDs', array()); $member_handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->withPHIDs($phids) ->execute(); - $status = $saved_query->getParameter('status'); + $status = $saved->getParameter('status'); $form ->appendChild( @@ -56,6 +64,8 @@ final class PhabricatorProjectSearchEngine ->setName('status') ->setOptions($this->getStatusOptions()) ->setValue($status)); + + $this->appendCustomFieldsToForm($form, $saved); } protected function getURI($path) { diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index f496109791..b7dd56ecd5 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -4,7 +4,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO implements PhabricatorFlaggableInterface, PhabricatorPolicyInterface, - PhabricatorSubscribableInterface { + PhabricatorSubscribableInterface, + PhabricatorCustomFieldInterface { protected $name; protected $status = PhabricatorProjectStatus::STATUS_ACTIVE; @@ -19,6 +20,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO private $memberPHIDs = self::ATTACHABLE; private $sparseMembers = self::ATTACHABLE; private $profile = self::ATTACHABLE; + private $customFields = self::ATTACHABLE; public static function initializeNewProject(PhabricatorUser $actor) { return id(new PhabricatorProject()) @@ -168,4 +170,25 @@ final class PhabricatorProject extends PhabricatorProjectDAO } +/* -( PhabricatorCustomFieldInterface )------------------------------------ */ + + + public function getCustomFieldSpecificationForRole($role) { + return PhabricatorEnv::getEnvConfig('projects.fields'); + } + + public function getCustomFieldBaseClass() { + return 'PhabricatorProjectCustomField'; + } + + public function getCustomFields() { + return $this->assertAttached($this->customFields); + } + + public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { + $this->customFields = $fields; + return $this; + } + + }