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:
parent
354858e434
commit
fc9db6e2a2
11 changed files with 542 additions and 141 deletions
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
=================
|
||||
|
||||
|
|
Loading…
Reference in a new issue