Page MenuHomePhabricator

D15152.id.diff
No OneTemporary

D15152.id.diff

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
@@ -2913,7 +2913,6 @@
'PhabricatorProjectMembersProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectMembersProfilePanel.php',
'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php',
'PhabricatorProjectMembersViewController' => 'applications/project/controller/PhabricatorProjectMembersViewController.php',
- 'PhabricatorProjectMilestonesController' => 'applications/project/controller/PhabricatorProjectMilestonesController.php',
'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php',
'PhabricatorProjectNameContextFreeGrammar' => 'applications/project/lipsum/PhabricatorProjectNameContextFreeGrammar.php',
'PhabricatorProjectNoProjectsDatasource' => 'applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php',
@@ -2937,7 +2936,9 @@
'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php',
'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php',
'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php',
+ 'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php',
'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php',
+ 'PhabricatorProjectSubprojectsProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php',
'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php',
'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php',
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
@@ -7330,7 +7331,6 @@
'PhabricatorProjectMembersProfilePanel' => 'PhabricatorProfilePanel',
'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController',
'PhabricatorProjectMembersViewController' => 'PhabricatorProjectController',
- 'PhabricatorProjectMilestonesController' => 'PhabricatorProjectController',
'PhabricatorProjectMoveController' => 'PhabricatorProjectController',
'PhabricatorProjectNameContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource',
@@ -7357,7 +7357,9 @@
'PhabricatorStandardCustomFieldInterface',
),
'PhabricatorProjectStatus' => 'Phobject',
+ 'PhabricatorProjectSubprojectWarningController' => 'PhabricatorProjectController',
'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController',
+ 'PhabricatorProjectSubprojectsProfilePanel' => 'PhabricatorProfilePanel',
'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator',
'PhabricatorProjectTransaction' => 'PhabricatorApplicationTransaction',
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php
--- a/src/applications/project/application/PhabricatorProjectApplication.php
+++ b/src/applications/project/application/PhabricatorProjectApplication.php
@@ -91,6 +91,8 @@
=> 'PhabricatorProjectWatchController',
'silence/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectSilenceController',
+ 'warning/(?P<id>[1-9]\d*)/'
+ => 'PhabricatorProjectSubprojectWarningController',
),
'/tag/' => array(
'(?P<slug>[^/]+)/' => 'PhabricatorProjectViewController',
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
@@ -42,6 +42,7 @@
if ($parent_id) {
$query = id(new PhabricatorProjectQuery())
->setViewer($viewer)
+ ->needImages(true)
->requireCapabilities(
array(
PhabricatorPolicyCapability::CAN_VIEW,
@@ -58,7 +59,7 @@
if ($is_milestone) {
if (!$parent->supportsMilestones()) {
- $cancel_uri = "/project/milestones/{$parent_id}/";
+ $cancel_uri = "/project/subprojects/{$parent_id}/";
return $this->newDialog()
->setTitle(pht('No Milestones'))
->appendParagraph(
@@ -91,20 +92,13 @@
$engine = $this->getEngine();
if ($engine) {
$parent = $engine->getParentProject();
- if ($parent) {
- $id = $parent->getID();
+ $milestone = $engine->getMilestoneProject();
+ if ($parent || $milestone) {
+ $id = nonempty($parent, $milestone)->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/PhabricatorProjectMilestonesController.php b/src/applications/project/controller/PhabricatorProjectMilestonesController.php
deleted file mode 100644
--- a/src/applications/project/controller/PhabricatorProjectMilestonesController.php
+++ /dev/null
@@ -1,92 +0,0 @@
-<?php
-
-final class PhabricatorProjectMilestonesController
- extends PhabricatorProjectController {
-
- public function shouldAllowPublic() {
- return true;
- }
-
- public function handleRequest(AphrontRequest $request) {
- $viewer = $request->getViewer();
-
- $response = $this->loadProject();
- if ($response) {
- return $response;
- }
-
- $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}")
- ->setIcon('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->getProfileMenu();
- $nav->selectFilter(PhabricatorProject::PANEL_MILESTONES);
-
- $crumbs = $this->buildApplicationCrumbs();
- $crumbs->addTextCrumb(pht('Milestones'));
-
- return $this->newPage()
- ->setNavigation($nav)
- ->setCrumbs($crumbs)
- ->setTitle(array($project->getName(), pht('Milestones')))
- ->appendChild($box);
- }
-
-}
diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php
--- a/src/applications/project/controller/PhabricatorProjectProfileController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileController.php
@@ -45,6 +45,9 @@
$watch_action = $this->renderWatchAction($project);
$header->addActionLink($watch_action);
+ $milestone_list = $this->buildMilestoneList($project);
+ $subproject_list = $this->buildSubprojectList($project);
+
$member_list = id(new PhabricatorProjectMemberListView())
->setUser($viewer)
->setProject($project)
@@ -82,6 +85,8 @@
))
->setSideColumn(
array(
+ $milestone_list,
+ $subproject_list,
$member_list,
$watcher_list,
));
@@ -176,5 +181,90 @@
->setHref($watch_href);
}
+ private function buildMilestoneList(PhabricatorProject $project) {
+ if (!$project->getHasMilestones()) {
+ return null;
+ }
+
+ $viewer = $this->getViewer();
+ $id = $project->getID();
+
+ $milestones = id(new PhabricatorProjectQuery())
+ ->setViewer($viewer)
+ ->withParentProjectPHIDs(array($project->getPHID()))
+ ->needImages(true)
+ ->withIsMilestone(true)
+ ->setOrder('newest')
+ ->execute();
+ if (!$milestones) {
+ return null;
+ }
+
+ $milestone_list = id(new PhabricatorProjectListView())
+ ->setUser($viewer)
+ ->setProjects($milestones)
+ ->renderList();
+
+ $view_all = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon(
+ id(new PHUIIconView())
+ ->setIcon('fa-list-ul'))
+ ->setText(pht('View All'))
+ ->setHref("/project/subprojects/{$id}/");
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Milestones'))
+ ->addActionLink($view_all);
+
+ return id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->setBackground(PHUIBoxView::GREY)
+ ->setObjectList($milestone_list);
+ }
+
+ private function buildSubprojectList(PhabricatorProject $project) {
+ if (!$project->getHasSubprojects()) {
+ return null;
+ }
+
+ $viewer = $this->getViewer();
+ $id = $project->getID();
+
+ $limit = 25;
+
+ $subprojects = id(new PhabricatorProjectQuery())
+ ->setViewer($viewer)
+ ->withParentProjectPHIDs(array($project->getPHID()))
+ ->needImages(true)
+ ->withIsMilestone(false)
+ ->setLimit($limit)
+ ->execute();
+ if (!$subprojects) {
+ return null;
+ }
+
+ $subproject_list = id(new PhabricatorProjectListView())
+ ->setUser($viewer)
+ ->setProjects($subprojects)
+ ->renderList();
+
+ $view_all = id(new PHUIButtonView())
+ ->setTag('a')
+ ->setIcon(
+ id(new PHUIIconView())
+ ->setIcon('fa-list-ul'))
+ ->setText(pht('View All'))
+ ->setHref("/project/subprojects/{$id}/");
+
+ $header = id(new PHUIHeaderView())
+ ->setHeader(pht('Subprojects'))
+ ->addActionLink($view_all);
+
+ return id(new PHUIObjectBoxView())
+ ->setHeader($header)
+ ->setBackground(PHUIBoxView::GREY)
+ ->setObjectList($subproject_list);
+ }
}
diff --git a/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php b/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php
@@ -0,0 +1,51 @@
+<?php
+
+final class PhabricatorProjectSubprojectWarningController
+ extends PhabricatorProjectController {
+
+ public function handleRequest(AphrontRequest $request) {
+ $viewer = $request->getViewer();
+
+ $response = $this->loadProject();
+ if ($response) {
+ return $response;
+ }
+
+ $project = $this->getProject();
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $project,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ if (!$can_edit) {
+ return new Aphront404Response();
+ }
+
+ $id = $project->getID();
+ $cancel_uri = "/project/subprojects/{$id}/";
+ $done_uri = "/project/edit/?parent={$id}";
+
+ if ($request->isFormPost()) {
+ return id(new AphrontRedirectResponse())
+ ->setURI($done_uri);
+ }
+
+ $doc_href = PhabricatorEnv::getDoclink('Projects User Guide');
+
+ $conversion_help = pht(
+ "Creating a project's first subproject **moves all ".
+ "members** and **destroys all workboard columns**.".
+ "\n\n".
+ "See [[ %s | Projects User Guide ]] in the documentation for details. ".
+ "This process can not be undone.",
+ $doc_href);
+
+ return $this->newDialog()
+ ->setTitle(pht('Convert to Parent Project'))
+ ->appendChild(new PHUIRemarkupView($viewer, $conversion_help))
+ ->addCancelButton($cancel_uri)
+ ->addSubmitButton(pht('Convert Project'));
+ }
+
+}
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
@@ -23,9 +23,10 @@
$project,
PhabricatorPolicyCapability::CAN_EDIT);
- $has_support = $project->supportsSubprojects();
+ $allows_subprojects = $project->supportsSubprojects();
+ $allows_milestones = $project->supportsMilestones();
- if ($has_support) {
+ if ($allows_subprojects) {
$subprojects = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->withParentProjectPHIDs(array($project->getPHID()))
@@ -36,44 +37,57 @@
$subprojects = array();
}
- $can_create = $can_edit && $has_support;
+ if ($allows_milestones) {
+ $milestones = id(new PhabricatorProjectQuery())
+ ->setViewer($viewer)
+ ->withParentProjectPHIDs(array($project->getPHID()))
+ ->needImages(true)
+ ->withIsMilestone(true)
+ ->setOrder('newest')
+ ->execute();
+ } else {
+ $milestones = array();
+ }
- if ($project->getHasSubprojects()) {
- $button_text = pht('Create Subproject');
+ if ($milestones) {
+ $milestone_list = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Milestones'))
+ ->setObjectList(
+ id(new PhabricatorProjectListView())
+ ->setUser($viewer)
+ ->setProjects($milestones)
+ ->renderList());
} else {
- $button_text = pht('Add Subprojects');
+ $milestone_list = null;
}
- $header = id(new PHUIHeaderView())
- ->setHeader(pht('Subprojects'))
- ->addActionLink(
- id(new PHUIButtonView())
- ->setTag('a')
- ->setHref("/project/edit/?parent={$id}")
- ->setIcon('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);
+ if ($subprojects) {
+ $subproject_list = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Subprojects'))
+ ->setObjectList(
+ id(new PhabricatorProjectListView())
+ ->setUser($viewer)
+ ->setProjects($subprojects)
+ ->renderList());
+ } else {
+ $subproject_list = null;
}
- $box->setObjectList(
- id(new PhabricatorProjectListView())
- ->setUser($viewer)
- ->setProjects($subprojects)
- ->renderList());
+ $property_list = $this->buildPropertyList(
+ $project,
+ $milestones,
+ $subprojects);
+
+ $action_list = $this->buildActionList(
+ $project,
+ $milestones,
+ $subprojects);
+
+ $property_list->setActionList($action_list);
+
+ $header_box = id(new PHUIObjectBoxView())
+ ->setHeaderText(pht('Subprojects and Milestones'))
+ ->addPropertyList($property_list);
$nav = $this->getProfileMenu();
$nav->selectFilter(PhabricatorProject::PANEL_SUBPROJECTS);
@@ -85,7 +99,151 @@
->setNavigation($nav)
->setCrumbs($crumbs)
->setTitle(array($project->getName(), pht('Subprojects')))
- ->appendChild($box);
+ ->appendChild(
+ array(
+ $header_box,
+ $milestone_list,
+ $subproject_list,
+ ));
}
+ private function buildPropertyList(
+ PhabricatorProject $project,
+ array $milestones,
+ array $subprojects) {
+ $viewer = $this->getViewer();
+
+ $view = id(new PHUIPropertyListView())
+ ->setUser($viewer);
+
+ $view->addProperty(
+ pht('Prototype'),
+ $this->renderStatus(
+ 'fa-exclamation-triangle red',
+ pht('Warning'),
+ pht('Subprojects and milestones are only partially implemented.')));
+
+ if (!$project->supportsMilestones()) {
+ $milestone_status = $this->renderStatus(
+ 'fa-times grey',
+ pht('Already Milestone'),
+ pht(
+ 'This project is already a milestone, and milestones may not '.
+ 'have their own milestones.'));
+ } else {
+ if (!$milestones) {
+ $milestone_status = $this->renderStatus(
+ 'fa-check grey',
+ pht('None Created'),
+ pht(
+ 'You can create milestones for this project.'));
+ } else {
+ $milestone_status = $this->renderStatus(
+ 'fa-check green',
+ pht('Has Milestones'),
+ pht('This project has milestones.'));
+ }
+ }
+
+ $view->addProperty(pht('Milestones'), $milestone_status);
+
+ if (!$project->supportsSubprojects()) {
+ $subproject_status = $this->renderStatus(
+ 'fa-times grey',
+ pht('Milestone'),
+ pht(
+ 'This project is a milestone, and milestones may not have '.
+ 'subprojects.'));
+ } else {
+ if (!$subprojects) {
+ $subproject_status = $this->renderStatus(
+ 'fa-check grey',
+ pht('None Created'),
+ pht('You can create subprojects for this project.'));
+ } else {
+ $subproject_status = $this->renderStatus(
+ 'fa-check green',
+ pht('Has Subprojects'),
+ pht(
+ 'This project has subprojects.'));
+ }
+ }
+
+ $view->addProperty(pht('Subprojects'), $subproject_status);
+
+ return $view;
+ }
+
+ private function buildActionList(
+ PhabricatorProject $project,
+ array $milestones,
+ array $subprojects) {
+ $viewer = $this->getViewer();
+ $id = $project->getID();
+
+ $can_edit = PhabricatorPolicyFilter::hasCapability(
+ $viewer,
+ $project,
+ PhabricatorPolicyCapability::CAN_EDIT);
+
+ $allows_milestones = $project->supportsMilestones();
+ $allows_subprojects = $project->supportsSubprojects();
+
+ $view = id(new PhabricatorActionListView())
+ ->setUser($viewer);
+
+ if ($allows_milestones && $milestones) {
+ $milestone_text = pht('Create Next Milestone');
+ } else {
+ $milestone_text = pht('Create Milestone');
+ }
+
+ $can_milestone = ($can_edit && $allows_milestones);
+ $milestone_href = "/project/edit/?milestone={$id}";
+
+ $view->addAction(
+ id(new PhabricatorActionView())
+ ->setName($milestone_text)
+ ->setIcon('fa-plus')
+ ->setHref($milestone_href)
+ ->setDisabled(!$can_milestone)
+ ->setWorkflow(!$can_milestone));
+
+ $can_subproject = ($can_edit && $allows_subprojects);
+
+ // If we're offering to create the first subproject, we're going to warn
+ // the user about the effects before moving forward.
+ if ($can_subproject && !$subprojects) {
+ $subproject_href = "/project/warning/{$id}/";
+ $subproject_disabled = false;
+ $subproject_workflow = true;
+ } else {
+ $subproject_href = "/project/edit/?parent={$id}";
+ $subproject_disabled = !$can_subproject;
+ $subproject_workflow = !$can_subproject;
+ }
+
+ $view->addAction(
+ id(new PhabricatorActionView())
+ ->setName(pht('Create Subproject'))
+ ->setIcon('fa-plus')
+ ->setHref($subproject_href)
+ ->setDisabled($subproject_disabled)
+ ->setWorkflow($subproject_workflow));
+
+ return $view;
+ }
+
+ private function renderStatus($icon, $target, $note) {
+ $item = id(new PHUIStatusItemView())
+ ->setIcon($icon)
+ ->setTarget(phutil_tag('strong', array(), $target))
+ ->setNote($note);
+
+ return id(new PHUIStatusListView())
+ ->addItem($item);
+ }
+
+
+
}
diff --git a/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php b/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php
--- a/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php
+++ b/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php
@@ -29,6 +29,10 @@
->setPanelKey(PhabricatorProjectMembersProfilePanel::PANELKEY);
$panels[] = $this->newPanel()
+ ->setBuiltinKey(PhabricatorProject::PANEL_SUBPROJECTS)
+ ->setPanelKey(PhabricatorProjectSubprojectsProfilePanel::PANELKEY);
+
+ $panels[] = $this->newPanel()
->setBuiltinKey(PhabricatorProject::PANEL_MANAGE)
->setPanelKey(PhabricatorProjectManageProfilePanel::PANELKEY);
diff --git a/src/applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php b/src/applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php
--- a/src/applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php
+++ b/src/applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php
@@ -61,6 +61,15 @@
$conn_w = $project->establishConnection('w');
+ $any_milestone = queryfx_one(
+ $conn_w,
+ 'SELECT id FROM %T
+ WHERE parentProjectPHID = %s AND milestoneNumber IS NOT NULL
+ LIMIT 1',
+ $project->getTableName(),
+ $project_phid);
+ $has_milestones = (bool)$any_milestone;
+
$project->openTransaction();
// Delete any existing materialized member edges.
@@ -92,6 +101,14 @@
(int)$has_subprojects,
$project->getID());
+ // Update the hasMilestones flag.
+ queryfx(
+ $conn_w,
+ 'UPDATE %T SET hasMilestones = %d WHERE id = %d',
+ $project->getTableName(),
+ (int)$has_milestones,
+ $project->getID());
+
$project->saveTransaction();
}
diff --git a/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php b/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php
new file mode 100644
--- /dev/null
+++ b/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php
@@ -0,0 +1,63 @@
+<?php
+
+final class PhabricatorProjectSubprojectsProfilePanel
+ extends PhabricatorProfilePanel {
+
+ const PANELKEY = 'project.subprojects';
+
+ public function getPanelTypeName() {
+ return pht('Project Subprojects');
+ }
+
+ private function getDefaultName() {
+ return pht('Subprojects');
+ }
+
+ public function getDisplayName(
+ PhabricatorProfilePanelConfiguration $config) {
+ $name = $config->getPanelProperty('name');
+
+ if (strlen($name)) {
+ return $name;
+ }
+
+ return $this->getDefaultName();
+ }
+
+ public function buildEditEngineFields(
+ PhabricatorProfilePanelConfiguration $config) {
+ return array(
+ id(new PhabricatorTextEditField())
+ ->setKey('name')
+ ->setLabel(pht('Name'))
+ ->setPlaceholder($this->getDefaultName())
+ ->setValue($config->getPanelProperty('name')),
+ );
+ }
+
+ protected function newNavigationMenuItems(
+ PhabricatorProfilePanelConfiguration $config) {
+
+ $project = $config->getProfileObject();
+
+ $has_children = ($project->getHasSubprojects()) ||
+ ($project->getHasMilestones());
+
+ $id = $project->getID();
+
+ $name = $this->getDisplayName($config);
+ $icon = 'fa-sitemap';
+ $href = "/project/subprojects/{$id}/";
+
+ $item = $this->newItem()
+ ->setHref($href)
+ ->setName($name)
+ ->setDisabled(!$has_children)
+ ->setIcon($icon);
+
+ return array(
+ $item,
+ );
+ }
+
+}
diff --git a/src/docs/user/userguide/projects.diviner b/src/docs/user/userguide/projects.diviner
--- a/src/docs/user/userguide/projects.diviner
+++ b/src/docs/user/userguide/projects.diviner
@@ -135,6 +135,118 @@
menu.
+Subprojects and Milestones
+==========================
+
+IMPORTANT: This feature is only partially implemented.
+
+After creating a project, you can use the
+{nav icon="sitemap", name="Subprojects"} menu item to add subprojects or
+milestones.
+
+**Subprojects** are projects that are contained inside the main project. You
+can use them to break large or complex groups, tags, lists, or undertakings
+apart into smaller pieces.
+
+**Milestones** are a special kind of subproject for organizing tasks into
+blocks of work. You can use them to implement sprints, iterations, milestones,
+versions, etc.
+
+Subprojects and milestones have some additional special behaviors and rules,
+particularly around policies and membership. See below for details.
+
+This is a brief summary of the major differences between normal projects,
+subprojects, parent projects, and milestones.
+
+| | Normal | Parent | Subproject | Milestone |
+|---|---|---|---|---|
+| //Members// | Yes | Union of Subprojects | Yes | Same as Parent |
+| //Policies// | Yes | Yes | Affected by Parent | Same as Parent |
+| //Workboard// | Yes | No Custom Columns | Yes | Yes |
+| //Hashtags// | Yes | Yes | Yes | Special |
+
+
+Subprojects
+===========
+
+Subprojects are full-power projects that are contained inside some parent
+project. You can use them to divide a large or complex project into smaller
+parts.
+
+Subprojects have normal members and normal policies, but note that the policies
+of the parent project affect the policies of the subproject (see "Parent
+Projects", below).
+
+Subprojects can have their own subprojects, milestones, or both. If a
+subproject has its own subprojects, it is both a subproject and a parent
+project. Thus, the parent project rules apply to it, and are stronger than the
+subproject rules.
+
+Subprojects can have normal workboards.
+
+
+Milestones
+==========
+
+Milestones are simple subprojects for tracking sprints, iterations, versions,
+or other similar blocks of work. Milestones make it easier to create and manage
+a large number of similar subprojects (for example: {nav Sprint 1},
+{nav Sprint 2}, {nav Sprint 3}, etc).
+
+Milestones can not have direct members or policies. Instead, the membership
+and policies of a milestones are always the same as the milestone's parent
+project. This makes large numbers of milestones more manageable when changes
+occur.
+
+Milestones can not have subprojects, and can not have their own milestones.
+
+By default, Milestones do not have their own hashtags.
+
+Milestones can have normal workboards.
+
+
+Parent Projects
+===============
+
+When you add the first subproject to an existing project, it is converted into
+a **parent project**. Parent projects have some special rules.
+
+**No Direct Members**: Parent projects can not have members of their own.
+Instead, all of the users who are members of any subproject count as members
+of the parent project. By joining (or leaving) a subproject, a user is
+implicitly added to (or removed from) all ancestors of that project.
+
+Consequently, when you add the first subproject to an existing project, all of
+the project's current members are moved to become members of the subproject
+instead. Implicitly, they will remain members of the parent project because the
+parent project is an ancestor of the new subproject.
+
+You can edit the project afterward to change or remove members if you want to
+split membership apart in a more granular way across multiple new subprojects.
+
+**No Workboard Columns**: Parent projects can not have their own workboard
+columns: instead, the workboard of a parent project shows columns representing
+the child projects.
+
+Thus, a project's workboard columns are destroyed when you add the first
+subproject. All objects on the workboard will be returned to the project's
+backlog. The new board will show columns for subprojects instead.
+
+**Searching**: When you search for a parent project, results for any subproject
+are returned. For example, if you search for {nav Engineering}, your query will
+match results in {nav Engineering} itself, but also subprojects like
+{nav Engineering > Warp Drive} and {nav Engineering > Shield Batteries}.
+
+**Policy Effects**: To view a subproject or milestone, you must be able to
+view the parent project. As a result, the parent project's view policy now
+affects child projects. If you restrict the visibility of the parent, you also
+restrict the visibility of the children.
+
+In contrast, permission to edit a parent project grants permission to edit
+any subproject. If a user can {nav Root Project}, they can also edit
+{nav Root Project > Child} and {nav Root Project > Child > Sprint 3}.
+
+
Policies In Depth
=================

File Metadata

Mime Type
text/plain
Expires
Sun, Mar 16, 11:47 PM (2 w, 5 d ago)
Storage Engine
blob
Storage Format
Encrypted (AES-256-CBC)
Storage Handle
7707861
Default Alt Text
D15152.id.diff (29 KB)

Event Timeline