diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'a419cf4b', + 'core.pkg.css' => '3ea6dc33', 'core.pkg.js' => '57dff7df', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -114,7 +114,7 @@ 'rsrc/css/font/phui-font-icon-base.css' => 'ecbbb4c2', 'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82', 'rsrc/css/layout/phabricator-hovercard-view.css' => '1239cd52', - 'rsrc/css/layout/phabricator-side-menu-view.css' => 'bec2458e', + 'rsrc/css/layout/phabricator-side-menu-view.css' => '91b7a42c', 'rsrc/css/layout/phabricator-source-code-view.css' => 'cbeef983', 'rsrc/css/phui/calendar/phui-calendar-day.css' => 'd1cf6f93', 'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1c7f338', @@ -762,7 +762,7 @@ 'phabricator-remarkup-css' => '7afb543c', 'phabricator-search-results-css' => '7dea472c', 'phabricator-shaped-request' => '7cbe244b', - 'phabricator-side-menu-view-css' => 'bec2458e', + 'phabricator-side-menu-view-css' => '91b7a42c', 'phabricator-slowvote-css' => 'da0afb1b', 'phabricator-source-code-view-css' => 'cbeef983', 'phabricator-standard-page-view' => '3c99cdf4', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2855,6 +2855,7 @@ 'PhabricatorProjectIconSet' => 'applications/project/icon/PhabricatorProjectIconSet.php', 'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php', 'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php', + 'PhabricatorProjectListView' => 'applications/project/view/PhabricatorProjectListView.php', 'PhabricatorProjectLockController' => 'applications/project/controller/PhabricatorProjectLockController.php', 'PhabricatorProjectLogicalAndDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php', 'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php', @@ -7206,6 +7207,7 @@ 'PhabricatorProjectHeraldAction' => 'HeraldAction', 'PhabricatorProjectIconSet' => 'PhabricatorIconSet', 'PhabricatorProjectListController' => 'PhabricatorProjectController', + 'PhabricatorProjectListView' => 'AphrontView', 'PhabricatorProjectLockController' => 'PhabricatorProjectController', 'PhabricatorProjectLogicalAndDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource', diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php --- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php +++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php @@ -748,15 +748,15 @@ ->setNewValue($name); if ($parent) { - $xactions[] = id(new PhabricatorProjectTransaction()) - ->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT) - ->setNewValue($parent->getPHID()); - } - - if ($is_milestone) { - $xactions[] = id(new PhabricatorProjectTransaction()) - ->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE) - ->setNewValue(true); + if ($is_milestone) { + $xactions[] = id(new PhabricatorProjectTransaction()) + ->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE) + ->setNewValue($parent->getPHID()); + } else { + $xactions[] = id(new PhabricatorProjectTransaction()) + ->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT) + ->setNewValue($parent->getPHID()); + } } $this->applyTransactions($project, $user, $xactions); diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -99,7 +99,6 @@ $nav->addFilter("board/{$id}/", pht('Workboard')); $nav->addFilter("members/{$id}/", pht('Members')); $nav->addFilter("feed/{$id}/", pht('Feed')); - $nav->addFilter("details/{$id}/", pht('Edit Details')); } $nav->addFilter('create', pht('Create Project')); } @@ -149,11 +148,29 @@ $nav->addIcon("feed/{$id}/", pht('Feed'), 'fa-newspaper-o'); $nav->addIcon("members/{$id}/", pht('Members'), 'fa-group'); - $nav->addIcon("details/{$id}/", pht('Edit Details'), 'fa-pencil'); if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { - $nav->addIcon("subprojects/{$id}/", pht('Subprojects'), 'fa-sitemap'); - $nav->addIcon("milestones/{$id}/", pht('Milestones'), 'fa-map-marker'); + if ($project->supportsSubprojects()) { + $subprojects_icon = 'fa-sitemap'; + } else { + $subprojects_icon = 'fa-sitemap grey'; + } + + if ($project->supportsMilestones()) { + $milestones_icon = 'fa-map-marker'; + } else { + $milestones_icon = 'fa-map-marker grey'; + } + + $nav->addIcon( + "subprojects/{$id}/", + pht('Subprojects'), + $subprojects_icon); + + $nav->addIcon( + "milestones/{$id}/", + pht('Milestones'), + $milestones_icon); } @@ -170,8 +187,8 @@ $ancestors[] = $project; foreach ($ancestors as $ancestor) { $crumbs->addTextCrumb( - $project->getName(), - $project->getURI()); + $ancestor->getName(), + $ancestor->getURI()); } } diff --git a/src/applications/project/controller/PhabricatorProjectEditController.php b/src/applications/project/controller/PhabricatorProjectEditController.php --- a/src/applications/project/controller/PhabricatorProjectEditController.php +++ b/src/applications/project/controller/PhabricatorProjectEditController.php @@ -3,10 +3,111 @@ final class PhabricatorProjectEditController extends PhabricatorProjectController { + private $engine; + + public function setEngine(PhabricatorProjectEditEngine $engine) { + $this->engine = $engine; + return $this; + } + + public function getEngine() { + return $this->engine; + } + public function handleRequest(AphrontRequest $request) { - return id(new PhabricatorProjectEditEngine()) - ->setController($this) - ->buildResponse(); + $viewer = $this->getViewer(); + + $engine = id(new PhabricatorProjectEditEngine()) + ->setController($this); + + $this->setEngine($engine); + + $id = $request->getURIData('id'); + if (!$id) { + $parent_id = head($request->getArr('parent')); + if (!$parent_id) { + $parent_id = $request->getStr('parent'); + } + + if ($parent_id) { + $is_milestone = false; + } else { + $parent_id = head($request->getArr('milestone')); + if (!$parent_id) { + $parent_id = $request->getStr('milestone'); + } + $is_milestone = true; + } + + if ($parent_id) { + $query = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )); + + if (ctype_digit($parent_id)) { + $query->withIDs(array($parent_id)); + } else { + $query->withPHIDs(array($parent_id)); + } + + $parent = $query->executeOne(); + + if ($is_milestone) { + if (!$parent->supportsMilestones()) { + $cancel_uri = "/project/milestones/{$parent_id}/"; + return $this->newDialog() + ->setTitle(pht('No Milestones')) + ->appendParagraph( + pht('You can not add milestones to this project.')) + ->addCancelButton($cancel_uri); + } + $engine->setMilestoneProject($parent); + } else { + if (!$parent->supportsSubprojects()) { + $cancel_uri = "/project/subprojects/{$parent_id}/"; + return $this->newDialog() + ->setTitle(pht('No Subprojects')) + ->appendParagraph( + pht('You can not add subprojects to this project.')) + ->addCancelButton($cancel_uri); + } + $engine->setParentProject($parent); + } + + $this->setProject($parent); + } + } + + return $engine->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $engine = $this->getEngine(); + if ($engine) { + $parent = $engine->getParentProject(); + if ($parent) { + $id = $parent->getID(); + $crumbs->addTextCrumb( + pht('Subprojects'), + $this->getApplicationURI("subprojects/{$id}/")); + } + + $milestone = $engine->getMilestoneProject(); + if ($milestone) { + $id = $milestone->getID(); + $crumbs->addTextCrumb( + pht('Milestones'), + $this->getApplicationURI("milestones/{$id}/")); + } + } + + return $crumbs; } } diff --git a/src/applications/project/controller/PhabricatorProjectMembersEditController.php b/src/applications/project/controller/PhabricatorProjectMembersEditController.php --- a/src/applications/project/controller/PhabricatorProjectMembersEditController.php +++ b/src/applications/project/controller/PhabricatorProjectMembersEditController.php @@ -68,9 +68,11 @@ $project, PhabricatorPolicyCapability::CAN_EDIT); + $supports_edit = $project->supportsEditMembers(); + $form_box = null; $title = pht('Add Members'); - if ($can_edit) { + if ($can_edit && $supports_edit) { $header_name = pht('Edit Members'); $view_uri = $this->getApplicationURI('profile/'.$project->getID().'/'); diff --git a/src/applications/project/controller/PhabricatorProjectMilestonesController.php b/src/applications/project/controller/PhabricatorProjectMilestonesController.php --- a/src/applications/project/controller/PhabricatorProjectMilestonesController.php +++ b/src/applications/project/controller/PhabricatorProjectMilestonesController.php @@ -18,6 +18,64 @@ $project = $this->getProject(); $id = $project->getID(); + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + + $has_support = $project->supportsMilestones(); + if ($has_support) { + $milestones = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withParentProjectPHIDs(array($project->getPHID())) + ->needImages(true) + ->withIsMilestone(true) + ->setOrder('newest') + ->execute(); + } else { + $milestones = array(); + } + + $can_create = $can_edit && $has_support; + + if ($project->getHasMilestones()) { + $button_text = pht('Create Next Milestone'); + } else { + $button_text = pht('Add Milestones'); + } + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Milestones')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setHref("/project/edit/?milestone={$id}") + ->setIconFont('fa-plus') + ->setDisabled(!$can_create) + ->setWorkflow(!$can_create) + ->setText($button_text)); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header); + + if (!$has_support) { + $no_support = pht( + 'This project is a milestone. Milestones can not have their own '. + 'milestones.'); + + $info_view = id(new PHUIInfoView()) + ->setErrors(array($no_support)) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING); + + $box->setInfoView($info_view); + } + + $box->setObjectList( + id(new PhabricatorProjectListView()) + ->setUser($viewer) + ->setProjects($milestones) + ->renderList()); + $nav = $this->buildIconNavView($project); $nav->selectFilter("milestones/{$id}/"); @@ -27,7 +85,8 @@ return $this->newPage() ->setNavigation($nav) ->setCrumbs($crumbs) - ->setTitle(array($project->getName(), pht('Milestones'))); + ->setTitle(array($project->getName(), pht('Milestones'))) + ->appendChild($box); } } diff --git a/src/applications/project/controller/PhabricatorProjectSubprojectsController.php b/src/applications/project/controller/PhabricatorProjectSubprojectsController.php --- a/src/applications/project/controller/PhabricatorProjectSubprojectsController.php +++ b/src/applications/project/controller/PhabricatorProjectSubprojectsController.php @@ -18,6 +18,63 @@ $project = $this->getProject(); $id = $project->getID(); + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + + $has_support = $project->supportsSubprojects(); + + if ($has_support) { + $subprojects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withParentProjectPHIDs(array($project->getPHID())) + ->needImages(true) + ->withIsMilestone(false) + ->execute(); + } else { + $subprojects = array(); + } + + $can_create = $can_edit && $has_support; + + if ($project->getHasSubprojects()) { + $button_text = pht('Create Subproject'); + } else { + $button_text = pht('Add Subprojects'); + } + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Subprojects')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setHref("/project/edit/?parent={$id}") + ->setIconFont('fa-plus') + ->setDisabled(!$can_create) + ->setWorkflow(!$can_create) + ->setText($button_text)); + + $box = id(new PHUIObjectBoxView()) + ->setHeader($header); + + if (!$has_support) { + $no_support = pht( + 'This project is a milestone. Milestones can not have subprojects.'); + + $info_view = id(new PHUIInfoView()) + ->setErrors(array($no_support)) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING); + + $box->setInfoView($info_view); + } + + $box->setObjectList( + id(new PhabricatorProjectListView()) + ->setUser($viewer) + ->setProjects($subprojects) + ->renderList()); + $nav = $this->buildIconNavView($project); $nav->selectFilter("subprojects/{$id}/"); @@ -27,7 +84,8 @@ return $this->newPage() ->setNavigation($nav) ->setCrumbs($crumbs) - ->setTitle(array($project->getName(), pht('Subprojects'))); + ->setTitle(array($project->getName(), pht('Subprojects'))) + ->appendChild($box); } } diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -74,23 +74,10 @@ case PhabricatorProjectTransaction::TYPE_COLOR: case PhabricatorProjectTransaction::TYPE_LOCKED: case PhabricatorProjectTransaction::TYPE_PARENT: + case PhabricatorProjectTransaction::TYPE_MILESTONE: return $xaction->getNewValue(); case PhabricatorProjectTransaction::TYPE_SLUGS: return $this->normalizeSlugs($xaction->getNewValue()); - case PhabricatorProjectTransaction::TYPE_MILESTONE: - $current = queryfx_one( - $object->establishConnection('w'), - 'SELECT MAX(milestoneNumber) n - FROM %T - WHERE parentProjectPHID = %s', - $object->getTableName(), - $object->getParentProject()->getPHID()); - if (!$current) { - $number = 1; - } else { - $number = (int)$current['n'] + 1; - } - return $number; } return parent::getCustomTransactionNewValue($object, $xaction); @@ -127,7 +114,21 @@ $object->setParentProjectPHID($xaction->getNewValue()); return; case PhabricatorProjectTransaction::TYPE_MILESTONE: - $object->setMilestoneNumber($xaction->getNewValue()); + $current = queryfx_one( + $object->establishConnection('w'), + 'SELECT MAX(milestoneNumber) n + FROM %T + WHERE parentProjectPHID = %s', + $object->getTableName(), + $object->getParentProject()->getPHID()); + if (!$current) { + $number = 1; + } else { + $number = (int)$current['n'] + 1; + } + + $object->setMilestoneNumber($number); + $object->setParentProjectPHID($xaction->getNewValue()); return; } @@ -239,6 +240,84 @@ return parent::applyBuiltinExternalTransaction($object, $xaction); } + protected function validateAllTransactions( + PhabricatorLiskDAO $object, + array $xactions) { + + $errors = array(); + + // Prevent creating projects which are both subprojects and milestones, + // since this does not make sense, won't work, and will break everything. + $parent_xaction = null; + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case PhabricatorProjectTransaction::TYPE_PARENT: + case PhabricatorProjectTransaction::TYPE_MILESTONE: + if ($xaction->getNewValue() === null) { + continue; + } + + if (!$parent_xaction) { + $parent_xaction = $xaction; + continue; + } + + $errors[] = new PhabricatorApplicationTransactionValidationError( + $xaction->getTransactionType(), + pht('Invalid'), + pht( + 'When creating a project, specify a maximum of one parent '. + 'project or milestone project. A project can not be both a '. + 'subproject and a milestone.'), + $xaction); + break; + break; + } + } + + $is_milestone = $object->isMilestone(); + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case PhabricatorProjectTransaction::TYPE_MILESTONE: + if ($xaction->getNewValue() !== null) { + $is_milestone = true; + } + break; + } + } + + $is_parent = $object->getHasSubprojects(); + + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case PhabricatorProjectTransaction::TYPE_MEMBERS: + if ($is_parent) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $xaction->getTransactionType(), + pht('Invalid'), + pht( + 'You can not change members of a project with subprojects '. + 'directly. Members of any subproject are automatically '. + 'members of the parent project.'), + $xaction); + } + + if ($is_milestone) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $xaction->getTransactionType(), + pht('Invalid'), + pht( + 'You can not change members of a milestone. Members of the '. + 'parent project are automatically members of the milestone.'), + $xaction); + } + break; + } + } + + return $errors; + } + protected function validateTransaction( PhabricatorLiskDAO $object, $type, @@ -367,25 +446,29 @@ break; case PhabricatorProjectTransaction::TYPE_PARENT: + case PhabricatorProjectTransaction::TYPE_MILESTONE: if (!$xactions) { break; } $xaction = last($xactions); + $parent_phid = $xaction->getNewValue(); + if (!$parent_phid) { + continue; + } + if (!$this->getIsNewObject()) { $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), pht( - 'You can only set a parent project when creating a project '. - 'for the first time.'), + 'You can only set a parent or milestone project when creating a '. + 'project for the first time.'), $xaction); break; } - $parent_phid = $xaction->getNewValue(); - $projects = id(new PhabricatorProjectQuery()) ->setViewer($this->requireActor()) ->withPHIDs(array($parent_phid)) @@ -400,8 +483,8 @@ $type, pht('Invalid'), pht( - 'Parent project PHID ("%s") must be the PHID of a valid, '. - 'visible project which you have permission to edit.', + 'Parent or milestone project PHID ("%s") must be the PHID of a '. + 'valid, visible project which you have permission to edit.', $parent_phid), $xaction); break; @@ -414,8 +497,8 @@ $type, pht('Invalid'), pht( - 'Parent project PHID ("%s") must not be a milestone. '. - 'Milestones may not have subprojects.', + 'Parent or milestone project PHID ("%s") must not be a '. + 'milestone. Milestones may not have subprojects or milestones.', $parent_phid), $xaction); break; @@ -427,9 +510,9 @@ $type, pht('Invalid'), pht( - 'You can not create a subproject under this parent because '. - 'it would nest projects too deeply. The maximum nesting '. - 'depth of projects is %s.', + 'You can not create a subproject or mielstone under this parent '. + 'because it would nest projects too deeply. The maximum '. + 'nesting depth of projects is %s.', new PhutilNumber($limit)), $xaction); break; @@ -611,6 +694,7 @@ array $xactions) { $materialize = false; + $new_parent = null; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorTransactions::TYPE_EDGE: @@ -622,10 +706,34 @@ break; case PhabricatorProjectTransaction::TYPE_PARENT: $materialize = true; + $new_parent = $object->getParentProject(); break; } } + if ($new_parent) { + // If we just created the first subproject of this parent, we want to + // copy all of the real members to the subproject. + if (!$new_parent->getHasSubprojects()) { + $member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; + + $project_members = PhabricatorEdgeQuery::loadDestinationPHIDs( + $new_parent->getPHID(), + $member_type); + + if ($project_members) { + $editor = id(new PhabricatorEdgeEditor()); + foreach ($project_members as $phid) { + $editor->addEdge($object->getPHID(), $member_type, $phid); + } + $editor->save(); + } + } + } + + // TODO: We should dump an informational transaction onto the parent + // project to show that we created the sub-thing. + if ($materialize) { id(new PhabricatorProjectsMembershipIndexEngineExtension()) ->rematerialize($object); diff --git a/src/applications/project/engine/PhabricatorProjectEditEngine.php b/src/applications/project/engine/PhabricatorProjectEditEngine.php --- a/src/applications/project/engine/PhabricatorProjectEditEngine.php +++ b/src/applications/project/engine/PhabricatorProjectEditEngine.php @@ -5,6 +5,27 @@ const ENGINECONST = 'projects.project'; + private $parentProject; + private $milestoneProject; + + public function setParentProject(PhabricatorProject $parent_project) { + $this->parentProject = $parent_project; + return $this; + } + + public function getParentProject() { + return $this->parentProject; + } + + public function setMilestoneProject(PhabricatorProject $milestone_project) { + $this->milestoneProject = $milestone_project; + return $this; + } + + public function getMilestoneProject() { + return $this->milestoneProject; + } + public function getEngineName() { return pht('Projects'); } @@ -50,6 +71,22 @@ return $object->getURI(); } + protected function getObjectCreateCancelURI($object) { + $parent = $this->getParentProject(); + if ($parent) { + $id = $parent->getID(); + return "/project/subprojects/{$id}/"; + } + + $milestone = $this->getMilestoneProject(); + if ($milestone) { + $id = $milestone->getID(); + return "/project/milestones/{$id}/"; + } + + return parent::getObjectCreateCancelURI($object); + } + protected function getCreateNewObjectPolicy() { return $this->getApplication()->getPolicy( ProjectCreateProjectsCapability::CAPABILITY); @@ -65,6 +102,8 @@ $configuration ->setFieldOrder( array( + 'parent', + 'milestone', 'name', 'std:project:internal:description', 'icon', @@ -84,7 +123,52 @@ unset($slugs[$object->getPrimarySlug()]); $slugs = array_values($slugs); + $milestone = $this->getMilestoneProject(); + $parent = $this->getParentProject(); + + if ($parent) { + $parent_phid = $parent->getPHID(); + } else { + $parent_phid = null; + } + + if ($milestone) { + $milestone_phid = $milestone->getPHID(); + } else { + $milestone_phid = null; + } + return array( + id(new PhabricatorHandlesEditField()) + ->setKey('parent') + ->setLabel(pht('Parent')) + ->setDescription(pht('Create a subproject of an existing project.')) + ->setConduitDescription( + pht('Choose a parent project to create a subproject beneath.')) + ->setConduitTypeDescription(pht('PHID of the parent project.')) + ->setAliases(array('parentPHID')) + ->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT) + ->setHandleParameterType(new AphrontPHIDHTTPParameterType()) + ->setSingleValue($parent_phid) + ->setIsReorderable(false) + ->setIsDefaultable(false) + ->setIsLockable(false) + ->setIsLocked(true), + id(new PhabricatorHandlesEditField()) + ->setKey('milestone') + ->setLabel(pht('Milestone Of')) + ->setDescription(pht('Parent project to create a milestone for.')) + ->setConduitDescription( + pht('Choose a parent project to create a new milestone for.')) + ->setConduitTypeDescription(pht('PHID of the parent project.')) + ->setAliases(array('milestonePHID')) + ->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE) + ->setHandleParameterType(new AphrontPHIDHTTPParameterType()) + ->setSingleValue($milestone_phid) + ->setIsReorderable(false) + ->setIsDefaultable(false) + ->setIsLockable(false) + ->setIsLocked(true), id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) diff --git a/src/applications/project/query/PhabricatorProjectSearchEngine.php b/src/applications/project/query/PhabricatorProjectSearchEngine.php --- a/src/applications/project/query/PhabricatorProjectSearchEngine.php +++ b/src/applications/project/query/PhabricatorProjectSearchEngine.php @@ -164,42 +164,15 @@ array $handles) { assert_instances_of($projects, 'PhabricatorProject'); $viewer = $this->requireViewer(); - $handles = $viewer->loadHandles(mpull($projects, 'getPHID')); - - $list = new PHUIObjectItemListView(); - $list->setUser($viewer); - $can_edit_projects = id(new PhabricatorPolicyFilter()) - ->setViewer($viewer) - ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) - ->apply($projects); - - foreach ($projects as $key => $project) { - $id = $project->getID(); - - $tag_list = id(new PHUIHandleTagListView()) - ->setSlim(true) - ->setHandles(array($handles[$project->getPHID()])); - - $item = id(new PHUIObjectItemView()) - ->setHeader($project->getName()) - ->setHref($this->getApplicationURI("view/{$id}/")) - ->setImageURI($project->getProfileImageURI()) - ->addAttribute($tag_list); - - if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) { - $item->addIcon('delete-grey', pht('Archived')); - $item->setDisabled(true); - } - - $list->addItem($item); - } - - $result = new PhabricatorApplicationSearchResultView(); - $result->setObjectList($list); - $result->setNoDataString(pht('No projects found.')); - return $result; + $list = id(new PhabricatorProjectListView()) + ->setUser($viewer) + ->setProjects($projects) + ->renderList(); + return id(new PhabricatorApplicationSearchResultView()) + ->setObjectList($list) + ->setNoDataString(pht('No projects found.')); } protected function getNewUserBody() { diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -88,6 +88,10 @@ } public function getPolicy($capability) { + if ($this->isMilestone()) { + return $this->getParentProject()->getPolicy($capability); + } + switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); @@ -99,6 +103,12 @@ } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + if ($this->isMilestone()) { + return $this->getParentProject()->hasAutomaticCapability( + $capability, + $viewer); + } + $can_edit = PhabricatorPolicyCapability::CAN_EDIT; switch ($capability) { @@ -437,6 +447,34 @@ return $ancestors; } + public function supportsEditMembers() { + if ($this->isMilestone()) { + return false; + } + + if ($this->getHasSubprojects()) { + return false; + } + + return true; + } + + public function supportsMilestones() { + if ($this->isMilestone()) { + return false; + } + + return true; + } + + public function supportsSubprojects() { + if ($this->isMilestone()) { + return false; + } + + return true; + } + /* -( PhabricatorSubscribableInterface )----------------------------------- */ diff --git a/src/applications/project/view/PhabricatorProjectListView.php b/src/applications/project/view/PhabricatorProjectListView.php new file mode 100644 --- /dev/null +++ b/src/applications/project/view/PhabricatorProjectListView.php @@ -0,0 +1,53 @@ +projects = $projects; + return $this; + } + + public function getProjects() { + return $this->projects; + } + + public function renderList() { + $viewer = $this->getUser(); + $projects = $this->getProjects(); + + $handles = $viewer->loadHandles(mpull($projects, 'getPHID')); + + $list = id(new PHUIObjectItemListView()) + ->setUser($viewer); + + foreach ($projects as $key => $project) { + $id = $project->getID(); + + $tag_list = id(new PHUIHandleTagListView()) + ->setSlim(true) + ->setHandles(array($handles[$project->getPHID()])); + + $item = id(new PHUIObjectItemView()) + ->setHeader($project->getName()) + ->setHref("/project/view/{$id}/") + ->setImageURI($project->getProfileImageURI()) + ->addAttribute($tag_list); + + if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) { + $item->addIcon('delete-grey', pht('Archived')); + $item->setDisabled(true); + } + + $list->addItem($item); + } + + return $list; + } + + public function render() { + return $this->renderList(); + } + +} diff --git a/webroot/rsrc/css/layout/phabricator-side-menu-view.css b/webroot/rsrc/css/layout/phabricator-side-menu-view.css --- a/webroot/rsrc/css/layout/phabricator-side-menu-view.css +++ b/webroot/rsrc/css/layout/phabricator-side-menu-view.css @@ -89,6 +89,10 @@ color: {$blue}; } +.phabricator-icon-nav .phabricator-side-menu .phui-list-item-icon.grey { + color: {$lightgreyborder}; +} + .phabricator-icon-nav .phabricator-side-menu .phui-list-item-selected { border: none; }