1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-19 05:12:41 +01:00

Put subprojects and milestones back into the Project UI

Summary: Ref T10010. Restores subprojects and milestones to the UI with a more modern style and more warnings.

Test Plan:
{F1085207}

{F1085208}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10010

Differential Revision: https://secure.phabricator.com/D15152
This commit is contained in:
epriestley 2016-02-01 07:04:19 -08:00
parent 354858e434
commit fc9db6e2a2
11 changed files with 542 additions and 141 deletions

View file

@ -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',

View file

@ -91,6 +91,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication {
=> 'PhabricatorProjectWatchController',
'silence/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectSilenceController',
'warning/(?P<id>[1-9]\d*)/'
=> 'PhabricatorProjectSubprojectWarningController',
),
'/tag/' => array(
'(?P<slug>[^/]+)/' => 'PhabricatorProjectViewController',

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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'));
}
}

View file

@ -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(
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);
}
}

View file

@ -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);

View file

@ -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();
}

View file

@ -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,
);
}
}

View file

@ -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
=================