mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-30 10:42:41 +01:00
Add very basic UI for creating milestones and subprojects
Summary: Ref T10010. This has a lot of UI/UX problems but I think it: - technically allows subproject creation; - technically allows milestone creation; - doesn't let users unwittingly destroy their installs (probably). Test Plan: - Created milestones. - Created subprojects. - Created and edited normal projects. - Observed some reasonable interactions (e.g., you can't create milestones for a milestone or edit a superproject's members). - Observed plenty of silly/confusing interactions that need additional work. {F1046657} {F1046658} {F1046655} {F1046656} {F1046654} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10010 Differential Revision: https://secure.phabricator.com/D14904
This commit is contained in:
parent
7732f9c03c
commit
7c5ad63fd1
14 changed files with 583 additions and 84 deletions
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
return array(
|
return array(
|
||||||
'names' => array(
|
'names' => array(
|
||||||
'core.pkg.css' => 'a419cf4b',
|
'core.pkg.css' => '3ea6dc33',
|
||||||
'core.pkg.js' => '57dff7df',
|
'core.pkg.js' => '57dff7df',
|
||||||
'darkconsole.pkg.js' => 'e7393ebb',
|
'darkconsole.pkg.js' => 'e7393ebb',
|
||||||
'differential.pkg.css' => '2de124c9',
|
'differential.pkg.css' => '2de124c9',
|
||||||
|
@ -114,7 +114,7 @@ return array(
|
||||||
'rsrc/css/font/phui-font-icon-base.css' => 'ecbbb4c2',
|
'rsrc/css/font/phui-font-icon-base.css' => 'ecbbb4c2',
|
||||||
'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82',
|
'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82',
|
||||||
'rsrc/css/layout/phabricator-hovercard-view.css' => '1239cd52',
|
'rsrc/css/layout/phabricator-hovercard-view.css' => '1239cd52',
|
||||||
'rsrc/css/layout/phabricator-side-menu-view.css' => 'bec2458e',
|
'rsrc/css/layout/phabricator-side-menu-view.css' => '91b7a42c',
|
||||||
'rsrc/css/layout/phabricator-source-code-view.css' => 'cbeef983',
|
'rsrc/css/layout/phabricator-source-code-view.css' => 'cbeef983',
|
||||||
'rsrc/css/phui/calendar/phui-calendar-day.css' => 'd1cf6f93',
|
'rsrc/css/phui/calendar/phui-calendar-day.css' => 'd1cf6f93',
|
||||||
'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1c7f338',
|
'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1c7f338',
|
||||||
|
@ -762,7 +762,7 @@ return array(
|
||||||
'phabricator-remarkup-css' => '7afb543c',
|
'phabricator-remarkup-css' => '7afb543c',
|
||||||
'phabricator-search-results-css' => '7dea472c',
|
'phabricator-search-results-css' => '7dea472c',
|
||||||
'phabricator-shaped-request' => '7cbe244b',
|
'phabricator-shaped-request' => '7cbe244b',
|
||||||
'phabricator-side-menu-view-css' => 'bec2458e',
|
'phabricator-side-menu-view-css' => '91b7a42c',
|
||||||
'phabricator-slowvote-css' => 'da0afb1b',
|
'phabricator-slowvote-css' => 'da0afb1b',
|
||||||
'phabricator-source-code-view-css' => 'cbeef983',
|
'phabricator-source-code-view-css' => 'cbeef983',
|
||||||
'phabricator-standard-page-view' => '3c99cdf4',
|
'phabricator-standard-page-view' => '3c99cdf4',
|
||||||
|
|
|
@ -2855,6 +2855,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorProjectIconSet' => 'applications/project/icon/PhabricatorProjectIconSet.php',
|
'PhabricatorProjectIconSet' => 'applications/project/icon/PhabricatorProjectIconSet.php',
|
||||||
'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php',
|
'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php',
|
||||||
'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php',
|
'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php',
|
||||||
|
'PhabricatorProjectListView' => 'applications/project/view/PhabricatorProjectListView.php',
|
||||||
'PhabricatorProjectLockController' => 'applications/project/controller/PhabricatorProjectLockController.php',
|
'PhabricatorProjectLockController' => 'applications/project/controller/PhabricatorProjectLockController.php',
|
||||||
'PhabricatorProjectLogicalAndDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php',
|
'PhabricatorProjectLogicalAndDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php',
|
||||||
'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php',
|
'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php',
|
||||||
|
@ -7206,6 +7207,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorProjectHeraldAction' => 'HeraldAction',
|
'PhabricatorProjectHeraldAction' => 'HeraldAction',
|
||||||
'PhabricatorProjectIconSet' => 'PhabricatorIconSet',
|
'PhabricatorProjectIconSet' => 'PhabricatorIconSet',
|
||||||
'PhabricatorProjectListController' => 'PhabricatorProjectController',
|
'PhabricatorProjectListController' => 'PhabricatorProjectController',
|
||||||
|
'PhabricatorProjectListView' => 'AphrontView',
|
||||||
'PhabricatorProjectLockController' => 'PhabricatorProjectController',
|
'PhabricatorProjectLockController' => 'PhabricatorProjectController',
|
||||||
'PhabricatorProjectLogicalAndDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
|
'PhabricatorProjectLogicalAndDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
|
||||||
'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
|
'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
|
||||||
|
|
|
@ -748,15 +748,15 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
|
||||||
->setNewValue($name);
|
->setNewValue($name);
|
||||||
|
|
||||||
if ($parent) {
|
if ($parent) {
|
||||||
|
if ($is_milestone) {
|
||||||
|
$xactions[] = id(new PhabricatorProjectTransaction())
|
||||||
|
->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE)
|
||||||
|
->setNewValue($parent->getPHID());
|
||||||
|
} else {
|
||||||
$xactions[] = id(new PhabricatorProjectTransaction())
|
$xactions[] = id(new PhabricatorProjectTransaction())
|
||||||
->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT)
|
->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT)
|
||||||
->setNewValue($parent->getPHID());
|
->setNewValue($parent->getPHID());
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($is_milestone) {
|
|
||||||
$xactions[] = id(new PhabricatorProjectTransaction())
|
|
||||||
->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE)
|
|
||||||
->setNewValue(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->applyTransactions($project, $user, $xactions);
|
$this->applyTransactions($project, $user, $xactions);
|
||||||
|
|
|
@ -99,7 +99,6 @@ abstract class PhabricatorProjectController extends PhabricatorController {
|
||||||
$nav->addFilter("board/{$id}/", pht('Workboard'));
|
$nav->addFilter("board/{$id}/", pht('Workboard'));
|
||||||
$nav->addFilter("members/{$id}/", pht('Members'));
|
$nav->addFilter("members/{$id}/", pht('Members'));
|
||||||
$nav->addFilter("feed/{$id}/", pht('Feed'));
|
$nav->addFilter("feed/{$id}/", pht('Feed'));
|
||||||
$nav->addFilter("details/{$id}/", pht('Edit Details'));
|
|
||||||
}
|
}
|
||||||
$nav->addFilter('create', pht('Create Project'));
|
$nav->addFilter('create', pht('Create Project'));
|
||||||
}
|
}
|
||||||
|
@ -149,11 +148,29 @@ abstract class PhabricatorProjectController extends PhabricatorController {
|
||||||
|
|
||||||
$nav->addIcon("feed/{$id}/", pht('Feed'), 'fa-newspaper-o');
|
$nav->addIcon("feed/{$id}/", pht('Feed'), 'fa-newspaper-o');
|
||||||
$nav->addIcon("members/{$id}/", pht('Members'), 'fa-group');
|
$nav->addIcon("members/{$id}/", pht('Members'), 'fa-group');
|
||||||
$nav->addIcon("details/{$id}/", pht('Edit Details'), 'fa-pencil');
|
|
||||||
|
|
||||||
if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) {
|
if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) {
|
||||||
$nav->addIcon("subprojects/{$id}/", pht('Subprojects'), 'fa-sitemap');
|
if ($project->supportsSubprojects()) {
|
||||||
$nav->addIcon("milestones/{$id}/", pht('Milestones'), 'fa-map-marker');
|
$subprojects_icon = 'fa-sitemap';
|
||||||
|
} else {
|
||||||
|
$subprojects_icon = 'fa-sitemap grey';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($project->supportsMilestones()) {
|
||||||
|
$milestones_icon = 'fa-map-marker';
|
||||||
|
} else {
|
||||||
|
$milestones_icon = 'fa-map-marker grey';
|
||||||
|
}
|
||||||
|
|
||||||
|
$nav->addIcon(
|
||||||
|
"subprojects/{$id}/",
|
||||||
|
pht('Subprojects'),
|
||||||
|
$subprojects_icon);
|
||||||
|
|
||||||
|
$nav->addIcon(
|
||||||
|
"milestones/{$id}/",
|
||||||
|
pht('Milestones'),
|
||||||
|
$milestones_icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -170,8 +187,8 @@ abstract class PhabricatorProjectController extends PhabricatorController {
|
||||||
$ancestors[] = $project;
|
$ancestors[] = $project;
|
||||||
foreach ($ancestors as $ancestor) {
|
foreach ($ancestors as $ancestor) {
|
||||||
$crumbs->addTextCrumb(
|
$crumbs->addTextCrumb(
|
||||||
$project->getName(),
|
$ancestor->getName(),
|
||||||
$project->getURI());
|
$ancestor->getURI());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,111 @@
|
||||||
final class PhabricatorProjectEditController
|
final class PhabricatorProjectEditController
|
||||||
extends PhabricatorProjectController {
|
extends PhabricatorProjectController {
|
||||||
|
|
||||||
|
private $engine;
|
||||||
|
|
||||||
|
public function setEngine(PhabricatorProjectEditEngine $engine) {
|
||||||
|
$this->engine = $engine;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEngine() {
|
||||||
|
return $this->engine;
|
||||||
|
}
|
||||||
|
|
||||||
public function handleRequest(AphrontRequest $request) {
|
public function handleRequest(AphrontRequest $request) {
|
||||||
return id(new PhabricatorProjectEditEngine())
|
$viewer = $this->getViewer();
|
||||||
->setController($this)
|
|
||||||
->buildResponse();
|
$engine = id(new PhabricatorProjectEditEngine())
|
||||||
|
->setController($this);
|
||||||
|
|
||||||
|
$this->setEngine($engine);
|
||||||
|
|
||||||
|
$id = $request->getURIData('id');
|
||||||
|
if (!$id) {
|
||||||
|
$parent_id = head($request->getArr('parent'));
|
||||||
|
if (!$parent_id) {
|
||||||
|
$parent_id = $request->getStr('parent');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parent_id) {
|
||||||
|
$is_milestone = false;
|
||||||
|
} else {
|
||||||
|
$parent_id = head($request->getArr('milestone'));
|
||||||
|
if (!$parent_id) {
|
||||||
|
$parent_id = $request->getStr('milestone');
|
||||||
|
}
|
||||||
|
$is_milestone = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parent_id) {
|
||||||
|
$query = id(new PhabricatorProjectQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->requireCapabilities(
|
||||||
|
array(
|
||||||
|
PhabricatorPolicyCapability::CAN_VIEW,
|
||||||
|
PhabricatorPolicyCapability::CAN_EDIT,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (ctype_digit($parent_id)) {
|
||||||
|
$query->withIDs(array($parent_id));
|
||||||
|
} else {
|
||||||
|
$query->withPHIDs(array($parent_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
$parent = $query->executeOne();
|
||||||
|
|
||||||
|
if ($is_milestone) {
|
||||||
|
if (!$parent->supportsMilestones()) {
|
||||||
|
$cancel_uri = "/project/milestones/{$parent_id}/";
|
||||||
|
return $this->newDialog()
|
||||||
|
->setTitle(pht('No Milestones'))
|
||||||
|
->appendParagraph(
|
||||||
|
pht('You can not add milestones to this project.'))
|
||||||
|
->addCancelButton($cancel_uri);
|
||||||
|
}
|
||||||
|
$engine->setMilestoneProject($parent);
|
||||||
|
} else {
|
||||||
|
if (!$parent->supportsSubprojects()) {
|
||||||
|
$cancel_uri = "/project/subprojects/{$parent_id}/";
|
||||||
|
return $this->newDialog()
|
||||||
|
->setTitle(pht('No Subprojects'))
|
||||||
|
->appendParagraph(
|
||||||
|
pht('You can not add subprojects to this project.'))
|
||||||
|
->addCancelButton($cancel_uri);
|
||||||
|
}
|
||||||
|
$engine->setParentProject($parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->setProject($parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $engine->buildResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function buildApplicationCrumbs() {
|
||||||
|
$crumbs = parent::buildApplicationCrumbs();
|
||||||
|
|
||||||
|
$engine = $this->getEngine();
|
||||||
|
if ($engine) {
|
||||||
|
$parent = $engine->getParentProject();
|
||||||
|
if ($parent) {
|
||||||
|
$id = $parent->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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,9 +68,11 @@ final class PhabricatorProjectMembersEditController
|
||||||
$project,
|
$project,
|
||||||
PhabricatorPolicyCapability::CAN_EDIT);
|
PhabricatorPolicyCapability::CAN_EDIT);
|
||||||
|
|
||||||
|
$supports_edit = $project->supportsEditMembers();
|
||||||
|
|
||||||
$form_box = null;
|
$form_box = null;
|
||||||
$title = pht('Add Members');
|
$title = pht('Add Members');
|
||||||
if ($can_edit) {
|
if ($can_edit && $supports_edit) {
|
||||||
$header_name = pht('Edit Members');
|
$header_name = pht('Edit Members');
|
||||||
$view_uri = $this->getApplicationURI('profile/'.$project->getID().'/');
|
$view_uri = $this->getApplicationURI('profile/'.$project->getID().'/');
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,64 @@ final class PhabricatorProjectMilestonesController
|
||||||
$project = $this->getProject();
|
$project = $this->getProject();
|
||||||
$id = $project->getID();
|
$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}")
|
||||||
|
->setIconFont('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->buildIconNavView($project);
|
$nav = $this->buildIconNavView($project);
|
||||||
$nav->selectFilter("milestones/{$id}/");
|
$nav->selectFilter("milestones/{$id}/");
|
||||||
|
|
||||||
|
@ -27,7 +85,8 @@ final class PhabricatorProjectMilestonesController
|
||||||
return $this->newPage()
|
return $this->newPage()
|
||||||
->setNavigation($nav)
|
->setNavigation($nav)
|
||||||
->setCrumbs($crumbs)
|
->setCrumbs($crumbs)
|
||||||
->setTitle(array($project->getName(), pht('Milestones')));
|
->setTitle(array($project->getName(), pht('Milestones')))
|
||||||
|
->appendChild($box);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,63 @@ final class PhabricatorProjectSubprojectsController
|
||||||
$project = $this->getProject();
|
$project = $this->getProject();
|
||||||
$id = $project->getID();
|
$id = $project->getID();
|
||||||
|
|
||||||
|
$can_edit = PhabricatorPolicyFilter::hasCapability(
|
||||||
|
$viewer,
|
||||||
|
$project,
|
||||||
|
PhabricatorPolicyCapability::CAN_EDIT);
|
||||||
|
|
||||||
|
$has_support = $project->supportsSubprojects();
|
||||||
|
|
||||||
|
if ($has_support) {
|
||||||
|
$subprojects = id(new PhabricatorProjectQuery())
|
||||||
|
->setViewer($viewer)
|
||||||
|
->withParentProjectPHIDs(array($project->getPHID()))
|
||||||
|
->needImages(true)
|
||||||
|
->withIsMilestone(false)
|
||||||
|
->execute();
|
||||||
|
} else {
|
||||||
|
$subprojects = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$can_create = $can_edit && $has_support;
|
||||||
|
|
||||||
|
if ($project->getHasSubprojects()) {
|
||||||
|
$button_text = pht('Create Subproject');
|
||||||
|
} else {
|
||||||
|
$button_text = pht('Add Subprojects');
|
||||||
|
}
|
||||||
|
|
||||||
|
$header = id(new PHUIHeaderView())
|
||||||
|
->setHeader(pht('Subprojects'))
|
||||||
|
->addActionLink(
|
||||||
|
id(new PHUIButtonView())
|
||||||
|
->setTag('a')
|
||||||
|
->setHref("/project/edit/?parent={$id}")
|
||||||
|
->setIconFont('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);
|
||||||
|
}
|
||||||
|
|
||||||
|
$box->setObjectList(
|
||||||
|
id(new PhabricatorProjectListView())
|
||||||
|
->setUser($viewer)
|
||||||
|
->setProjects($subprojects)
|
||||||
|
->renderList());
|
||||||
|
|
||||||
$nav = $this->buildIconNavView($project);
|
$nav = $this->buildIconNavView($project);
|
||||||
$nav->selectFilter("subprojects/{$id}/");
|
$nav->selectFilter("subprojects/{$id}/");
|
||||||
|
|
||||||
|
@ -27,7 +84,8 @@ final class PhabricatorProjectSubprojectsController
|
||||||
return $this->newPage()
|
return $this->newPage()
|
||||||
->setNavigation($nav)
|
->setNavigation($nav)
|
||||||
->setCrumbs($crumbs)
|
->setCrumbs($crumbs)
|
||||||
->setTitle(array($project->getName(), pht('Subprojects')));
|
->setTitle(array($project->getName(), pht('Subprojects')))
|
||||||
|
->appendChild($box);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,23 +74,10 @@ final class PhabricatorProjectTransactionEditor
|
||||||
case PhabricatorProjectTransaction::TYPE_COLOR:
|
case PhabricatorProjectTransaction::TYPE_COLOR:
|
||||||
case PhabricatorProjectTransaction::TYPE_LOCKED:
|
case PhabricatorProjectTransaction::TYPE_LOCKED:
|
||||||
case PhabricatorProjectTransaction::TYPE_PARENT:
|
case PhabricatorProjectTransaction::TYPE_PARENT:
|
||||||
|
case PhabricatorProjectTransaction::TYPE_MILESTONE:
|
||||||
return $xaction->getNewValue();
|
return $xaction->getNewValue();
|
||||||
case PhabricatorProjectTransaction::TYPE_SLUGS:
|
case PhabricatorProjectTransaction::TYPE_SLUGS:
|
||||||
return $this->normalizeSlugs($xaction->getNewValue());
|
return $this->normalizeSlugs($xaction->getNewValue());
|
||||||
case PhabricatorProjectTransaction::TYPE_MILESTONE:
|
|
||||||
$current = queryfx_one(
|
|
||||||
$object->establishConnection('w'),
|
|
||||||
'SELECT MAX(milestoneNumber) n
|
|
||||||
FROM %T
|
|
||||||
WHERE parentProjectPHID = %s',
|
|
||||||
$object->getTableName(),
|
|
||||||
$object->getParentProject()->getPHID());
|
|
||||||
if (!$current) {
|
|
||||||
$number = 1;
|
|
||||||
} else {
|
|
||||||
$number = (int)$current['n'] + 1;
|
|
||||||
}
|
|
||||||
return $number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::getCustomTransactionNewValue($object, $xaction);
|
return parent::getCustomTransactionNewValue($object, $xaction);
|
||||||
|
@ -127,7 +114,21 @@ final class PhabricatorProjectTransactionEditor
|
||||||
$object->setParentProjectPHID($xaction->getNewValue());
|
$object->setParentProjectPHID($xaction->getNewValue());
|
||||||
return;
|
return;
|
||||||
case PhabricatorProjectTransaction::TYPE_MILESTONE:
|
case PhabricatorProjectTransaction::TYPE_MILESTONE:
|
||||||
$object->setMilestoneNumber($xaction->getNewValue());
|
$current = queryfx_one(
|
||||||
|
$object->establishConnection('w'),
|
||||||
|
'SELECT MAX(milestoneNumber) n
|
||||||
|
FROM %T
|
||||||
|
WHERE parentProjectPHID = %s',
|
||||||
|
$object->getTableName(),
|
||||||
|
$object->getParentProject()->getPHID());
|
||||||
|
if (!$current) {
|
||||||
|
$number = 1;
|
||||||
|
} else {
|
||||||
|
$number = (int)$current['n'] + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$object->setMilestoneNumber($number);
|
||||||
|
$object->setParentProjectPHID($xaction->getNewValue());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +240,84 @@ final class PhabricatorProjectTransactionEditor
|
||||||
return parent::applyBuiltinExternalTransaction($object, $xaction);
|
return parent::applyBuiltinExternalTransaction($object, $xaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function validateAllTransactions(
|
||||||
|
PhabricatorLiskDAO $object,
|
||||||
|
array $xactions) {
|
||||||
|
|
||||||
|
$errors = array();
|
||||||
|
|
||||||
|
// Prevent creating projects which are both subprojects and milestones,
|
||||||
|
// since this does not make sense, won't work, and will break everything.
|
||||||
|
$parent_xaction = null;
|
||||||
|
foreach ($xactions as $xaction) {
|
||||||
|
switch ($xaction->getTransactionType()) {
|
||||||
|
case PhabricatorProjectTransaction::TYPE_PARENT:
|
||||||
|
case PhabricatorProjectTransaction::TYPE_MILESTONE:
|
||||||
|
if ($xaction->getNewValue() === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$parent_xaction) {
|
||||||
|
$parent_xaction = $xaction;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors[] = new PhabricatorApplicationTransactionValidationError(
|
||||||
|
$xaction->getTransactionType(),
|
||||||
|
pht('Invalid'),
|
||||||
|
pht(
|
||||||
|
'When creating a project, specify a maximum of one parent '.
|
||||||
|
'project or milestone project. A project can not be both a '.
|
||||||
|
'subproject and a milestone.'),
|
||||||
|
$xaction);
|
||||||
|
break;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$is_milestone = $object->isMilestone();
|
||||||
|
foreach ($xactions as $xaction) {
|
||||||
|
switch ($xaction->getTransactionType()) {
|
||||||
|
case PhabricatorProjectTransaction::TYPE_MILESTONE:
|
||||||
|
if ($xaction->getNewValue() !== null) {
|
||||||
|
$is_milestone = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$is_parent = $object->getHasSubprojects();
|
||||||
|
|
||||||
|
foreach ($xactions as $xaction) {
|
||||||
|
switch ($xaction->getTransactionType()) {
|
||||||
|
case PhabricatorProjectTransaction::TYPE_MEMBERS:
|
||||||
|
if ($is_parent) {
|
||||||
|
$errors[] = new PhabricatorApplicationTransactionValidationError(
|
||||||
|
$xaction->getTransactionType(),
|
||||||
|
pht('Invalid'),
|
||||||
|
pht(
|
||||||
|
'You can not change members of a project with subprojects '.
|
||||||
|
'directly. Members of any subproject are automatically '.
|
||||||
|
'members of the parent project.'),
|
||||||
|
$xaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_milestone) {
|
||||||
|
$errors[] = new PhabricatorApplicationTransactionValidationError(
|
||||||
|
$xaction->getTransactionType(),
|
||||||
|
pht('Invalid'),
|
||||||
|
pht(
|
||||||
|
'You can not change members of a milestone. Members of the '.
|
||||||
|
'parent project are automatically members of the milestone.'),
|
||||||
|
$xaction);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
protected function validateTransaction(
|
protected function validateTransaction(
|
||||||
PhabricatorLiskDAO $object,
|
PhabricatorLiskDAO $object,
|
||||||
$type,
|
$type,
|
||||||
|
@ -367,25 +446,29 @@ final class PhabricatorProjectTransactionEditor
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case PhabricatorProjectTransaction::TYPE_PARENT:
|
case PhabricatorProjectTransaction::TYPE_PARENT:
|
||||||
|
case PhabricatorProjectTransaction::TYPE_MILESTONE:
|
||||||
if (!$xactions) {
|
if (!$xactions) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$xaction = last($xactions);
|
$xaction = last($xactions);
|
||||||
|
|
||||||
|
$parent_phid = $xaction->getNewValue();
|
||||||
|
if (!$parent_phid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$this->getIsNewObject()) {
|
if (!$this->getIsNewObject()) {
|
||||||
$errors[] = new PhabricatorApplicationTransactionValidationError(
|
$errors[] = new PhabricatorApplicationTransactionValidationError(
|
||||||
$type,
|
$type,
|
||||||
pht('Invalid'),
|
pht('Invalid'),
|
||||||
pht(
|
pht(
|
||||||
'You can only set a parent project when creating a project '.
|
'You can only set a parent or milestone project when creating a '.
|
||||||
'for the first time.'),
|
'project for the first time.'),
|
||||||
$xaction);
|
$xaction);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$parent_phid = $xaction->getNewValue();
|
|
||||||
|
|
||||||
$projects = id(new PhabricatorProjectQuery())
|
$projects = id(new PhabricatorProjectQuery())
|
||||||
->setViewer($this->requireActor())
|
->setViewer($this->requireActor())
|
||||||
->withPHIDs(array($parent_phid))
|
->withPHIDs(array($parent_phid))
|
||||||
|
@ -400,8 +483,8 @@ final class PhabricatorProjectTransactionEditor
|
||||||
$type,
|
$type,
|
||||||
pht('Invalid'),
|
pht('Invalid'),
|
||||||
pht(
|
pht(
|
||||||
'Parent project PHID ("%s") must be the PHID of a valid, '.
|
'Parent or milestone project PHID ("%s") must be the PHID of a '.
|
||||||
'visible project which you have permission to edit.',
|
'valid, visible project which you have permission to edit.',
|
||||||
$parent_phid),
|
$parent_phid),
|
||||||
$xaction);
|
$xaction);
|
||||||
break;
|
break;
|
||||||
|
@ -414,8 +497,8 @@ final class PhabricatorProjectTransactionEditor
|
||||||
$type,
|
$type,
|
||||||
pht('Invalid'),
|
pht('Invalid'),
|
||||||
pht(
|
pht(
|
||||||
'Parent project PHID ("%s") must not be a milestone. '.
|
'Parent or milestone project PHID ("%s") must not be a '.
|
||||||
'Milestones may not have subprojects.',
|
'milestone. Milestones may not have subprojects or milestones.',
|
||||||
$parent_phid),
|
$parent_phid),
|
||||||
$xaction);
|
$xaction);
|
||||||
break;
|
break;
|
||||||
|
@ -427,9 +510,9 @@ final class PhabricatorProjectTransactionEditor
|
||||||
$type,
|
$type,
|
||||||
pht('Invalid'),
|
pht('Invalid'),
|
||||||
pht(
|
pht(
|
||||||
'You can not create a subproject under this parent because '.
|
'You can not create a subproject or mielstone under this parent '.
|
||||||
'it would nest projects too deeply. The maximum nesting '.
|
'because it would nest projects too deeply. The maximum '.
|
||||||
'depth of projects is %s.',
|
'nesting depth of projects is %s.',
|
||||||
new PhutilNumber($limit)),
|
new PhutilNumber($limit)),
|
||||||
$xaction);
|
$xaction);
|
||||||
break;
|
break;
|
||||||
|
@ -611,6 +694,7 @@ final class PhabricatorProjectTransactionEditor
|
||||||
array $xactions) {
|
array $xactions) {
|
||||||
|
|
||||||
$materialize = false;
|
$materialize = false;
|
||||||
|
$new_parent = null;
|
||||||
foreach ($xactions as $xaction) {
|
foreach ($xactions as $xaction) {
|
||||||
switch ($xaction->getTransactionType()) {
|
switch ($xaction->getTransactionType()) {
|
||||||
case PhabricatorTransactions::TYPE_EDGE:
|
case PhabricatorTransactions::TYPE_EDGE:
|
||||||
|
@ -622,10 +706,34 @@ final class PhabricatorProjectTransactionEditor
|
||||||
break;
|
break;
|
||||||
case PhabricatorProjectTransaction::TYPE_PARENT:
|
case PhabricatorProjectTransaction::TYPE_PARENT:
|
||||||
$materialize = true;
|
$materialize = true;
|
||||||
|
$new_parent = $object->getParentProject();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($new_parent) {
|
||||||
|
// If we just created the first subproject of this parent, we want to
|
||||||
|
// copy all of the real members to the subproject.
|
||||||
|
if (!$new_parent->getHasSubprojects()) {
|
||||||
|
$member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST;
|
||||||
|
|
||||||
|
$project_members = PhabricatorEdgeQuery::loadDestinationPHIDs(
|
||||||
|
$new_parent->getPHID(),
|
||||||
|
$member_type);
|
||||||
|
|
||||||
|
if ($project_members) {
|
||||||
|
$editor = id(new PhabricatorEdgeEditor());
|
||||||
|
foreach ($project_members as $phid) {
|
||||||
|
$editor->addEdge($object->getPHID(), $member_type, $phid);
|
||||||
|
}
|
||||||
|
$editor->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: We should dump an informational transaction onto the parent
|
||||||
|
// project to show that we created the sub-thing.
|
||||||
|
|
||||||
if ($materialize) {
|
if ($materialize) {
|
||||||
id(new PhabricatorProjectsMembershipIndexEngineExtension())
|
id(new PhabricatorProjectsMembershipIndexEngineExtension())
|
||||||
->rematerialize($object);
|
->rematerialize($object);
|
||||||
|
|
|
@ -5,6 +5,27 @@ final class PhabricatorProjectEditEngine
|
||||||
|
|
||||||
const ENGINECONST = 'projects.project';
|
const ENGINECONST = 'projects.project';
|
||||||
|
|
||||||
|
private $parentProject;
|
||||||
|
private $milestoneProject;
|
||||||
|
|
||||||
|
public function setParentProject(PhabricatorProject $parent_project) {
|
||||||
|
$this->parentProject = $parent_project;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getParentProject() {
|
||||||
|
return $this->parentProject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMilestoneProject(PhabricatorProject $milestone_project) {
|
||||||
|
$this->milestoneProject = $milestone_project;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getMilestoneProject() {
|
||||||
|
return $this->milestoneProject;
|
||||||
|
}
|
||||||
|
|
||||||
public function getEngineName() {
|
public function getEngineName() {
|
||||||
return pht('Projects');
|
return pht('Projects');
|
||||||
}
|
}
|
||||||
|
@ -50,6 +71,22 @@ final class PhabricatorProjectEditEngine
|
||||||
return $object->getURI();
|
return $object->getURI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getObjectCreateCancelURI($object) {
|
||||||
|
$parent = $this->getParentProject();
|
||||||
|
if ($parent) {
|
||||||
|
$id = $parent->getID();
|
||||||
|
return "/project/subprojects/{$id}/";
|
||||||
|
}
|
||||||
|
|
||||||
|
$milestone = $this->getMilestoneProject();
|
||||||
|
if ($milestone) {
|
||||||
|
$id = $milestone->getID();
|
||||||
|
return "/project/milestones/{$id}/";
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::getObjectCreateCancelURI($object);
|
||||||
|
}
|
||||||
|
|
||||||
protected function getCreateNewObjectPolicy() {
|
protected function getCreateNewObjectPolicy() {
|
||||||
return $this->getApplication()->getPolicy(
|
return $this->getApplication()->getPolicy(
|
||||||
ProjectCreateProjectsCapability::CAPABILITY);
|
ProjectCreateProjectsCapability::CAPABILITY);
|
||||||
|
@ -65,6 +102,8 @@ final class PhabricatorProjectEditEngine
|
||||||
$configuration
|
$configuration
|
||||||
->setFieldOrder(
|
->setFieldOrder(
|
||||||
array(
|
array(
|
||||||
|
'parent',
|
||||||
|
'milestone',
|
||||||
'name',
|
'name',
|
||||||
'std:project:internal:description',
|
'std:project:internal:description',
|
||||||
'icon',
|
'icon',
|
||||||
|
@ -84,7 +123,52 @@ final class PhabricatorProjectEditEngine
|
||||||
unset($slugs[$object->getPrimarySlug()]);
|
unset($slugs[$object->getPrimarySlug()]);
|
||||||
$slugs = array_values($slugs);
|
$slugs = array_values($slugs);
|
||||||
|
|
||||||
|
$milestone = $this->getMilestoneProject();
|
||||||
|
$parent = $this->getParentProject();
|
||||||
|
|
||||||
|
if ($parent) {
|
||||||
|
$parent_phid = $parent->getPHID();
|
||||||
|
} else {
|
||||||
|
$parent_phid = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($milestone) {
|
||||||
|
$milestone_phid = $milestone->getPHID();
|
||||||
|
} else {
|
||||||
|
$milestone_phid = null;
|
||||||
|
}
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
|
id(new PhabricatorHandlesEditField())
|
||||||
|
->setKey('parent')
|
||||||
|
->setLabel(pht('Parent'))
|
||||||
|
->setDescription(pht('Create a subproject of an existing project.'))
|
||||||
|
->setConduitDescription(
|
||||||
|
pht('Choose a parent project to create a subproject beneath.'))
|
||||||
|
->setConduitTypeDescription(pht('PHID of the parent project.'))
|
||||||
|
->setAliases(array('parentPHID'))
|
||||||
|
->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT)
|
||||||
|
->setHandleParameterType(new AphrontPHIDHTTPParameterType())
|
||||||
|
->setSingleValue($parent_phid)
|
||||||
|
->setIsReorderable(false)
|
||||||
|
->setIsDefaultable(false)
|
||||||
|
->setIsLockable(false)
|
||||||
|
->setIsLocked(true),
|
||||||
|
id(new PhabricatorHandlesEditField())
|
||||||
|
->setKey('milestone')
|
||||||
|
->setLabel(pht('Milestone Of'))
|
||||||
|
->setDescription(pht('Parent project to create a milestone for.'))
|
||||||
|
->setConduitDescription(
|
||||||
|
pht('Choose a parent project to create a new milestone for.'))
|
||||||
|
->setConduitTypeDescription(pht('PHID of the parent project.'))
|
||||||
|
->setAliases(array('milestonePHID'))
|
||||||
|
->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE)
|
||||||
|
->setHandleParameterType(new AphrontPHIDHTTPParameterType())
|
||||||
|
->setSingleValue($milestone_phid)
|
||||||
|
->setIsReorderable(false)
|
||||||
|
->setIsDefaultable(false)
|
||||||
|
->setIsLockable(false)
|
||||||
|
->setIsLocked(true),
|
||||||
id(new PhabricatorTextEditField())
|
id(new PhabricatorTextEditField())
|
||||||
->setKey('name')
|
->setKey('name')
|
||||||
->setLabel(pht('Name'))
|
->setLabel(pht('Name'))
|
||||||
|
|
|
@ -164,42 +164,15 @@ protected function buildQueryFromParameters(array $map) {
|
||||||
array $handles) {
|
array $handles) {
|
||||||
assert_instances_of($projects, 'PhabricatorProject');
|
assert_instances_of($projects, 'PhabricatorProject');
|
||||||
$viewer = $this->requireViewer();
|
$viewer = $this->requireViewer();
|
||||||
$handles = $viewer->loadHandles(mpull($projects, 'getPHID'));
|
|
||||||
|
|
||||||
$list = new PHUIObjectItemListView();
|
$list = id(new PhabricatorProjectListView())
|
||||||
$list->setUser($viewer);
|
->setUser($viewer)
|
||||||
$can_edit_projects = id(new PhabricatorPolicyFilter())
|
->setProjects($projects)
|
||||||
->setViewer($viewer)
|
->renderList();
|
||||||
->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT))
|
|
||||||
->apply($projects);
|
|
||||||
|
|
||||||
foreach ($projects as $key => $project) {
|
|
||||||
$id = $project->getID();
|
|
||||||
|
|
||||||
$tag_list = id(new PHUIHandleTagListView())
|
|
||||||
->setSlim(true)
|
|
||||||
->setHandles(array($handles[$project->getPHID()]));
|
|
||||||
|
|
||||||
$item = id(new PHUIObjectItemView())
|
|
||||||
->setHeader($project->getName())
|
|
||||||
->setHref($this->getApplicationURI("view/{$id}/"))
|
|
||||||
->setImageURI($project->getProfileImageURI())
|
|
||||||
->addAttribute($tag_list);
|
|
||||||
|
|
||||||
if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) {
|
|
||||||
$item->addIcon('delete-grey', pht('Archived'));
|
|
||||||
$item->setDisabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$list->addItem($item);
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = new PhabricatorApplicationSearchResultView();
|
|
||||||
$result->setObjectList($list);
|
|
||||||
$result->setNoDataString(pht('No projects found.'));
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
|
|
||||||
|
return id(new PhabricatorApplicationSearchResultView())
|
||||||
|
->setObjectList($list)
|
||||||
|
->setNoDataString(pht('No projects found.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getNewUserBody() {
|
protected function getNewUserBody() {
|
||||||
|
|
|
@ -88,6 +88,10 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPolicy($capability) {
|
public function getPolicy($capability) {
|
||||||
|
if ($this->isMilestone()) {
|
||||||
|
return $this->getParentProject()->getPolicy($capability);
|
||||||
|
}
|
||||||
|
|
||||||
switch ($capability) {
|
switch ($capability) {
|
||||||
case PhabricatorPolicyCapability::CAN_VIEW:
|
case PhabricatorPolicyCapability::CAN_VIEW:
|
||||||
return $this->getViewPolicy();
|
return $this->getViewPolicy();
|
||||||
|
@ -99,6 +103,12 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
||||||
}
|
}
|
||||||
|
|
||||||
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
|
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
|
||||||
|
if ($this->isMilestone()) {
|
||||||
|
return $this->getParentProject()->hasAutomaticCapability(
|
||||||
|
$capability,
|
||||||
|
$viewer);
|
||||||
|
}
|
||||||
|
|
||||||
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
|
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
|
||||||
|
|
||||||
switch ($capability) {
|
switch ($capability) {
|
||||||
|
@ -437,6 +447,34 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
||||||
return $ancestors;
|
return $ancestors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function supportsEditMembers() {
|
||||||
|
if ($this->isMilestone()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->getHasSubprojects()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsMilestones() {
|
||||||
|
if ($this->isMilestone()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsSubprojects() {
|
||||||
|
if ($this->isMilestone()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* -( PhabricatorSubscribableInterface )----------------------------------- */
|
/* -( PhabricatorSubscribableInterface )----------------------------------- */
|
||||||
|
|
||||||
|
|
53
src/applications/project/view/PhabricatorProjectListView.php
Normal file
53
src/applications/project/view/PhabricatorProjectListView.php
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorProjectListView extends AphrontView {
|
||||||
|
|
||||||
|
private $projects;
|
||||||
|
|
||||||
|
public function setProjects(array $projects) {
|
||||||
|
$this->projects = $projects;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProjects() {
|
||||||
|
return $this->projects;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderList() {
|
||||||
|
$viewer = $this->getUser();
|
||||||
|
$projects = $this->getProjects();
|
||||||
|
|
||||||
|
$handles = $viewer->loadHandles(mpull($projects, 'getPHID'));
|
||||||
|
|
||||||
|
$list = id(new PHUIObjectItemListView())
|
||||||
|
->setUser($viewer);
|
||||||
|
|
||||||
|
foreach ($projects as $key => $project) {
|
||||||
|
$id = $project->getID();
|
||||||
|
|
||||||
|
$tag_list = id(new PHUIHandleTagListView())
|
||||||
|
->setSlim(true)
|
||||||
|
->setHandles(array($handles[$project->getPHID()]));
|
||||||
|
|
||||||
|
$item = id(new PHUIObjectItemView())
|
||||||
|
->setHeader($project->getName())
|
||||||
|
->setHref("/project/view/{$id}/")
|
||||||
|
->setImageURI($project->getProfileImageURI())
|
||||||
|
->addAttribute($tag_list);
|
||||||
|
|
||||||
|
if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) {
|
||||||
|
$item->addIcon('delete-grey', pht('Archived'));
|
||||||
|
$item->setDisabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$list->addItem($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render() {
|
||||||
|
return $this->renderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -89,6 +89,10 @@
|
||||||
color: {$blue};
|
color: {$blue};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.phabricator-icon-nav .phabricator-side-menu .phui-list-item-icon.grey {
|
||||||
|
color: {$lightgreyborder};
|
||||||
|
}
|
||||||
|
|
||||||
.phabricator-icon-nav .phabricator-side-menu .phui-list-item-selected {
|
.phabricator-icon-nav .phabricator-side-menu .phui-list-item-selected {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue