diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6339e6c564..b986242b04 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2913,7 +2913,6 @@ phutil_register_library_map(array( '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 @@ phutil_register_library_map(array( '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 @@ phutil_register_library_map(array( 'PhabricatorProjectMembersProfilePanel' => 'PhabricatorProfilePanel', 'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController', 'PhabricatorProjectMembersViewController' => 'PhabricatorProjectController', - 'PhabricatorProjectMilestonesController' => 'PhabricatorProjectController', 'PhabricatorProjectMoveController' => 'PhabricatorProjectController', 'PhabricatorProjectNameContextFreeGrammar' => 'PhutilContextFreeGrammar', 'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource', @@ -7357,7 +7357,9 @@ phutil_register_library_map(array( '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 index 59904cb06b..5f3cb9e090 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -91,6 +91,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectWatchController', 'silence/(?P[1-9]\d*)/' => 'PhabricatorProjectSilenceController', + 'warning/(?P[1-9]\d*)/' + => 'PhabricatorProjectSubprojectWarningController', ), '/tag/' => array( '(?P[^/]+)/' => 'PhabricatorProjectViewController', diff --git a/src/applications/project/controller/PhabricatorProjectEditController.php b/src/applications/project/controller/PhabricatorProjectEditController.php index d87910462c..5091135bec 100644 --- a/src/applications/project/controller/PhabricatorProjectEditController.php +++ b/src/applications/project/controller/PhabricatorProjectEditController.php @@ -42,6 +42,7 @@ final class PhabricatorProjectEditController if ($parent_id) { $query = id(new PhabricatorProjectQuery()) ->setViewer($viewer) + ->needImages(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -58,7 +59,7 @@ final class PhabricatorProjectEditController 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 @@ final class PhabricatorProjectEditController $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 index ebbe5bc2e3..0000000000 --- a/src/applications/project/controller/PhabricatorProjectMilestonesController.php +++ /dev/null @@ -1,92 +0,0 @@ -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 index 74acd1edbf..146c1da96f 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -45,6 +45,9 @@ final class PhabricatorProjectProfileController $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 @@ final class PhabricatorProjectProfileController )) ->setSideColumn( array( + $milestone_list, + $subproject_list, $member_list, $watcher_list, )); @@ -176,5 +181,90 @@ final class PhabricatorProjectProfileController ->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 index 0000000000..9e089f138c --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php @@ -0,0 +1,51 @@ +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 index a88bf7d07c..afafed77b1 100644 --- a/src/applications/project/controller/PhabricatorProjectSubprojectsController.php +++ b/src/applications/project/controller/PhabricatorProjectSubprojectsController.php @@ -23,9 +23,10 @@ final class PhabricatorProjectSubprojectsController $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 @@ final class PhabricatorProjectSubprojectsController $subprojects = array(); } - $can_create = $can_edit && $has_support; - - if ($project->getHasSubprojects()) { - $button_text = pht('Create Subproject'); + if ($allows_milestones) { + $milestones = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withParentProjectPHIDs(array($project->getPHID())) + ->needImages(true) + ->withIsMilestone(true) + ->setOrder('newest') + ->execute(); } else { - $button_text = pht('Add Subprojects'); + $milestones = array(); } - $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 ($milestones) { + $milestone_list = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Milestones')) + ->setObjectList( + id(new PhabricatorProjectListView()) + ->setUser($viewer) + ->setProjects($milestones) + ->renderList()); + } else { + $milestone_list = null; } - $box->setObjectList( - id(new PhabricatorProjectListView()) - ->setUser($viewer) - ->setProjects($subprojects) - ->renderList()); + if ($subprojects) { + $subproject_list = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Subprojects')) + ->setObjectList( + id(new PhabricatorProjectListView()) + ->setUser($viewer) + ->setProjects($subprojects) + ->renderList()); + } else { + $subproject_list = null; + } + + $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 @@ final class PhabricatorProjectSubprojectsController ->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 index 612022d380..58b9b0b1ab 100644 --- a/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php +++ b/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php @@ -28,6 +28,10 @@ final class PhabricatorProjectProfilePanelEngine ->setBuiltinKey(PhabricatorProject::PANEL_MEMBERS) ->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 index e1460381ed..567f5b749e 100644 --- a/src/applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php +++ b/src/applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php @@ -61,6 +61,15 @@ final class PhabricatorProjectsMembershipIndexEngineExtension $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 @@ final class PhabricatorProjectsMembershipIndexEngineExtension (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 index 0000000000..fad6378d69 --- /dev/null +++ b/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php @@ -0,0 +1,63 @@ +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 index f563d527f2..b1456a9e77 100644 --- a/src/docs/user/userguide/projects.diviner +++ b/src/docs/user/userguide/projects.diviner @@ -135,6 +135,118 @@ members or won't have a workboard, you can hide these items to streamline the 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 =================