@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.

Put subprojects and milestones back into the Project UI

Summary: Ref T10010. Restores subprojects and milestones to the UI with a more modern style and more warnings.

Test Plan:
{F1085207}

{F1085208}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10010

Differential Revision: https://secure.phabricator.com/D15152

+539 -138
+4 -2
src/__phutil_library_map__.php
··· 2913 2913 'PhabricatorProjectMembersProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectMembersProfilePanel.php', 2914 2914 'PhabricatorProjectMembersRemoveController' => 'applications/project/controller/PhabricatorProjectMembersRemoveController.php', 2915 2915 'PhabricatorProjectMembersViewController' => 'applications/project/controller/PhabricatorProjectMembersViewController.php', 2916 - 'PhabricatorProjectMilestonesController' => 'applications/project/controller/PhabricatorProjectMilestonesController.php', 2917 2916 'PhabricatorProjectMoveController' => 'applications/project/controller/PhabricatorProjectMoveController.php', 2918 2917 'PhabricatorProjectNameContextFreeGrammar' => 'applications/project/lipsum/PhabricatorProjectNameContextFreeGrammar.php', 2919 2918 'PhabricatorProjectNoProjectsDatasource' => 'applications/project/typeahead/PhabricatorProjectNoProjectsDatasource.php', ··· 2937 2936 'PhabricatorProjectSlug' => 'applications/project/storage/PhabricatorProjectSlug.php', 2938 2937 'PhabricatorProjectStandardCustomField' => 'applications/project/customfield/PhabricatorProjectStandardCustomField.php', 2939 2938 'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php', 2939 + 'PhabricatorProjectSubprojectWarningController' => 'applications/project/controller/PhabricatorProjectSubprojectWarningController.php', 2940 2940 'PhabricatorProjectSubprojectsController' => 'applications/project/controller/PhabricatorProjectSubprojectsController.php', 2941 + 'PhabricatorProjectSubprojectsProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php', 2941 2942 'PhabricatorProjectTestDataGenerator' => 'applications/project/lipsum/PhabricatorProjectTestDataGenerator.php', 2942 2943 'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php', 2943 2944 'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php', ··· 7330 7331 'PhabricatorProjectMembersProfilePanel' => 'PhabricatorProfilePanel', 7331 7332 'PhabricatorProjectMembersRemoveController' => 'PhabricatorProjectController', 7332 7333 'PhabricatorProjectMembersViewController' => 'PhabricatorProjectController', 7333 - 'PhabricatorProjectMilestonesController' => 'PhabricatorProjectController', 7334 7334 'PhabricatorProjectMoveController' => 'PhabricatorProjectController', 7335 7335 'PhabricatorProjectNameContextFreeGrammar' => 'PhutilContextFreeGrammar', 7336 7336 'PhabricatorProjectNoProjectsDatasource' => 'PhabricatorTypeaheadDatasource', ··· 7357 7357 'PhabricatorStandardCustomFieldInterface', 7358 7358 ), 7359 7359 'PhabricatorProjectStatus' => 'Phobject', 7360 + 'PhabricatorProjectSubprojectWarningController' => 'PhabricatorProjectController', 7360 7361 'PhabricatorProjectSubprojectsController' => 'PhabricatorProjectController', 7362 + 'PhabricatorProjectSubprojectsProfilePanel' => 'PhabricatorProfilePanel', 7361 7363 'PhabricatorProjectTestDataGenerator' => 'PhabricatorTestDataGenerator', 7362 7364 'PhabricatorProjectTransaction' => 'PhabricatorApplicationTransaction', 7363 7365 'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
+2
src/applications/project/application/PhabricatorProjectApplication.php
··· 91 91 => 'PhabricatorProjectWatchController', 92 92 'silence/(?P<id>[1-9]\d*)/' 93 93 => 'PhabricatorProjectSilenceController', 94 + 'warning/(?P<id>[1-9]\d*)/' 95 + => 'PhabricatorProjectSubprojectWarningController', 94 96 ), 95 97 '/tag/' => array( 96 98 '(?P<slug>[^/]+)/' => 'PhabricatorProjectViewController',
+5 -11
src/applications/project/controller/PhabricatorProjectEditController.php
··· 42 42 if ($parent_id) { 43 43 $query = id(new PhabricatorProjectQuery()) 44 44 ->setViewer($viewer) 45 + ->needImages(true) 45 46 ->requireCapabilities( 46 47 array( 47 48 PhabricatorPolicyCapability::CAN_VIEW, ··· 58 59 59 60 if ($is_milestone) { 60 61 if (!$parent->supportsMilestones()) { 61 - $cancel_uri = "/project/milestones/{$parent_id}/"; 62 + $cancel_uri = "/project/subprojects/{$parent_id}/"; 62 63 return $this->newDialog() 63 64 ->setTitle(pht('No Milestones')) 64 65 ->appendParagraph( ··· 91 92 $engine = $this->getEngine(); 92 93 if ($engine) { 93 94 $parent = $engine->getParentProject(); 94 - if ($parent) { 95 - $id = $parent->getID(); 95 + $milestone = $engine->getMilestoneProject(); 96 + if ($parent || $milestone) { 97 + $id = nonempty($parent, $milestone)->getID(); 96 98 $crumbs->addTextCrumb( 97 99 pht('Subprojects'), 98 100 $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 101 } 108 102 } 109 103
-92
src/applications/project/controller/PhabricatorProjectMilestonesController.php
··· 1 - <?php 2 - 3 - final class PhabricatorProjectMilestonesController 4 - extends PhabricatorProjectController { 5 - 6 - public function shouldAllowPublic() { 7 - return true; 8 - } 9 - 10 - public function handleRequest(AphrontRequest $request) { 11 - $viewer = $request->getViewer(); 12 - 13 - $response = $this->loadProject(); 14 - if ($response) { 15 - return $response; 16 - } 17 - 18 - $project = $this->getProject(); 19 - $id = $project->getID(); 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 - ->setIcon('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 - 79 - $nav = $this->getProfileMenu(); 80 - $nav->selectFilter(PhabricatorProject::PANEL_MILESTONES); 81 - 82 - $crumbs = $this->buildApplicationCrumbs(); 83 - $crumbs->addTextCrumb(pht('Milestones')); 84 - 85 - return $this->newPage() 86 - ->setNavigation($nav) 87 - ->setCrumbs($crumbs) 88 - ->setTitle(array($project->getName(), pht('Milestones'))) 89 - ->appendChild($box); 90 - } 91 - 92 - }
+90
src/applications/project/controller/PhabricatorProjectProfileController.php
··· 45 45 $watch_action = $this->renderWatchAction($project); 46 46 $header->addActionLink($watch_action); 47 47 48 + $milestone_list = $this->buildMilestoneList($project); 49 + $subproject_list = $this->buildSubprojectList($project); 50 + 48 51 $member_list = id(new PhabricatorProjectMemberListView()) 49 52 ->setUser($viewer) 50 53 ->setProject($project) ··· 82 85 )) 83 86 ->setSideColumn( 84 87 array( 88 + $milestone_list, 89 + $subproject_list, 85 90 $member_list, 86 91 $watcher_list, 87 92 )); ··· 176 181 ->setHref($watch_href); 177 182 } 178 183 184 + private function buildMilestoneList(PhabricatorProject $project) { 185 + if (!$project->getHasMilestones()) { 186 + return null; 187 + } 188 + 189 + $viewer = $this->getViewer(); 190 + $id = $project->getID(); 191 + 192 + $milestones = id(new PhabricatorProjectQuery()) 193 + ->setViewer($viewer) 194 + ->withParentProjectPHIDs(array($project->getPHID())) 195 + ->needImages(true) 196 + ->withIsMilestone(true) 197 + ->setOrder('newest') 198 + ->execute(); 199 + if (!$milestones) { 200 + return null; 201 + } 202 + 203 + $milestone_list = id(new PhabricatorProjectListView()) 204 + ->setUser($viewer) 205 + ->setProjects($milestones) 206 + ->renderList(); 207 + 208 + $view_all = id(new PHUIButtonView()) 209 + ->setTag('a') 210 + ->setIcon( 211 + id(new PHUIIconView()) 212 + ->setIcon('fa-list-ul')) 213 + ->setText(pht('View All')) 214 + ->setHref("/project/subprojects/{$id}/"); 215 + 216 + $header = id(new PHUIHeaderView()) 217 + ->setHeader(pht('Milestones')) 218 + ->addActionLink($view_all); 219 + 220 + return id(new PHUIObjectBoxView()) 221 + ->setHeader($header) 222 + ->setBackground(PHUIBoxView::GREY) 223 + ->setObjectList($milestone_list); 224 + } 225 + 226 + private function buildSubprojectList(PhabricatorProject $project) { 227 + if (!$project->getHasSubprojects()) { 228 + return null; 229 + } 230 + 231 + $viewer = $this->getViewer(); 232 + $id = $project->getID(); 233 + 234 + $limit = 25; 235 + 236 + $subprojects = id(new PhabricatorProjectQuery()) 237 + ->setViewer($viewer) 238 + ->withParentProjectPHIDs(array($project->getPHID())) 239 + ->needImages(true) 240 + ->withIsMilestone(false) 241 + ->setLimit($limit) 242 + ->execute(); 243 + if (!$subprojects) { 244 + return null; 245 + } 246 + 247 + $subproject_list = id(new PhabricatorProjectListView()) 248 + ->setUser($viewer) 249 + ->setProjects($subprojects) 250 + ->renderList(); 251 + 252 + $view_all = id(new PHUIButtonView()) 253 + ->setTag('a') 254 + ->setIcon( 255 + id(new PHUIIconView()) 256 + ->setIcon('fa-list-ul')) 257 + ->setText(pht('View All')) 258 + ->setHref("/project/subprojects/{$id}/"); 259 + 260 + $header = id(new PHUIHeaderView()) 261 + ->setHeader(pht('Subprojects')) 262 + ->addActionLink($view_all); 263 + 264 + return id(new PHUIObjectBoxView()) 265 + ->setHeader($header) 266 + ->setBackground(PHUIBoxView::GREY) 267 + ->setObjectList($subproject_list); 268 + } 179 269 180 270 }
+51
src/applications/project/controller/PhabricatorProjectSubprojectWarningController.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectSubprojectWarningController 4 + extends PhabricatorProjectController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $viewer = $request->getViewer(); 8 + 9 + $response = $this->loadProject(); 10 + if ($response) { 11 + return $response; 12 + } 13 + 14 + $project = $this->getProject(); 15 + 16 + $can_edit = PhabricatorPolicyFilter::hasCapability( 17 + $viewer, 18 + $project, 19 + PhabricatorPolicyCapability::CAN_EDIT); 20 + 21 + if (!$can_edit) { 22 + return new Aphront404Response(); 23 + } 24 + 25 + $id = $project->getID(); 26 + $cancel_uri = "/project/subprojects/{$id}/"; 27 + $done_uri = "/project/edit/?parent={$id}"; 28 + 29 + if ($request->isFormPost()) { 30 + return id(new AphrontRedirectResponse()) 31 + ->setURI($done_uri); 32 + } 33 + 34 + $doc_href = PhabricatorEnv::getDoclink('Projects User Guide'); 35 + 36 + $conversion_help = pht( 37 + "Creating a project's first subproject **moves all ". 38 + "members** and **destroys all workboard columns**.". 39 + "\n\n". 40 + "See [[ %s | Projects User Guide ]] in the documentation for details. ". 41 + "This process can not be undone.", 42 + $doc_href); 43 + 44 + return $this->newDialog() 45 + ->setTitle(pht('Convert to Parent Project')) 46 + ->appendChild(new PHUIRemarkupView($viewer, $conversion_help)) 47 + ->addCancelButton($cancel_uri) 48 + ->addSubmitButton(pht('Convert Project')); 49 + } 50 + 51 + }
+191 -33
src/applications/project/controller/PhabricatorProjectSubprojectsController.php
··· 23 23 $project, 24 24 PhabricatorPolicyCapability::CAN_EDIT); 25 25 26 - $has_support = $project->supportsSubprojects(); 26 + $allows_subprojects = $project->supportsSubprojects(); 27 + $allows_milestones = $project->supportsMilestones(); 27 28 28 - if ($has_support) { 29 + if ($allows_subprojects) { 29 30 $subprojects = id(new PhabricatorProjectQuery()) 30 31 ->setViewer($viewer) 31 32 ->withParentProjectPHIDs(array($project->getPHID())) ··· 36 37 $subprojects = array(); 37 38 } 38 39 39 - $can_create = $can_edit && $has_support; 40 - 41 - if ($project->getHasSubprojects()) { 42 - $button_text = pht('Create Subproject'); 40 + if ($allows_milestones) { 41 + $milestones = id(new PhabricatorProjectQuery()) 42 + ->setViewer($viewer) 43 + ->withParentProjectPHIDs(array($project->getPHID())) 44 + ->needImages(true) 45 + ->withIsMilestone(true) 46 + ->setOrder('newest') 47 + ->execute(); 43 48 } else { 44 - $button_text = pht('Add Subprojects'); 49 + $milestones = array(); 45 50 } 46 51 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 - ->setIcon('fa-plus') 54 - ->setDisabled(!$can_create) 55 - ->setWorkflow(!$can_create) 56 - ->setText($button_text)); 52 + if ($milestones) { 53 + $milestone_list = id(new PHUIObjectBoxView()) 54 + ->setHeaderText(pht('Milestones')) 55 + ->setObjectList( 56 + id(new PhabricatorProjectListView()) 57 + ->setUser($viewer) 58 + ->setProjects($milestones) 59 + ->renderList()); 60 + } else { 61 + $milestone_list = null; 62 + } 57 63 58 - $box = id(new PHUIObjectBoxView()) 59 - ->setHeader($header); 64 + if ($subprojects) { 65 + $subproject_list = id(new PHUIObjectBoxView()) 66 + ->setHeaderText(pht('Subprojects')) 67 + ->setObjectList( 68 + id(new PhabricatorProjectListView()) 69 + ->setUser($viewer) 70 + ->setProjects($subprojects) 71 + ->renderList()); 72 + } else { 73 + $subproject_list = null; 74 + } 60 75 61 - if (!$has_support) { 62 - $no_support = pht( 63 - 'This project is a milestone. Milestones can not have subprojects.'); 76 + $property_list = $this->buildPropertyList( 77 + $project, 78 + $milestones, 79 + $subprojects); 64 80 65 - $info_view = id(new PHUIInfoView()) 66 - ->setErrors(array($no_support)) 67 - ->setSeverity(PHUIInfoView::SEVERITY_WARNING); 81 + $action_list = $this->buildActionList( 82 + $project, 83 + $milestones, 84 + $subprojects); 68 85 69 - $box->setInfoView($info_view); 70 - } 86 + $property_list->setActionList($action_list); 71 87 72 - $box->setObjectList( 73 - id(new PhabricatorProjectListView()) 74 - ->setUser($viewer) 75 - ->setProjects($subprojects) 76 - ->renderList()); 88 + $header_box = id(new PHUIObjectBoxView()) 89 + ->setHeaderText(pht('Subprojects and Milestones')) 90 + ->addPropertyList($property_list); 77 91 78 92 $nav = $this->getProfileMenu(); 79 93 $nav->selectFilter(PhabricatorProject::PANEL_SUBPROJECTS); ··· 85 99 ->setNavigation($nav) 86 100 ->setCrumbs($crumbs) 87 101 ->setTitle(array($project->getName(), pht('Subprojects'))) 88 - ->appendChild($box); 102 + ->appendChild( 103 + array( 104 + $header_box, 105 + $milestone_list, 106 + $subproject_list, 107 + )); 89 108 } 109 + 110 + private function buildPropertyList( 111 + PhabricatorProject $project, 112 + array $milestones, 113 + array $subprojects) { 114 + $viewer = $this->getViewer(); 115 + 116 + $view = id(new PHUIPropertyListView()) 117 + ->setUser($viewer); 118 + 119 + $view->addProperty( 120 + pht('Prototype'), 121 + $this->renderStatus( 122 + 'fa-exclamation-triangle red', 123 + pht('Warning'), 124 + pht('Subprojects and milestones are only partially implemented.'))); 125 + 126 + if (!$project->supportsMilestones()) { 127 + $milestone_status = $this->renderStatus( 128 + 'fa-times grey', 129 + pht('Already Milestone'), 130 + pht( 131 + 'This project is already a milestone, and milestones may not '. 132 + 'have their own milestones.')); 133 + } else { 134 + if (!$milestones) { 135 + $milestone_status = $this->renderStatus( 136 + 'fa-check grey', 137 + pht('None Created'), 138 + pht( 139 + 'You can create milestones for this project.')); 140 + } else { 141 + $milestone_status = $this->renderStatus( 142 + 'fa-check green', 143 + pht('Has Milestones'), 144 + pht('This project has milestones.')); 145 + } 146 + } 147 + 148 + $view->addProperty(pht('Milestones'), $milestone_status); 149 + 150 + if (!$project->supportsSubprojects()) { 151 + $subproject_status = $this->renderStatus( 152 + 'fa-times grey', 153 + pht('Milestone'), 154 + pht( 155 + 'This project is a milestone, and milestones may not have '. 156 + 'subprojects.')); 157 + } else { 158 + if (!$subprojects) { 159 + $subproject_status = $this->renderStatus( 160 + 'fa-check grey', 161 + pht('None Created'), 162 + pht('You can create subprojects for this project.')); 163 + } else { 164 + $subproject_status = $this->renderStatus( 165 + 'fa-check green', 166 + pht('Has Subprojects'), 167 + pht( 168 + 'This project has subprojects.')); 169 + } 170 + } 171 + 172 + $view->addProperty(pht('Subprojects'), $subproject_status); 173 + 174 + return $view; 175 + } 176 + 177 + private function buildActionList( 178 + PhabricatorProject $project, 179 + array $milestones, 180 + array $subprojects) { 181 + $viewer = $this->getViewer(); 182 + $id = $project->getID(); 183 + 184 + $can_edit = PhabricatorPolicyFilter::hasCapability( 185 + $viewer, 186 + $project, 187 + PhabricatorPolicyCapability::CAN_EDIT); 188 + 189 + $allows_milestones = $project->supportsMilestones(); 190 + $allows_subprojects = $project->supportsSubprojects(); 191 + 192 + $view = id(new PhabricatorActionListView()) 193 + ->setUser($viewer); 194 + 195 + if ($allows_milestones && $milestones) { 196 + $milestone_text = pht('Create Next Milestone'); 197 + } else { 198 + $milestone_text = pht('Create Milestone'); 199 + } 200 + 201 + $can_milestone = ($can_edit && $allows_milestones); 202 + $milestone_href = "/project/edit/?milestone={$id}"; 203 + 204 + $view->addAction( 205 + id(new PhabricatorActionView()) 206 + ->setName($milestone_text) 207 + ->setIcon('fa-plus') 208 + ->setHref($milestone_href) 209 + ->setDisabled(!$can_milestone) 210 + ->setWorkflow(!$can_milestone)); 211 + 212 + $can_subproject = ($can_edit && $allows_subprojects); 213 + 214 + // If we're offering to create the first subproject, we're going to warn 215 + // the user about the effects before moving forward. 216 + if ($can_subproject && !$subprojects) { 217 + $subproject_href = "/project/warning/{$id}/"; 218 + $subproject_disabled = false; 219 + $subproject_workflow = true; 220 + } else { 221 + $subproject_href = "/project/edit/?parent={$id}"; 222 + $subproject_disabled = !$can_subproject; 223 + $subproject_workflow = !$can_subproject; 224 + } 225 + 226 + $view->addAction( 227 + id(new PhabricatorActionView()) 228 + ->setName(pht('Create Subproject')) 229 + ->setIcon('fa-plus') 230 + ->setHref($subproject_href) 231 + ->setDisabled($subproject_disabled) 232 + ->setWorkflow($subproject_workflow)); 233 + 234 + return $view; 235 + } 236 + 237 + private function renderStatus($icon, $target, $note) { 238 + $item = id(new PHUIStatusItemView()) 239 + ->setIcon($icon) 240 + ->setTarget(phutil_tag('strong', array(), $target)) 241 + ->setNote($note); 242 + 243 + return id(new PHUIStatusListView()) 244 + ->addItem($item); 245 + } 246 + 247 + 90 248 91 249 }
+4
src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php
··· 29 29 ->setPanelKey(PhabricatorProjectMembersProfilePanel::PANELKEY); 30 30 31 31 $panels[] = $this->newPanel() 32 + ->setBuiltinKey(PhabricatorProject::PANEL_SUBPROJECTS) 33 + ->setPanelKey(PhabricatorProjectSubprojectsProfilePanel::PANELKEY); 34 + 35 + $panels[] = $this->newPanel() 32 36 ->setBuiltinKey(PhabricatorProject::PANEL_MANAGE) 33 37 ->setPanelKey(PhabricatorProjectManageProfilePanel::PANELKEY); 34 38
+17
src/applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php
··· 61 61 62 62 $conn_w = $project->establishConnection('w'); 63 63 64 + $any_milestone = queryfx_one( 65 + $conn_w, 66 + 'SELECT id FROM %T 67 + WHERE parentProjectPHID = %s AND milestoneNumber IS NOT NULL 68 + LIMIT 1', 69 + $project->getTableName(), 70 + $project_phid); 71 + $has_milestones = (bool)$any_milestone; 72 + 64 73 $project->openTransaction(); 65 74 66 75 // Delete any existing materialized member edges. ··· 90 99 'UPDATE %T SET hasSubprojects = %d WHERE id = %d', 91 100 $project->getTableName(), 92 101 (int)$has_subprojects, 102 + $project->getID()); 103 + 104 + // Update the hasMilestones flag. 105 + queryfx( 106 + $conn_w, 107 + 'UPDATE %T SET hasMilestones = %d WHERE id = %d', 108 + $project->getTableName(), 109 + (int)$has_milestones, 93 110 $project->getID()); 94 111 95 112 $project->saveTransaction();
+63
src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php
··· 1 + <?php 2 + 3 + final class PhabricatorProjectSubprojectsProfilePanel 4 + extends PhabricatorProfilePanel { 5 + 6 + const PANELKEY = 'project.subprojects'; 7 + 8 + public function getPanelTypeName() { 9 + return pht('Project Subprojects'); 10 + } 11 + 12 + private function getDefaultName() { 13 + return pht('Subprojects'); 14 + } 15 + 16 + public function getDisplayName( 17 + PhabricatorProfilePanelConfiguration $config) { 18 + $name = $config->getPanelProperty('name'); 19 + 20 + if (strlen($name)) { 21 + return $name; 22 + } 23 + 24 + return $this->getDefaultName(); 25 + } 26 + 27 + public function buildEditEngineFields( 28 + PhabricatorProfilePanelConfiguration $config) { 29 + return array( 30 + id(new PhabricatorTextEditField()) 31 + ->setKey('name') 32 + ->setLabel(pht('Name')) 33 + ->setPlaceholder($this->getDefaultName()) 34 + ->setValue($config->getPanelProperty('name')), 35 + ); 36 + } 37 + 38 + protected function newNavigationMenuItems( 39 + PhabricatorProfilePanelConfiguration $config) { 40 + 41 + $project = $config->getProfileObject(); 42 + 43 + $has_children = ($project->getHasSubprojects()) || 44 + ($project->getHasMilestones()); 45 + 46 + $id = $project->getID(); 47 + 48 + $name = $this->getDisplayName($config); 49 + $icon = 'fa-sitemap'; 50 + $href = "/project/subprojects/{$id}/"; 51 + 52 + $item = $this->newItem() 53 + ->setHref($href) 54 + ->setName($name) 55 + ->setDisabled(!$has_children) 56 + ->setIcon($icon); 57 + 58 + return array( 59 + $item, 60 + ); 61 + } 62 + 63 + }
+112
src/docs/user/userguide/projects.diviner
··· 135 135 menu. 136 136 137 137 138 + Subprojects and Milestones 139 + ========================== 140 + 141 + IMPORTANT: This feature is only partially implemented. 142 + 143 + After creating a project, you can use the 144 + {nav icon="sitemap", name="Subprojects"} menu item to add subprojects or 145 + milestones. 146 + 147 + **Subprojects** are projects that are contained inside the main project. You 148 + can use them to break large or complex groups, tags, lists, or undertakings 149 + apart into smaller pieces. 150 + 151 + **Milestones** are a special kind of subproject for organizing tasks into 152 + blocks of work. You can use them to implement sprints, iterations, milestones, 153 + versions, etc. 154 + 155 + Subprojects and milestones have some additional special behaviors and rules, 156 + particularly around policies and membership. See below for details. 157 + 158 + This is a brief summary of the major differences between normal projects, 159 + subprojects, parent projects, and milestones. 160 + 161 + | | Normal | Parent | Subproject | Milestone | 162 + |---|---|---|---|---| 163 + | //Members// | Yes | Union of Subprojects | Yes | Same as Parent | 164 + | //Policies// | Yes | Yes | Affected by Parent | Same as Parent | 165 + | //Workboard// | Yes | No Custom Columns | Yes | Yes | 166 + | //Hashtags// | Yes | Yes | Yes | Special | 167 + 168 + 169 + Subprojects 170 + =========== 171 + 172 + Subprojects are full-power projects that are contained inside some parent 173 + project. You can use them to divide a large or complex project into smaller 174 + parts. 175 + 176 + Subprojects have normal members and normal policies, but note that the policies 177 + of the parent project affect the policies of the subproject (see "Parent 178 + Projects", below). 179 + 180 + Subprojects can have their own subprojects, milestones, or both. If a 181 + subproject has its own subprojects, it is both a subproject and a parent 182 + project. Thus, the parent project rules apply to it, and are stronger than the 183 + subproject rules. 184 + 185 + Subprojects can have normal workboards. 186 + 187 + 188 + Milestones 189 + ========== 190 + 191 + Milestones are simple subprojects for tracking sprints, iterations, versions, 192 + or other similar blocks of work. Milestones make it easier to create and manage 193 + a large number of similar subprojects (for example: {nav Sprint 1}, 194 + {nav Sprint 2}, {nav Sprint 3}, etc). 195 + 196 + Milestones can not have direct members or policies. Instead, the membership 197 + and policies of a milestones are always the same as the milestone's parent 198 + project. This makes large numbers of milestones more manageable when changes 199 + occur. 200 + 201 + Milestones can not have subprojects, and can not have their own milestones. 202 + 203 + By default, Milestones do not have their own hashtags. 204 + 205 + Milestones can have normal workboards. 206 + 207 + 208 + Parent Projects 209 + =============== 210 + 211 + When you add the first subproject to an existing project, it is converted into 212 + a **parent project**. Parent projects have some special rules. 213 + 214 + **No Direct Members**: Parent projects can not have members of their own. 215 + Instead, all of the users who are members of any subproject count as members 216 + of the parent project. By joining (or leaving) a subproject, a user is 217 + implicitly added to (or removed from) all ancestors of that project. 218 + 219 + Consequently, when you add the first subproject to an existing project, all of 220 + the project's current members are moved to become members of the subproject 221 + instead. Implicitly, they will remain members of the parent project because the 222 + parent project is an ancestor of the new subproject. 223 + 224 + You can edit the project afterward to change or remove members if you want to 225 + split membership apart in a more granular way across multiple new subprojects. 226 + 227 + **No Workboard Columns**: Parent projects can not have their own workboard 228 + columns: instead, the workboard of a parent project shows columns representing 229 + the child projects. 230 + 231 + Thus, a project's workboard columns are destroyed when you add the first 232 + subproject. All objects on the workboard will be returned to the project's 233 + backlog. The new board will show columns for subprojects instead. 234 + 235 + **Searching**: When you search for a parent project, results for any subproject 236 + are returned. For example, if you search for {nav Engineering}, your query will 237 + match results in {nav Engineering} itself, but also subprojects like 238 + {nav Engineering > Warp Drive} and {nav Engineering > Shield Batteries}. 239 + 240 + **Policy Effects**: To view a subproject or milestone, you must be able to 241 + view the parent project. As a result, the parent project's view policy now 242 + affects child projects. If you restrict the visibility of the parent, you also 243 + restrict the visibility of the children. 244 + 245 + In contrast, permission to edit a parent project grants permission to edit 246 + any subproject. If a user can {nav Root Project}, they can also edit 247 + {nav Root Project > Child} and {nav Root Project > Child > Sprint 3}. 248 + 249 + 138 250 Policies In Depth 139 251 ================= 140 252