mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-23 07:12: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(
|
||||
'names' => array(
|
||||
'core.pkg.css' => 'a419cf4b',
|
||||
'core.pkg.css' => '3ea6dc33',
|
||||
'core.pkg.js' => '57dff7df',
|
||||
'darkconsole.pkg.js' => 'e7393ebb',
|
||||
'differential.pkg.css' => '2de124c9',
|
||||
|
@ -114,7 +114,7 @@ return array(
|
|||
'rsrc/css/font/phui-font-icon-base.css' => 'ecbbb4c2',
|
||||
'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82',
|
||||
'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/phui/calendar/phui-calendar-day.css' => 'd1cf6f93',
|
||||
'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1c7f338',
|
||||
|
@ -762,7 +762,7 @@ return array(
|
|||
'phabricator-remarkup-css' => '7afb543c',
|
||||
'phabricator-search-results-css' => '7dea472c',
|
||||
'phabricator-shaped-request' => '7cbe244b',
|
||||
'phabricator-side-menu-view-css' => 'bec2458e',
|
||||
'phabricator-side-menu-view-css' => '91b7a42c',
|
||||
'phabricator-slowvote-css' => 'da0afb1b',
|
||||
'phabricator-source-code-view-css' => 'cbeef983',
|
||||
'phabricator-standard-page-view' => '3c99cdf4',
|
||||
|
|
|
@ -2855,6 +2855,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectIconSet' => 'applications/project/icon/PhabricatorProjectIconSet.php',
|
||||
'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php',
|
||||
'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php',
|
||||
'PhabricatorProjectListView' => 'applications/project/view/PhabricatorProjectListView.php',
|
||||
'PhabricatorProjectLockController' => 'applications/project/controller/PhabricatorProjectLockController.php',
|
||||
'PhabricatorProjectLogicalAndDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php',
|
||||
'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php',
|
||||
|
@ -7206,6 +7207,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectHeraldAction' => 'HeraldAction',
|
||||
'PhabricatorProjectIconSet' => 'PhabricatorIconSet',
|
||||
'PhabricatorProjectListController' => 'PhabricatorProjectController',
|
||||
'PhabricatorProjectListView' => 'AphrontView',
|
||||
'PhabricatorProjectLockController' => 'PhabricatorProjectController',
|
||||
'PhabricatorProjectLogicalAndDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
|
||||
'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
|
||||
|
|
|
@ -748,15 +748,15 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
|
|||
->setNewValue($name);
|
||||
|
||||
if ($parent) {
|
||||
$xactions[] = id(new PhabricatorProjectTransaction())
|
||||
->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT)
|
||||
->setNewValue($parent->getPHID());
|
||||
}
|
||||
|
||||
if ($is_milestone) {
|
||||
$xactions[] = id(new PhabricatorProjectTransaction())
|
||||
->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE)
|
||||
->setNewValue(true);
|
||||
if ($is_milestone) {
|
||||
$xactions[] = id(new PhabricatorProjectTransaction())
|
||||
->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE)
|
||||
->setNewValue($parent->getPHID());
|
||||
} else {
|
||||
$xactions[] = id(new PhabricatorProjectTransaction())
|
||||
->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT)
|
||||
->setNewValue($parent->getPHID());
|
||||
}
|
||||
}
|
||||
|
||||
$this->applyTransactions($project, $user, $xactions);
|
||||
|
|
|
@ -99,7 +99,6 @@ abstract class PhabricatorProjectController extends PhabricatorController {
|
|||
$nav->addFilter("board/{$id}/", pht('Workboard'));
|
||||
$nav->addFilter("members/{$id}/", pht('Members'));
|
||||
$nav->addFilter("feed/{$id}/", pht('Feed'));
|
||||
$nav->addFilter("details/{$id}/", pht('Edit Details'));
|
||||
}
|
||||
$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("members/{$id}/", pht('Members'), 'fa-group');
|
||||
$nav->addIcon("details/{$id}/", pht('Edit Details'), 'fa-pencil');
|
||||
|
||||
if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) {
|
||||
$nav->addIcon("subprojects/{$id}/", pht('Subprojects'), 'fa-sitemap');
|
||||
$nav->addIcon("milestones/{$id}/", pht('Milestones'), 'fa-map-marker');
|
||||
if ($project->supportsSubprojects()) {
|
||||
$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;
|
||||
foreach ($ancestors as $ancestor) {
|
||||
$crumbs->addTextCrumb(
|
||||
$project->getName(),
|
||||
$project->getURI());
|
||||
$ancestor->getName(),
|
||||
$ancestor->getURI());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,10 +3,111 @@
|
|||
final class PhabricatorProjectEditController
|
||||
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) {
|
||||
return id(new PhabricatorProjectEditEngine())
|
||||
->setController($this)
|
||||
->buildResponse();
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$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,
|
||||
PhabricatorPolicyCapability::CAN_EDIT);
|
||||
|
||||
$supports_edit = $project->supportsEditMembers();
|
||||
|
||||
$form_box = null;
|
||||
$title = pht('Add Members');
|
||||
if ($can_edit) {
|
||||
if ($can_edit && $supports_edit) {
|
||||
$header_name = pht('Edit Members');
|
||||
$view_uri = $this->getApplicationURI('profile/'.$project->getID().'/');
|
||||
|
||||
|
|
|
@ -18,6 +18,64 @@ final class PhabricatorProjectMilestonesController
|
|||
$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}")
|
||||
->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->selectFilter("milestones/{$id}/");
|
||||
|
||||
|
@ -27,7 +85,8 @@ final class PhabricatorProjectMilestonesController
|
|||
return $this->newPage()
|
||||
->setNavigation($nav)
|
||||
->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();
|
||||
$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->selectFilter("subprojects/{$id}/");
|
||||
|
||||
|
@ -27,7 +84,8 @@ final class PhabricatorProjectSubprojectsController
|
|||
return $this->newPage()
|
||||
->setNavigation($nav)
|
||||
->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_LOCKED:
|
||||
case PhabricatorProjectTransaction::TYPE_PARENT:
|
||||
case PhabricatorProjectTransaction::TYPE_MILESTONE:
|
||||
return $xaction->getNewValue();
|
||||
case PhabricatorProjectTransaction::TYPE_SLUGS:
|
||||
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);
|
||||
|
@ -127,7 +114,21 @@ final class PhabricatorProjectTransactionEditor
|
|||
$object->setParentProjectPHID($xaction->getNewValue());
|
||||
return;
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -239,6 +240,84 @@ final class PhabricatorProjectTransactionEditor
|
|||
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(
|
||||
PhabricatorLiskDAO $object,
|
||||
$type,
|
||||
|
@ -367,25 +446,29 @@ final class PhabricatorProjectTransactionEditor
|
|||
|
||||
break;
|
||||
case PhabricatorProjectTransaction::TYPE_PARENT:
|
||||
case PhabricatorProjectTransaction::TYPE_MILESTONE:
|
||||
if (!$xactions) {
|
||||
break;
|
||||
}
|
||||
|
||||
$xaction = last($xactions);
|
||||
|
||||
$parent_phid = $xaction->getNewValue();
|
||||
if (!$parent_phid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->getIsNewObject()) {
|
||||
$errors[] = new PhabricatorApplicationTransactionValidationError(
|
||||
$type,
|
||||
pht('Invalid'),
|
||||
pht(
|
||||
'You can only set a parent project when creating a project '.
|
||||
'for the first time.'),
|
||||
'You can only set a parent or milestone project when creating a '.
|
||||
'project for the first time.'),
|
||||
$xaction);
|
||||
break;
|
||||
}
|
||||
|
||||
$parent_phid = $xaction->getNewValue();
|
||||
|
||||
$projects = id(new PhabricatorProjectQuery())
|
||||
->setViewer($this->requireActor())
|
||||
->withPHIDs(array($parent_phid))
|
||||
|
@ -400,8 +483,8 @@ final class PhabricatorProjectTransactionEditor
|
|||
$type,
|
||||
pht('Invalid'),
|
||||
pht(
|
||||
'Parent project PHID ("%s") must be the PHID of a valid, '.
|
||||
'visible project which you have permission to edit.',
|
||||
'Parent or milestone project PHID ("%s") must be the PHID of a '.
|
||||
'valid, visible project which you have permission to edit.',
|
||||
$parent_phid),
|
||||
$xaction);
|
||||
break;
|
||||
|
@ -414,8 +497,8 @@ final class PhabricatorProjectTransactionEditor
|
|||
$type,
|
||||
pht('Invalid'),
|
||||
pht(
|
||||
'Parent project PHID ("%s") must not be a milestone. '.
|
||||
'Milestones may not have subprojects.',
|
||||
'Parent or milestone project PHID ("%s") must not be a '.
|
||||
'milestone. Milestones may not have subprojects or milestones.',
|
||||
$parent_phid),
|
||||
$xaction);
|
||||
break;
|
||||
|
@ -427,9 +510,9 @@ final class PhabricatorProjectTransactionEditor
|
|||
$type,
|
||||
pht('Invalid'),
|
||||
pht(
|
||||
'You can not create a subproject under this parent because '.
|
||||
'it would nest projects too deeply. The maximum nesting '.
|
||||
'depth of projects is %s.',
|
||||
'You can not create a subproject or mielstone under this parent '.
|
||||
'because it would nest projects too deeply. The maximum '.
|
||||
'nesting depth of projects is %s.',
|
||||
new PhutilNumber($limit)),
|
||||
$xaction);
|
||||
break;
|
||||
|
@ -611,6 +694,7 @@ final class PhabricatorProjectTransactionEditor
|
|||
array $xactions) {
|
||||
|
||||
$materialize = false;
|
||||
$new_parent = null;
|
||||
foreach ($xactions as $xaction) {
|
||||
switch ($xaction->getTransactionType()) {
|
||||
case PhabricatorTransactions::TYPE_EDGE:
|
||||
|
@ -622,10 +706,34 @@ final class PhabricatorProjectTransactionEditor
|
|||
break;
|
||||
case PhabricatorProjectTransaction::TYPE_PARENT:
|
||||
$materialize = true;
|
||||
$new_parent = $object->getParentProject();
|
||||
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) {
|
||||
id(new PhabricatorProjectsMembershipIndexEngineExtension())
|
||||
->rematerialize($object);
|
||||
|
|
|
@ -5,6 +5,27 @@ final class PhabricatorProjectEditEngine
|
|||
|
||||
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() {
|
||||
return pht('Projects');
|
||||
}
|
||||
|
@ -50,6 +71,22 @@ final class PhabricatorProjectEditEngine
|
|||
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() {
|
||||
return $this->getApplication()->getPolicy(
|
||||
ProjectCreateProjectsCapability::CAPABILITY);
|
||||
|
@ -65,6 +102,8 @@ final class PhabricatorProjectEditEngine
|
|||
$configuration
|
||||
->setFieldOrder(
|
||||
array(
|
||||
'parent',
|
||||
'milestone',
|
||||
'name',
|
||||
'std:project:internal:description',
|
||||
'icon',
|
||||
|
@ -84,7 +123,52 @@ final class PhabricatorProjectEditEngine
|
|||
unset($slugs[$object->getPrimarySlug()]);
|
||||
$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(
|
||||
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())
|
||||
->setKey('name')
|
||||
->setLabel(pht('Name'))
|
||||
|
|
|
@ -164,42 +164,15 @@ protected function buildQueryFromParameters(array $map) {
|
|||
array $handles) {
|
||||
assert_instances_of($projects, 'PhabricatorProject');
|
||||
$viewer = $this->requireViewer();
|
||||
$handles = $viewer->loadHandles(mpull($projects, 'getPHID'));
|
||||
|
||||
$list = new PHUIObjectItemListView();
|
||||
$list->setUser($viewer);
|
||||
$can_edit_projects = id(new PhabricatorPolicyFilter())
|
||||
->setViewer($viewer)
|
||||
->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;
|
||||
$list = id(new PhabricatorProjectListView())
|
||||
->setUser($viewer)
|
||||
->setProjects($projects)
|
||||
->renderList();
|
||||
|
||||
return id(new PhabricatorApplicationSearchResultView())
|
||||
->setObjectList($list)
|
||||
->setNoDataString(pht('No projects found.'));
|
||||
}
|
||||
|
||||
protected function getNewUserBody() {
|
||||
|
|
|
@ -88,6 +88,10 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||
}
|
||||
|
||||
public function getPolicy($capability) {
|
||||
if ($this->isMilestone()) {
|
||||
return $this->getParentProject()->getPolicy($capability);
|
||||
}
|
||||
|
||||
switch ($capability) {
|
||||
case PhabricatorPolicyCapability::CAN_VIEW:
|
||||
return $this->getViewPolicy();
|
||||
|
@ -99,6 +103,12 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||
}
|
||||
|
||||
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
|
||||
if ($this->isMilestone()) {
|
||||
return $this->getParentProject()->hasAutomaticCapability(
|
||||
$capability,
|
||||
$viewer);
|
||||
}
|
||||
|
||||
$can_edit = PhabricatorPolicyCapability::CAN_EDIT;
|
||||
|
||||
switch ($capability) {
|
||||
|
@ -437,6 +447,34 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||
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 )----------------------------------- */
|
||||
|
||||
|
|
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};
|
||||
}
|
||||
|
||||
.phabricator-icon-nav .phabricator-side-menu .phui-list-item-icon.grey {
|
||||
color: {$lightgreyborder};
|
||||
}
|
||||
|
||||
.phabricator-icon-nav .phabricator-side-menu .phui-list-item-selected {
|
||||
border: none;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue