Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F15394303
D15152.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
29 KB
Referenced Files
None
Subscribers
None
D15152.diff
View Options
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
Details
Attached
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.diff (29 KB)
Attached To
Mode
D15152: Put subprojects and milestones back into the Project UI
Attached
Detach File
Event Timeline
Log In to Comment