@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+583 -84
+3 -3
resources/celerity/map.php
··· 7 7 */ 8 8 return array( 9 9 'names' => array( 10 - 'core.pkg.css' => 'a419cf4b', 10 + 'core.pkg.css' => '3ea6dc33', 11 11 'core.pkg.js' => '57dff7df', 12 12 'darkconsole.pkg.js' => 'e7393ebb', 13 13 'differential.pkg.css' => '2de124c9', ··· 114 114 'rsrc/css/font/phui-font-icon-base.css' => 'ecbbb4c2', 115 115 'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82', 116 116 'rsrc/css/layout/phabricator-hovercard-view.css' => '1239cd52', 117 - 'rsrc/css/layout/phabricator-side-menu-view.css' => 'bec2458e', 117 + 'rsrc/css/layout/phabricator-side-menu-view.css' => '91b7a42c', 118 118 'rsrc/css/layout/phabricator-source-code-view.css' => 'cbeef983', 119 119 'rsrc/css/phui/calendar/phui-calendar-day.css' => 'd1cf6f93', 120 120 'rsrc/css/phui/calendar/phui-calendar-list.css' => 'c1c7f338', ··· 762 762 'phabricator-remarkup-css' => '7afb543c', 763 763 'phabricator-search-results-css' => '7dea472c', 764 764 'phabricator-shaped-request' => '7cbe244b', 765 - 'phabricator-side-menu-view-css' => 'bec2458e', 765 + 'phabricator-side-menu-view-css' => '91b7a42c', 766 766 'phabricator-slowvote-css' => 'da0afb1b', 767 767 'phabricator-source-code-view-css' => 'cbeef983', 768 768 'phabricator-standard-page-view' => '3c99cdf4',
+2
src/__phutil_library_map__.php
··· 2855 2855 'PhabricatorProjectIconSet' => 'applications/project/icon/PhabricatorProjectIconSet.php', 2856 2856 'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php', 2857 2857 'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php', 2858 + 'PhabricatorProjectListView' => 'applications/project/view/PhabricatorProjectListView.php', 2858 2859 'PhabricatorProjectLockController' => 'applications/project/controller/PhabricatorProjectLockController.php', 2859 2860 'PhabricatorProjectLogicalAndDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalAndDatasource.php', 2860 2861 'PhabricatorProjectLogicalDatasource' => 'applications/project/typeahead/PhabricatorProjectLogicalDatasource.php', ··· 7206 7207 'PhabricatorProjectHeraldAction' => 'HeraldAction', 7207 7208 'PhabricatorProjectIconSet' => 'PhabricatorIconSet', 7208 7209 'PhabricatorProjectListController' => 'PhabricatorProjectController', 7210 + 'PhabricatorProjectListView' => 'AphrontView', 7209 7211 'PhabricatorProjectLockController' => 'PhabricatorProjectController', 7210 7212 'PhabricatorProjectLogicalAndDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 7211 7213 'PhabricatorProjectLogicalDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
+9 -9
src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php
··· 748 748 ->setNewValue($name); 749 749 750 750 if ($parent) { 751 - $xactions[] = id(new PhabricatorProjectTransaction()) 752 - ->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT) 753 - ->setNewValue($parent->getPHID()); 754 - } 755 - 756 - if ($is_milestone) { 757 - $xactions[] = id(new PhabricatorProjectTransaction()) 758 - ->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE) 759 - ->setNewValue(true); 751 + if ($is_milestone) { 752 + $xactions[] = id(new PhabricatorProjectTransaction()) 753 + ->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE) 754 + ->setNewValue($parent->getPHID()); 755 + } else { 756 + $xactions[] = id(new PhabricatorProjectTransaction()) 757 + ->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT) 758 + ->setNewValue($parent->getPHID()); 759 + } 760 760 } 761 761 762 762 $this->applyTransactions($project, $user, $xactions);
+23 -6
src/applications/project/controller/PhabricatorProjectController.php
··· 99 99 $nav->addFilter("board/{$id}/", pht('Workboard')); 100 100 $nav->addFilter("members/{$id}/", pht('Members')); 101 101 $nav->addFilter("feed/{$id}/", pht('Feed')); 102 - $nav->addFilter("details/{$id}/", pht('Edit Details')); 103 102 } 104 103 $nav->addFilter('create', pht('Create Project')); 105 104 } ··· 149 148 150 149 $nav->addIcon("feed/{$id}/", pht('Feed'), 'fa-newspaper-o'); 151 150 $nav->addIcon("members/{$id}/", pht('Members'), 'fa-group'); 152 - $nav->addIcon("details/{$id}/", pht('Edit Details'), 'fa-pencil'); 153 151 154 152 if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { 155 - $nav->addIcon("subprojects/{$id}/", pht('Subprojects'), 'fa-sitemap'); 156 - $nav->addIcon("milestones/{$id}/", pht('Milestones'), 'fa-map-marker'); 153 + if ($project->supportsSubprojects()) { 154 + $subprojects_icon = 'fa-sitemap'; 155 + } else { 156 + $subprojects_icon = 'fa-sitemap grey'; 157 + } 158 + 159 + if ($project->supportsMilestones()) { 160 + $milestones_icon = 'fa-map-marker'; 161 + } else { 162 + $milestones_icon = 'fa-map-marker grey'; 163 + } 164 + 165 + $nav->addIcon( 166 + "subprojects/{$id}/", 167 + pht('Subprojects'), 168 + $subprojects_icon); 169 + 170 + $nav->addIcon( 171 + "milestones/{$id}/", 172 + pht('Milestones'), 173 + $milestones_icon); 157 174 } 158 175 159 176 ··· 170 187 $ancestors[] = $project; 171 188 foreach ($ancestors as $ancestor) { 172 189 $crumbs->addTextCrumb( 173 - $project->getName(), 174 - $project->getURI()); 190 + $ancestor->getName(), 191 + $ancestor->getURI()); 175 192 } 176 193 } 177 194
+104 -3
src/applications/project/controller/PhabricatorProjectEditController.php
··· 3 3 final class PhabricatorProjectEditController 4 4 extends PhabricatorProjectController { 5 5 6 + private $engine; 7 + 8 + public function setEngine(PhabricatorProjectEditEngine $engine) { 9 + $this->engine = $engine; 10 + return $this; 11 + } 12 + 13 + public function getEngine() { 14 + return $this->engine; 15 + } 16 + 6 17 public function handleRequest(AphrontRequest $request) { 7 - return id(new PhabricatorProjectEditEngine()) 8 - ->setController($this) 9 - ->buildResponse(); 18 + $viewer = $this->getViewer(); 19 + 20 + $engine = id(new PhabricatorProjectEditEngine()) 21 + ->setController($this); 22 + 23 + $this->setEngine($engine); 24 + 25 + $id = $request->getURIData('id'); 26 + if (!$id) { 27 + $parent_id = head($request->getArr('parent')); 28 + if (!$parent_id) { 29 + $parent_id = $request->getStr('parent'); 30 + } 31 + 32 + if ($parent_id) { 33 + $is_milestone = false; 34 + } else { 35 + $parent_id = head($request->getArr('milestone')); 36 + if (!$parent_id) { 37 + $parent_id = $request->getStr('milestone'); 38 + } 39 + $is_milestone = true; 40 + } 41 + 42 + if ($parent_id) { 43 + $query = id(new PhabricatorProjectQuery()) 44 + ->setViewer($viewer) 45 + ->requireCapabilities( 46 + array( 47 + PhabricatorPolicyCapability::CAN_VIEW, 48 + PhabricatorPolicyCapability::CAN_EDIT, 49 + )); 50 + 51 + if (ctype_digit($parent_id)) { 52 + $query->withIDs(array($parent_id)); 53 + } else { 54 + $query->withPHIDs(array($parent_id)); 55 + } 56 + 57 + $parent = $query->executeOne(); 58 + 59 + if ($is_milestone) { 60 + if (!$parent->supportsMilestones()) { 61 + $cancel_uri = "/project/milestones/{$parent_id}/"; 62 + return $this->newDialog() 63 + ->setTitle(pht('No Milestones')) 64 + ->appendParagraph( 65 + pht('You can not add milestones to this project.')) 66 + ->addCancelButton($cancel_uri); 67 + } 68 + $engine->setMilestoneProject($parent); 69 + } else { 70 + if (!$parent->supportsSubprojects()) { 71 + $cancel_uri = "/project/subprojects/{$parent_id}/"; 72 + return $this->newDialog() 73 + ->setTitle(pht('No Subprojects')) 74 + ->appendParagraph( 75 + pht('You can not add subprojects to this project.')) 76 + ->addCancelButton($cancel_uri); 77 + } 78 + $engine->setParentProject($parent); 79 + } 80 + 81 + $this->setProject($parent); 82 + } 83 + } 84 + 85 + return $engine->buildResponse(); 86 + } 87 + 88 + protected function buildApplicationCrumbs() { 89 + $crumbs = parent::buildApplicationCrumbs(); 90 + 91 + $engine = $this->getEngine(); 92 + if ($engine) { 93 + $parent = $engine->getParentProject(); 94 + if ($parent) { 95 + $id = $parent->getID(); 96 + $crumbs->addTextCrumb( 97 + pht('Subprojects'), 98 + $this->getApplicationURI("subprojects/{$id}/")); 99 + } 100 + 101 + $milestone = $engine->getMilestoneProject(); 102 + if ($milestone) { 103 + $id = $milestone->getID(); 104 + $crumbs->addTextCrumb( 105 + pht('Milestones'), 106 + $this->getApplicationURI("milestones/{$id}/")); 107 + } 108 + } 109 + 110 + return $crumbs; 10 111 } 11 112 12 113 }
+3 -1
src/applications/project/controller/PhabricatorProjectMembersEditController.php
··· 68 68 $project, 69 69 PhabricatorPolicyCapability::CAN_EDIT); 70 70 71 + $supports_edit = $project->supportsEditMembers(); 72 + 71 73 $form_box = null; 72 74 $title = pht('Add Members'); 73 - if ($can_edit) { 75 + if ($can_edit && $supports_edit) { 74 76 $header_name = pht('Edit Members'); 75 77 $view_uri = $this->getApplicationURI('profile/'.$project->getID().'/'); 76 78
+60 -1
src/applications/project/controller/PhabricatorProjectMilestonesController.php
··· 18 18 $project = $this->getProject(); 19 19 $id = $project->getID(); 20 20 21 + $can_edit = PhabricatorPolicyFilter::hasCapability( 22 + $viewer, 23 + $project, 24 + PhabricatorPolicyCapability::CAN_EDIT); 25 + 26 + $has_support = $project->supportsMilestones(); 27 + if ($has_support) { 28 + $milestones = id(new PhabricatorProjectQuery()) 29 + ->setViewer($viewer) 30 + ->withParentProjectPHIDs(array($project->getPHID())) 31 + ->needImages(true) 32 + ->withIsMilestone(true) 33 + ->setOrder('newest') 34 + ->execute(); 35 + } else { 36 + $milestones = array(); 37 + } 38 + 39 + $can_create = $can_edit && $has_support; 40 + 41 + if ($project->getHasMilestones()) { 42 + $button_text = pht('Create Next Milestone'); 43 + } else { 44 + $button_text = pht('Add Milestones'); 45 + } 46 + 47 + $header = id(new PHUIHeaderView()) 48 + ->setHeader(pht('Milestones')) 49 + ->addActionLink( 50 + id(new PHUIButtonView()) 51 + ->setTag('a') 52 + ->setHref("/project/edit/?milestone={$id}") 53 + ->setIconFont('fa-plus') 54 + ->setDisabled(!$can_create) 55 + ->setWorkflow(!$can_create) 56 + ->setText($button_text)); 57 + 58 + $box = id(new PHUIObjectBoxView()) 59 + ->setHeader($header); 60 + 61 + if (!$has_support) { 62 + $no_support = pht( 63 + 'This project is a milestone. Milestones can not have their own '. 64 + 'milestones.'); 65 + 66 + $info_view = id(new PHUIInfoView()) 67 + ->setErrors(array($no_support)) 68 + ->setSeverity(PHUIInfoView::SEVERITY_WARNING); 69 + 70 + $box->setInfoView($info_view); 71 + } 72 + 73 + $box->setObjectList( 74 + id(new PhabricatorProjectListView()) 75 + ->setUser($viewer) 76 + ->setProjects($milestones) 77 + ->renderList()); 78 + 21 79 $nav = $this->buildIconNavView($project); 22 80 $nav->selectFilter("milestones/{$id}/"); 23 81 ··· 27 85 return $this->newPage() 28 86 ->setNavigation($nav) 29 87 ->setCrumbs($crumbs) 30 - ->setTitle(array($project->getName(), pht('Milestones'))); 88 + ->setTitle(array($project->getName(), pht('Milestones'))) 89 + ->appendChild($box); 31 90 } 32 91 33 92 }
+59 -1
src/applications/project/controller/PhabricatorProjectSubprojectsController.php
··· 18 18 $project = $this->getProject(); 19 19 $id = $project->getID(); 20 20 21 + $can_edit = PhabricatorPolicyFilter::hasCapability( 22 + $viewer, 23 + $project, 24 + PhabricatorPolicyCapability::CAN_EDIT); 25 + 26 + $has_support = $project->supportsSubprojects(); 27 + 28 + if ($has_support) { 29 + $subprojects = id(new PhabricatorProjectQuery()) 30 + ->setViewer($viewer) 31 + ->withParentProjectPHIDs(array($project->getPHID())) 32 + ->needImages(true) 33 + ->withIsMilestone(false) 34 + ->execute(); 35 + } else { 36 + $subprojects = array(); 37 + } 38 + 39 + $can_create = $can_edit && $has_support; 40 + 41 + if ($project->getHasSubprojects()) { 42 + $button_text = pht('Create Subproject'); 43 + } else { 44 + $button_text = pht('Add Subprojects'); 45 + } 46 + 47 + $header = id(new PHUIHeaderView()) 48 + ->setHeader(pht('Subprojects')) 49 + ->addActionLink( 50 + id(new PHUIButtonView()) 51 + ->setTag('a') 52 + ->setHref("/project/edit/?parent={$id}") 53 + ->setIconFont('fa-plus') 54 + ->setDisabled(!$can_create) 55 + ->setWorkflow(!$can_create) 56 + ->setText($button_text)); 57 + 58 + $box = id(new PHUIObjectBoxView()) 59 + ->setHeader($header); 60 + 61 + if (!$has_support) { 62 + $no_support = pht( 63 + 'This project is a milestone. Milestones can not have subprojects.'); 64 + 65 + $info_view = id(new PHUIInfoView()) 66 + ->setErrors(array($no_support)) 67 + ->setSeverity(PHUIInfoView::SEVERITY_WARNING); 68 + 69 + $box->setInfoView($info_view); 70 + } 71 + 72 + $box->setObjectList( 73 + id(new PhabricatorProjectListView()) 74 + ->setUser($viewer) 75 + ->setProjects($subprojects) 76 + ->renderList()); 77 + 21 78 $nav = $this->buildIconNavView($project); 22 79 $nav->selectFilter("subprojects/{$id}/"); 23 80 ··· 27 84 return $this->newPage() 28 85 ->setNavigation($nav) 29 86 ->setCrumbs($crumbs) 30 - ->setTitle(array($project->getName(), pht('Subprojects'))); 87 + ->setTitle(array($project->getName(), pht('Subprojects'))) 88 + ->appendChild($box); 31 89 } 32 90 33 91 }
+134 -26
src/applications/project/editor/PhabricatorProjectTransactionEditor.php
··· 74 74 case PhabricatorProjectTransaction::TYPE_COLOR: 75 75 case PhabricatorProjectTransaction::TYPE_LOCKED: 76 76 case PhabricatorProjectTransaction::TYPE_PARENT: 77 + case PhabricatorProjectTransaction::TYPE_MILESTONE: 77 78 return $xaction->getNewValue(); 78 79 case PhabricatorProjectTransaction::TYPE_SLUGS: 79 80 return $this->normalizeSlugs($xaction->getNewValue()); 80 - case PhabricatorProjectTransaction::TYPE_MILESTONE: 81 - $current = queryfx_one( 82 - $object->establishConnection('w'), 83 - 'SELECT MAX(milestoneNumber) n 84 - FROM %T 85 - WHERE parentProjectPHID = %s', 86 - $object->getTableName(), 87 - $object->getParentProject()->getPHID()); 88 - if (!$current) { 89 - $number = 1; 90 - } else { 91 - $number = (int)$current['n'] + 1; 92 - } 93 - return $number; 94 81 } 95 82 96 83 return parent::getCustomTransactionNewValue($object, $xaction); ··· 127 114 $object->setParentProjectPHID($xaction->getNewValue()); 128 115 return; 129 116 case PhabricatorProjectTransaction::TYPE_MILESTONE: 130 - $object->setMilestoneNumber($xaction->getNewValue()); 117 + $current = queryfx_one( 118 + $object->establishConnection('w'), 119 + 'SELECT MAX(milestoneNumber) n 120 + FROM %T 121 + WHERE parentProjectPHID = %s', 122 + $object->getTableName(), 123 + $object->getParentProject()->getPHID()); 124 + if (!$current) { 125 + $number = 1; 126 + } else { 127 + $number = (int)$current['n'] + 1; 128 + } 129 + 130 + $object->setMilestoneNumber($number); 131 + $object->setParentProjectPHID($xaction->getNewValue()); 131 132 return; 132 133 } 133 134 ··· 239 240 return parent::applyBuiltinExternalTransaction($object, $xaction); 240 241 } 241 242 243 + protected function validateAllTransactions( 244 + PhabricatorLiskDAO $object, 245 + array $xactions) { 246 + 247 + $errors = array(); 248 + 249 + // Prevent creating projects which are both subprojects and milestones, 250 + // since this does not make sense, won't work, and will break everything. 251 + $parent_xaction = null; 252 + foreach ($xactions as $xaction) { 253 + switch ($xaction->getTransactionType()) { 254 + case PhabricatorProjectTransaction::TYPE_PARENT: 255 + case PhabricatorProjectTransaction::TYPE_MILESTONE: 256 + if ($xaction->getNewValue() === null) { 257 + continue; 258 + } 259 + 260 + if (!$parent_xaction) { 261 + $parent_xaction = $xaction; 262 + continue; 263 + } 264 + 265 + $errors[] = new PhabricatorApplicationTransactionValidationError( 266 + $xaction->getTransactionType(), 267 + pht('Invalid'), 268 + pht( 269 + 'When creating a project, specify a maximum of one parent '. 270 + 'project or milestone project. A project can not be both a '. 271 + 'subproject and a milestone.'), 272 + $xaction); 273 + break; 274 + break; 275 + } 276 + } 277 + 278 + $is_milestone = $object->isMilestone(); 279 + foreach ($xactions as $xaction) { 280 + switch ($xaction->getTransactionType()) { 281 + case PhabricatorProjectTransaction::TYPE_MILESTONE: 282 + if ($xaction->getNewValue() !== null) { 283 + $is_milestone = true; 284 + } 285 + break; 286 + } 287 + } 288 + 289 + $is_parent = $object->getHasSubprojects(); 290 + 291 + foreach ($xactions as $xaction) { 292 + switch ($xaction->getTransactionType()) { 293 + case PhabricatorProjectTransaction::TYPE_MEMBERS: 294 + if ($is_parent) { 295 + $errors[] = new PhabricatorApplicationTransactionValidationError( 296 + $xaction->getTransactionType(), 297 + pht('Invalid'), 298 + pht( 299 + 'You can not change members of a project with subprojects '. 300 + 'directly. Members of any subproject are automatically '. 301 + 'members of the parent project.'), 302 + $xaction); 303 + } 304 + 305 + if ($is_milestone) { 306 + $errors[] = new PhabricatorApplicationTransactionValidationError( 307 + $xaction->getTransactionType(), 308 + pht('Invalid'), 309 + pht( 310 + 'You can not change members of a milestone. Members of the '. 311 + 'parent project are automatically members of the milestone.'), 312 + $xaction); 313 + } 314 + break; 315 + } 316 + } 317 + 318 + return $errors; 319 + } 320 + 242 321 protected function validateTransaction( 243 322 PhabricatorLiskDAO $object, 244 323 $type, ··· 367 446 368 447 break; 369 448 case PhabricatorProjectTransaction::TYPE_PARENT: 449 + case PhabricatorProjectTransaction::TYPE_MILESTONE: 370 450 if (!$xactions) { 371 451 break; 372 452 } 373 453 374 454 $xaction = last($xactions); 455 + 456 + $parent_phid = $xaction->getNewValue(); 457 + if (!$parent_phid) { 458 + continue; 459 + } 375 460 376 461 if (!$this->getIsNewObject()) { 377 462 $errors[] = new PhabricatorApplicationTransactionValidationError( 378 463 $type, 379 464 pht('Invalid'), 380 465 pht( 381 - 'You can only set a parent project when creating a project '. 382 - 'for the first time.'), 466 + 'You can only set a parent or milestone project when creating a '. 467 + 'project for the first time.'), 383 468 $xaction); 384 469 break; 385 470 } 386 471 387 - $parent_phid = $xaction->getNewValue(); 388 - 389 472 $projects = id(new PhabricatorProjectQuery()) 390 473 ->setViewer($this->requireActor()) 391 474 ->withPHIDs(array($parent_phid)) ··· 400 483 $type, 401 484 pht('Invalid'), 402 485 pht( 403 - 'Parent project PHID ("%s") must be the PHID of a valid, '. 404 - 'visible project which you have permission to edit.', 486 + 'Parent or milestone project PHID ("%s") must be the PHID of a '. 487 + 'valid, visible project which you have permission to edit.', 405 488 $parent_phid), 406 489 $xaction); 407 490 break; ··· 414 497 $type, 415 498 pht('Invalid'), 416 499 pht( 417 - 'Parent project PHID ("%s") must not be a milestone. '. 418 - 'Milestones may not have subprojects.', 500 + 'Parent or milestone project PHID ("%s") must not be a '. 501 + 'milestone. Milestones may not have subprojects or milestones.', 419 502 $parent_phid), 420 503 $xaction); 421 504 break; ··· 427 510 $type, 428 511 pht('Invalid'), 429 512 pht( 430 - 'You can not create a subproject under this parent because '. 431 - 'it would nest projects too deeply. The maximum nesting '. 432 - 'depth of projects is %s.', 513 + 'You can not create a subproject or mielstone under this parent '. 514 + 'because it would nest projects too deeply. The maximum '. 515 + 'nesting depth of projects is %s.', 433 516 new PhutilNumber($limit)), 434 517 $xaction); 435 518 break; ··· 611 694 array $xactions) { 612 695 613 696 $materialize = false; 697 + $new_parent = null; 614 698 foreach ($xactions as $xaction) { 615 699 switch ($xaction->getTransactionType()) { 616 700 case PhabricatorTransactions::TYPE_EDGE: ··· 622 706 break; 623 707 case PhabricatorProjectTransaction::TYPE_PARENT: 624 708 $materialize = true; 709 + $new_parent = $object->getParentProject(); 625 710 break; 626 711 } 627 712 } 713 + 714 + if ($new_parent) { 715 + // If we just created the first subproject of this parent, we want to 716 + // copy all of the real members to the subproject. 717 + if (!$new_parent->getHasSubprojects()) { 718 + $member_type = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; 719 + 720 + $project_members = PhabricatorEdgeQuery::loadDestinationPHIDs( 721 + $new_parent->getPHID(), 722 + $member_type); 723 + 724 + if ($project_members) { 725 + $editor = id(new PhabricatorEdgeEditor()); 726 + foreach ($project_members as $phid) { 727 + $editor->addEdge($object->getPHID(), $member_type, $phid); 728 + } 729 + $editor->save(); 730 + } 731 + } 732 + } 733 + 734 + // TODO: We should dump an informational transaction onto the parent 735 + // project to show that we created the sub-thing. 628 736 629 737 if ($materialize) { 630 738 id(new PhabricatorProjectsMembershipIndexEngineExtension())
+84
src/applications/project/engine/PhabricatorProjectEditEngine.php
··· 5 5 6 6 const ENGINECONST = 'projects.project'; 7 7 8 + private $parentProject; 9 + private $milestoneProject; 10 + 11 + public function setParentProject(PhabricatorProject $parent_project) { 12 + $this->parentProject = $parent_project; 13 + return $this; 14 + } 15 + 16 + public function getParentProject() { 17 + return $this->parentProject; 18 + } 19 + 20 + public function setMilestoneProject(PhabricatorProject $milestone_project) { 21 + $this->milestoneProject = $milestone_project; 22 + return $this; 23 + } 24 + 25 + public function getMilestoneProject() { 26 + return $this->milestoneProject; 27 + } 28 + 8 29 public function getEngineName() { 9 30 return pht('Projects'); 10 31 } ··· 50 71 return $object->getURI(); 51 72 } 52 73 74 + protected function getObjectCreateCancelURI($object) { 75 + $parent = $this->getParentProject(); 76 + if ($parent) { 77 + $id = $parent->getID(); 78 + return "/project/subprojects/{$id}/"; 79 + } 80 + 81 + $milestone = $this->getMilestoneProject(); 82 + if ($milestone) { 83 + $id = $milestone->getID(); 84 + return "/project/milestones/{$id}/"; 85 + } 86 + 87 + return parent::getObjectCreateCancelURI($object); 88 + } 89 + 53 90 protected function getCreateNewObjectPolicy() { 54 91 return $this->getApplication()->getPolicy( 55 92 ProjectCreateProjectsCapability::CAPABILITY); ··· 65 102 $configuration 66 103 ->setFieldOrder( 67 104 array( 105 + 'parent', 106 + 'milestone', 68 107 'name', 69 108 'std:project:internal:description', 70 109 'icon', ··· 84 123 unset($slugs[$object->getPrimarySlug()]); 85 124 $slugs = array_values($slugs); 86 125 126 + $milestone = $this->getMilestoneProject(); 127 + $parent = $this->getParentProject(); 128 + 129 + if ($parent) { 130 + $parent_phid = $parent->getPHID(); 131 + } else { 132 + $parent_phid = null; 133 + } 134 + 135 + if ($milestone) { 136 + $milestone_phid = $milestone->getPHID(); 137 + } else { 138 + $milestone_phid = null; 139 + } 140 + 87 141 return array( 142 + id(new PhabricatorHandlesEditField()) 143 + ->setKey('parent') 144 + ->setLabel(pht('Parent')) 145 + ->setDescription(pht('Create a subproject of an existing project.')) 146 + ->setConduitDescription( 147 + pht('Choose a parent project to create a subproject beneath.')) 148 + ->setConduitTypeDescription(pht('PHID of the parent project.')) 149 + ->setAliases(array('parentPHID')) 150 + ->setTransactionType(PhabricatorProjectTransaction::TYPE_PARENT) 151 + ->setHandleParameterType(new AphrontPHIDHTTPParameterType()) 152 + ->setSingleValue($parent_phid) 153 + ->setIsReorderable(false) 154 + ->setIsDefaultable(false) 155 + ->setIsLockable(false) 156 + ->setIsLocked(true), 157 + id(new PhabricatorHandlesEditField()) 158 + ->setKey('milestone') 159 + ->setLabel(pht('Milestone Of')) 160 + ->setDescription(pht('Parent project to create a milestone for.')) 161 + ->setConduitDescription( 162 + pht('Choose a parent project to create a new milestone for.')) 163 + ->setConduitTypeDescription(pht('PHID of the parent project.')) 164 + ->setAliases(array('milestonePHID')) 165 + ->setTransactionType(PhabricatorProjectTransaction::TYPE_MILESTONE) 166 + ->setHandleParameterType(new AphrontPHIDHTTPParameterType()) 167 + ->setSingleValue($milestone_phid) 168 + ->setIsReorderable(false) 169 + ->setIsDefaultable(false) 170 + ->setIsLockable(false) 171 + ->setIsLocked(true), 88 172 id(new PhabricatorTextEditField()) 89 173 ->setKey('name') 90 174 ->setLabel(pht('Name'))
+7 -34
src/applications/project/query/PhabricatorProjectSearchEngine.php
··· 164 164 array $handles) { 165 165 assert_instances_of($projects, 'PhabricatorProject'); 166 166 $viewer = $this->requireViewer(); 167 - $handles = $viewer->loadHandles(mpull($projects, 'getPHID')); 168 167 169 - $list = new PHUIObjectItemListView(); 170 - $list->setUser($viewer); 171 - $can_edit_projects = id(new PhabricatorPolicyFilter()) 172 - ->setViewer($viewer) 173 - ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) 174 - ->apply($projects); 168 + $list = id(new PhabricatorProjectListView()) 169 + ->setUser($viewer) 170 + ->setProjects($projects) 171 + ->renderList(); 175 172 176 - foreach ($projects as $key => $project) { 177 - $id = $project->getID(); 178 - 179 - $tag_list = id(new PHUIHandleTagListView()) 180 - ->setSlim(true) 181 - ->setHandles(array($handles[$project->getPHID()])); 182 - 183 - $item = id(new PHUIObjectItemView()) 184 - ->setHeader($project->getName()) 185 - ->setHref($this->getApplicationURI("view/{$id}/")) 186 - ->setImageURI($project->getProfileImageURI()) 187 - ->addAttribute($tag_list); 188 - 189 - if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) { 190 - $item->addIcon('delete-grey', pht('Archived')); 191 - $item->setDisabled(true); 192 - } 193 - 194 - $list->addItem($item); 195 - } 196 - 197 - $result = new PhabricatorApplicationSearchResultView(); 198 - $result->setObjectList($list); 199 - $result->setNoDataString(pht('No projects found.')); 200 - 201 - return $result; 202 - 173 + return id(new PhabricatorApplicationSearchResultView()) 174 + ->setObjectList($list) 175 + ->setNoDataString(pht('No projects found.')); 203 176 } 204 177 205 178 protected function getNewUserBody() {
+38
src/applications/project/storage/PhabricatorProject.php
··· 88 88 } 89 89 90 90 public function getPolicy($capability) { 91 + if ($this->isMilestone()) { 92 + return $this->getParentProject()->getPolicy($capability); 93 + } 94 + 91 95 switch ($capability) { 92 96 case PhabricatorPolicyCapability::CAN_VIEW: 93 97 return $this->getViewPolicy(); ··· 99 103 } 100 104 101 105 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 106 + if ($this->isMilestone()) { 107 + return $this->getParentProject()->hasAutomaticCapability( 108 + $capability, 109 + $viewer); 110 + } 111 + 102 112 $can_edit = PhabricatorPolicyCapability::CAN_EDIT; 103 113 104 114 switch ($capability) { ··· 435 445 } 436 446 437 447 return $ancestors; 448 + } 449 + 450 + public function supportsEditMembers() { 451 + if ($this->isMilestone()) { 452 + return false; 453 + } 454 + 455 + if ($this->getHasSubprojects()) { 456 + return false; 457 + } 458 + 459 + return true; 460 + } 461 + 462 + public function supportsMilestones() { 463 + if ($this->isMilestone()) { 464 + return false; 465 + } 466 + 467 + return true; 468 + } 469 + 470 + public function supportsSubprojects() { 471 + if ($this->isMilestone()) { 472 + return false; 473 + } 474 + 475 + return true; 438 476 } 439 477 440 478
+53
src/applications/project/view/PhabricatorProjectListView.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectListView extends AphrontView { 4 + 5 + private $projects; 6 + 7 + public function setProjects(array $projects) { 8 + $this->projects = $projects; 9 + return $this; 10 + } 11 + 12 + public function getProjects() { 13 + return $this->projects; 14 + } 15 + 16 + public function renderList() { 17 + $viewer = $this->getUser(); 18 + $projects = $this->getProjects(); 19 + 20 + $handles = $viewer->loadHandles(mpull($projects, 'getPHID')); 21 + 22 + $list = id(new PHUIObjectItemListView()) 23 + ->setUser($viewer); 24 + 25 + foreach ($projects as $key => $project) { 26 + $id = $project->getID(); 27 + 28 + $tag_list = id(new PHUIHandleTagListView()) 29 + ->setSlim(true) 30 + ->setHandles(array($handles[$project->getPHID()])); 31 + 32 + $item = id(new PHUIObjectItemView()) 33 + ->setHeader($project->getName()) 34 + ->setHref("/project/view/{$id}/") 35 + ->setImageURI($project->getProfileImageURI()) 36 + ->addAttribute($tag_list); 37 + 38 + if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ARCHIVED) { 39 + $item->addIcon('delete-grey', pht('Archived')); 40 + $item->setDisabled(true); 41 + } 42 + 43 + $list->addItem($item); 44 + } 45 + 46 + return $list; 47 + } 48 + 49 + public function render() { 50 + return $this->renderList(); 51 + } 52 + 53 + }
+4
webroot/rsrc/css/layout/phabricator-side-menu-view.css
··· 89 89 color: {$blue}; 90 90 } 91 91 92 + .phabricator-icon-nav .phabricator-side-menu .phui-list-item-icon.grey { 93 + color: {$lightgreyborder}; 94 + } 95 + 92 96 .phabricator-icon-nav .phabricator-side-menu .phui-list-item-selected { 93 97 border: none; 94 98 }