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',
|
'PhabricatorProjectMembersProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectMembersProfilePanel.php',
|
||||||
'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php',
|
'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php',
|
||||||
'PhabricatorProjectMembersViewController' => 'applications/project/controller/PhabricatorProjectMembersViewController.php',
|
'PhabricatorProjectMembersViewController' => 'applications/project/controller/PhabricatorProjectMembersViewController.php',
|
||||||
'PhabricatorProjectMilestonesController' => 'applications/project/controller/PhabricatorProjectMilestonesController.php',
|
|
||||||
'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php',
|
'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php',
|
||||||
'PhabricatorProjectNameContextFreeGrammar' => 'applications/project/lipsum/PhabricatorProjectNameContextFreeGrammar.php',
|
'PhabricatorProjectNameContextFreeGrammar' => 'applications/project/lipsum/PhabricatorProjectNameContextFreeGrammar.php',
|
||||||
'PhabricatorProjectNoProjectsDatasource' => 'applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php',
|
'PhabricatorProjectNoProjectsDatasource' => 'applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php',
|
||||||
|
@ -2937,7 +2936,9 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php',
|
'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php',
|
||||||
'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php',
|
'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php',
|
||||||
'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php',
|
'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php',
|
||||||
|
'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php',
|
||||||
'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php',
|
'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php',
|
||||||
|
'PhabricatorProjectSubprojectsProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php',
|
||||||
'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php',
|
'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php',
|
||||||
'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php',
|
'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php',
|
||||||
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
|
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
|
||||||
|
@ -7330,7 +7331,6 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorProjectMembersProfilePanel' => 'PhabricatorProfilePanel',
|
'PhabricatorProjectMembersProfilePanel' => 'PhabricatorProfilePanel',
|
||||||
'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController',
|
'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController',
|
||||||
'PhabricatorProjectMembersViewController' => 'PhabricatorProjectController',
|
'PhabricatorProjectMembersViewController' => 'PhabricatorProjectController',
|
||||||
'PhabricatorProjectMilestonesController' => 'PhabricatorProjectController',
|
|
||||||
'PhabricatorProjectMoveController' => 'PhabricatorProjectController',
|
'PhabricatorProjectMoveController' => 'PhabricatorProjectController',
|
||||||
'PhabricatorProjectNameContextFreeGrammar' => 'PhutilContextFreeGrammar',
|
'PhabricatorProjectNameContextFreeGrammar' => 'PhutilContextFreeGrammar',
|
||||||
'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource',
|
'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource',
|
||||||
|
@ -7357,7 +7357,9 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorStandardCustomFieldInterface',
|
'PhabricatorStandardCustomFieldInterface',
|
||||||
),
|
),
|
||||||
'PhabricatorProjectStatus' => 'Phobject',
|
'PhabricatorProjectStatus' => 'Phobject',
|
||||||
|
'PhabricatorProjectSubprojectWarningController' => 'PhabricatorProjectController',
|
||||||
'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController',
|
'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController',
|
||||||
|
'PhabricatorProjectSubprojectsProfilePanel' => 'PhabricatorProfilePanel',
|
||||||
'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator',
|
'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator',
|
||||||
'PhabricatorProjectTransaction' => 'PhabricatorApplicationTransaction',
|
'PhabricatorProjectTransaction' => 'PhabricatorApplicationTransaction',
|
||||||
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
|
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||||
|
|
|
@ -91,6 +91,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication {
|
||||||
=> 'PhabricatorProjectWatchController',
|
=> 'PhabricatorProjectWatchController',
|
||||||
'silence/(?P<id>[1-9]\d*)/'
|
'silence/(?P<id>[1-9]\d*)/'
|
||||||
=> 'PhabricatorProjectSilenceController',
|
=> 'PhabricatorProjectSilenceController',
|
||||||
|
'warning/(?P<id>[1-9]\d*)/'
|
||||||
|
=> 'PhabricatorProjectSubprojectWarningController',
|
||||||
),
|
),
|
||||||
'/tag/' => array(
|
'/tag/' => array(
|
||||||
'(?P<slug>[^/]+)/' => 'PhabricatorProjectViewController',
|
'(?P<slug>[^/]+)/' => 'PhabricatorProjectViewController',
|
||||||
|
|
|
@ -42,6 +42,7 @@ final class PhabricatorProjectEditController
|
||||||
if ($parent_id) {
|
if ($parent_id) {
|
||||||
$query = id(new PhabricatorProjectQuery())
|
$query = id(new PhabricatorProjectQuery())
|
||||||
->setViewer($viewer)
|
->setViewer($viewer)
|
||||||
|
->needImages(true)
|
||||||
->requireCapabilities(
|
->requireCapabilities(
|
||||||
array(
|
array(
|
||||||
PhabricatorPolicyCapability::CAN_VIEW,
|
PhabricatorPolicyCapability::CAN_VIEW,
|
||||||
|
@ -58,7 +59,7 @@ final class PhabricatorProjectEditController
|
||||||
|
|
||||||
if ($is_milestone) {
|
if ($is_milestone) {
|
||||||
if (!$parent->supportsMilestones()) {
|
if (!$parent->supportsMilestones()) {
|
||||||
$cancel_uri = "/project/milestones/{$parent_id}/";
|
$cancel_uri = "/project/subprojects/{$parent_id}/";
|
||||||
return $this->newDialog()
|
return $this->newDialog()
|
||||||
->setTitle(pht('No Milestones'))
|
->setTitle(pht('No Milestones'))
|
||||||
->appendParagraph(
|
->appendParagraph(
|
||||||
|
@ -91,20 +92,13 @@ final class PhabricatorProjectEditController
|
||||||
$engine = $this->getEngine();
|
$engine = $this->getEngine();
|
||||||
if ($engine) {
|
if ($engine) {
|
||||||
$parent = $engine->getParentProject();
|
$parent = $engine->getParentProject();
|
||||||
if ($parent) {
|
$milestone = $engine->getMilestoneProject();
|
||||||
$id = $parent->getID();
|
if ($parent || $milestone) {
|
||||||
|
$id = nonempty($parent, $milestone)->getID();
|
||||||
$crumbs->addTextCrumb(
|
$crumbs->addTextCrumb(
|
||||||
pht('Subprojects'),
|
pht('Subprojects'),
|
||||||
$this->getApplicationURI("subprojects/{$id}/"));
|
$this->getApplicationURI("subprojects/{$id}/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
$milestone = $engine->getMilestoneProject();
|
|
||||||
if ($milestone) {
|
|
||||||
$id = $milestone->getID();
|
|
||||||
$crumbs->addTextCrumb(
|
|
||||||
pht('Milestones'),
|
|
||||||
$this->getApplicationURI("milestones/{$id}/"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $crumbs;
|
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);
|
$watch_action = $this->renderWatchAction($project);
|
||||||
$header->addActionLink($watch_action);
|
$header->addActionLink($watch_action);
|
||||||
|
|
||||||
|
$milestone_list = $this->buildMilestoneList($project);
|
||||||
|
$subproject_list = $this->buildSubprojectList($project);
|
||||||
|
|
||||||
$member_list = id(new PhabricatorProjectMemberListView())
|
$member_list = id(new PhabricatorProjectMemberListView())
|
||||||
->setUser($viewer)
|
->setUser($viewer)
|
||||||
->setProject($project)
|
->setProject($project)
|
||||||
|
@ -82,6 +85,8 @@ final class PhabricatorProjectProfileController
|
||||||
))
|
))
|
||||||
->setSideColumn(
|
->setSideColumn(
|
||||||
array(
|
array(
|
||||||
|
$milestone_list,
|
||||||
|
$subproject_list,
|
||||||
$member_list,
|
$member_list,
|
||||||
$watcher_list,
|
$watcher_list,
|
||||||
));
|
));
|
||||||
|
@ -176,5 +181,90 @@ final class PhabricatorProjectProfileController
|
||||||
->setHref($watch_href);
|
->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,
|
$project,
|
||||||
PhabricatorPolicyCapability::CAN_EDIT);
|
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())
|
$subprojects = id(new PhabricatorProjectQuery())
|
||||||
->setViewer($viewer)
|
->setViewer($viewer)
|
||||||
->withParentProjectPHIDs(array($project->getPHID()))
|
->withParentProjectPHIDs(array($project->getPHID()))
|
||||||
|
@ -36,44 +37,57 @@ final class PhabricatorProjectSubprojectsController
|
||||||
$subprojects = array();
|
$subprojects = array();
|
||||||
}
|
}
|
||||||
|
|
||||||
$can_create = $can_edit && $has_support;
|
if ($allows_milestones) {
|
||||||
|
$milestones = id(new PhabricatorProjectQuery())
|
||||||
if ($project->getHasSubprojects()) {
|
->setViewer($viewer)
|
||||||
$button_text = pht('Create Subproject');
|
->withParentProjectPHIDs(array($project->getPHID()))
|
||||||
|
->needImages(true)
|
||||||
|
->withIsMilestone(true)
|
||||||
|
->setOrder('newest')
|
||||||
|
->execute();
|
||||||
} else {
|
} else {
|
||||||
$button_text = pht('Add Subprojects');
|
$milestones = array();
|
||||||
}
|
}
|
||||||
|
|
||||||
$header = id(new PHUIHeaderView())
|
if ($milestones) {
|
||||||
->setHeader(pht('Subprojects'))
|
$milestone_list = id(new PHUIObjectBoxView())
|
||||||
->addActionLink(
|
->setHeaderText(pht('Milestones'))
|
||||||
id(new PHUIButtonView())
|
->setObjectList(
|
||||||
->setTag('a')
|
id(new PhabricatorProjectListView())
|
||||||
->setHref("/project/edit/?parent={$id}")
|
->setUser($viewer)
|
||||||
->setIcon('fa-plus')
|
->setProjects($milestones)
|
||||||
->setDisabled(!$can_create)
|
->renderList());
|
||||||
->setWorkflow(!$can_create)
|
} else {
|
||||||
->setText($button_text));
|
$milestone_list = null;
|
||||||
|
|
||||||
$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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$box->setObjectList(
|
if ($subprojects) {
|
||||||
id(new PhabricatorProjectListView())
|
$subproject_list = id(new PHUIObjectBoxView())
|
||||||
->setUser($viewer)
|
->setHeaderText(pht('Subprojects'))
|
||||||
->setProjects($subprojects)
|
->setObjectList(
|
||||||
->renderList());
|
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 = $this->getProfileMenu();
|
||||||
$nav->selectFilter(PhabricatorProject::PANEL_SUBPROJECTS);
|
$nav->selectFilter(PhabricatorProject::PANEL_SUBPROJECTS);
|
||||||
|
@ -85,7 +99,151 @@ final class PhabricatorProjectSubprojectsController
|
||||||
->setNavigation($nav)
|
->setNavigation($nav)
|
||||||
->setCrumbs($crumbs)
|
->setCrumbs($crumbs)
|
||||||
->setTitle(array($project->getName(), pht('Subprojects')))
|
->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)
|
->setBuiltinKey(PhabricatorProject::PANEL_MEMBERS)
|
||||||
->setPanelKey(PhabricatorProjectMembersProfilePanel::PANELKEY);
|
->setPanelKey(PhabricatorProjectMembersProfilePanel::PANELKEY);
|
||||||
|
|
||||||
|
$panels[] = $this->newPanel()
|
||||||
|
->setBuiltinKey(PhabricatorProject::PANEL_SUBPROJECTS)
|
||||||
|
->setPanelKey(PhabricatorProjectSubprojectsProfilePanel::PANELKEY);
|
||||||
|
|
||||||
$panels[] = $this->newPanel()
|
$panels[] = $this->newPanel()
|
||||||
->setBuiltinKey(PhabricatorProject::PANEL_MANAGE)
|
->setBuiltinKey(PhabricatorProject::PANEL_MANAGE)
|
||||||
->setPanelKey(PhabricatorProjectManageProfilePanel::PANELKEY);
|
->setPanelKey(PhabricatorProjectManageProfilePanel::PANELKEY);
|
||||||
|
|
|
@ -61,6 +61,15 @@ final class PhabricatorProjectsMembershipIndexEngineExtension
|
||||||
|
|
||||||
$conn_w = $project->establishConnection('w');
|
$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();
|
$project->openTransaction();
|
||||||
|
|
||||||
// Delete any existing materialized member edges.
|
// Delete any existing materialized member edges.
|
||||||
|
@ -92,6 +101,14 @@ final class PhabricatorProjectsMembershipIndexEngineExtension
|
||||||
(int)$has_subprojects,
|
(int)$has_subprojects,
|
||||||
$project->getID());
|
$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();
|
$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.
|
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
|
Policies In Depth
|
||||||
=================
|
=================
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue