Page MenuHomePhabricator

D14904.diff
No OneTemporary

D14904.diff

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 @@
+<?php
+
+final class PhabricatorProjectListView extends AphrontView {
+
+ private $projects;
+
+ public function setProjects(array $projects) {
+ $this->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;
}

File Metadata

Mime Type
text/plain
Expires
May 13 2024, 10:38 PM (4 w, 5 d ago)
Storage Engine
amazon-s3
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
phabricator/secure/q7/bd/4cy6iiyabid3li2r
Default Alt Text
D14904.diff (32 KB)

Event Timeline