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

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

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