@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
at recaptime-dev/main 246 lines 9.0 kB view raw
1<?php 2 3final class PhabricatorProjectBoardImportController 4 extends PhabricatorProjectBoardController { 5 6 public function handleRequest(AphrontRequest $request) { 7 $viewer = $request->getViewer(); 8 $project_id = $request->getURIData('projectID'); 9 10 $project = id(new PhabricatorProjectQuery()) 11 ->setViewer($viewer) 12 ->requireCapabilities( 13 array( 14 PhabricatorPolicyCapability::CAN_VIEW, 15 PhabricatorPolicyCapability::CAN_EDIT, 16 )) 17 ->withIDs(array($project_id)) 18 ->executeOne(); 19 if (!$project) { 20 return new Aphront404Response(); 21 } 22 $this->setProject($project); 23 24 $project_id = $project->getID(); 25 $board_uri = $this->getApplicationURI("board/{$project_id}/"); 26 27 // See PHI1025. We only want to prevent the import if the board already has 28 // real columns. If it has proxy columns (for example, for milestones) you 29 // can still import columns from another board. 30 $columns = id(new PhabricatorProjectColumnQuery()) 31 ->setViewer($viewer) 32 ->withProjectPHIDs(array($project->getPHID())) 33 ->withIsProxyColumn(false) 34 ->execute(); 35 if ($columns) { 36 return $this->newDialog() 37 ->setTitle(pht('Workboard Already Has Columns')) 38 ->appendParagraph( 39 pht( 40 'You can not import columns into this workboard because it '. 41 'already has columns. You can only import into an empty '. 42 'workboard.')) 43 ->addCancelButton($board_uri); 44 } 45 46 if ($request->isFormPost()) { 47 $import_phid = $request->getArr('importProjectPHID'); 48 $import_phid = reset($import_phid); 49 50 $import_columns = id(new PhabricatorProjectColumnQuery()) 51 ->setViewer($viewer) 52 ->withProjectPHIDs(array($import_phid)) 53 ->withIsProxyColumn(false) 54 ->execute(); 55 if (!$import_columns) { 56 return $this->newDialog() 57 ->setTitle(pht('Source Workboard Has No Columns')) 58 ->appendParagraph( 59 pht( 60 'You can not import columns from that workboard because it has '. 61 'no importable columns.')) 62 ->addCancelButton($board_uri); 63 } 64 65 $table = id(new PhabricatorProjectColumn()) 66 ->openTransaction(); 67 foreach ($import_columns as $import_column) { 68 if ($import_column->isHidden()) { 69 continue; 70 } 71 72 $new_column = PhabricatorProjectColumn::initializeNewColumn($viewer) 73 ->setSequence($import_column->getSequence()) 74 ->setProjectPHID($project->getPHID()) 75 ->setName($import_column->getName()) 76 ->setProperties($import_column->getProperties()) 77 ->save(); 78 } 79 $xactions = array(); 80 $xactions[] = id(new PhabricatorProjectTransaction()) 81 ->setTransactionType( 82 PhabricatorProjectWorkboardTransaction::TRANSACTIONTYPE) 83 ->setNewValue(1); 84 85 id(new PhabricatorProjectTransactionEditor()) 86 ->setActor($viewer) 87 ->setContentSourceFromRequest($request) 88 ->setContinueOnNoEffect(true) 89 ->setContinueOnMissingFields(true) 90 ->applyTransactions($project, $xactions); 91 92 $table->saveTransaction(); 93 94 return id(new AphrontRedirectResponse())->setURI($board_uri); 95 } 96 97 // Default value. The Tokenizer wants an array of phids. 98 $tokenizer_value = array(); 99 100 // Default to the previous milestone, if available. 101 $value_candidate = $this->getPreviousMilestoneIfHasImportableColumns( 102 $viewer, $project); 103 104 if ($value_candidate) { 105 $tokenizer_value[] = $value_candidate->getPHID(); 106 } 107 108 $proj_selector = id(new AphrontFormTokenizerControl()) 109 ->setName('importProjectPHID') 110 ->setUser($viewer) 111 ->setValue($tokenizer_value) 112 ->setDatasource(id(new PhabricatorProjectDatasource()) 113 ->setParameters(array('mustHaveColumns' => true)) 114 ->setLimit(1)); 115 116 return $this->newDialog() 117 ->setTitle(pht('Import Columns')) 118 ->setWidth(AphrontDialogView::WIDTH_FORM) 119 ->appendParagraph(pht('Choose a project or a milestone to import '. 120 'columns from:')) 121 ->appendChild($proj_selector) 122 ->addCancelButton($board_uri) 123 ->addSubmitButton(pht('Import')); 124 } 125 126 /** 127 * Starting from a milestone, get the previous milestone, 128 * but only if it has at least one column that could be imported. 129 * @param PhabricatorUser $viewer Current user 130 * @param PhabricatorProject $milestone Current milestone with no 131 * workboard yet. 132 * Technically, this parameter 133 * could also be a project, 134 * and projects are silently 135 * rejected if passed here. 136 * @return PhabricatorProject|null The milestone preceding 137 * your specified milestone, 138 * but only if it has at least 139 * one importable column; 140 * null in any other case. 141 */ 142 private function getPreviousMilestoneIfHasImportableColumns( 143 PhabricatorUser $viewer, 144 PhabricatorProject $milestone): ?PhabricatorProject { 145 146 // We can suggest something, only if you are creating workboard 147 // on a milestone. 148 if (!$milestone->isMilestone()) { 149 return null; 150 } 151 152 // Possible design choices: 153 // 1. Suggest the most recent milestone (excluding this specific one). 154 // CONS: Suggesting the most recent milestone doesn't make much sense, 155 // when I want to create a workboard from a milestone from 156 // a year ago. 157 // Side note: to suggest the 'most recent', we need at least one 158 // more query to find that number. It needs a "SELECT MAX(n)". 159 // 2. Suggest the precedent milestone. 160 // PRO: convenient when you already worked on a workboard, and you just 161 // create an additional milestone; convenient also when you create 162 // a workboard on a old milestone, so as to maintain the 163 // workboard structure of that time. 164 // Side note: finding the previous milestone is also extremely 165 // easy. We avoid the extra query "SELECT MAX(n)". 166 // 167 // So we go with: 2. Suggest the precedent milestone. 168 $previous_milestone_num = (int)$milestone->getMilestoneNumber(); 169 $previous_milestone_num--; 170 if ($previous_milestone_num < 1) { 171 return null; 172 } 173 174 $previous_milestone = $this->getProjectMilestoneFromNumber( 175 $viewer, 176 $milestone->getParentProjectPHID(), 177 $previous_milestone_num); 178 179 if (!$previous_milestone) { 180 return null; 181 } 182 183 // Micro-optimization to avoid querying columns. 184 if (!$previous_milestone->getHasWorkboard()) { 185 return null; 186 } 187 188 // Check if this milestone has at least one 189 // existing column which could be imported, or we can cause the error 190 // 'Source Workboard Has No Columns', 191 // and it would be more confusing than useful. 192 $example_column = $this->getOneImportableProjectColumn( 193 $viewer, 194 $previous_milestone->getPHID()); 195 196 if (!$example_column) { 197 return null; 198 } 199 200 return $previous_milestone; 201 } 202 203 /** 204 * Get the milestone of a project from its milestone number. 205 * @param PhabricatorUser $viewer Current user 206 * @param string $proj_phid Project PHID 207 * @param int $number Milestone number 208 */ 209 private function getProjectMilestoneFromNumber( 210 PhabricatorUser $viewer, 211 string $proj_phid, int $number): ?PhabricatorProject { 212 213 $query_proj = new PhabricatorProjectQuery(); 214 return $query_proj 215 ->setViewer($viewer) 216 ->withParentProjectPHIDs(array($proj_phid)) 217 ->withIsMilestone(true) 218 ->withMilestoneNumberBetween($number, $number) 219 ->executeOne(); 220 } 221 222 /** 223 * Get whatever non-proxy column on the workboard, 224 * if existing, except for the default "Backlog" column. 225 * @param PhabricatorUser $viewer Current user 226 * @param string $proj_phid Project PHID 227 * @return PhabricatorProjectColumn|null Column, or null 228 * if there are none. 229 */ 230 private function getOneImportableProjectColumn( 231 PhabricatorUser $viewer, 232 string $proj_phid): ?PhabricatorProjectColumn { 233 234 // Query one (whatever) project column suitable for the import. 235 // This code is inspired from PhabricatorProjectDatasource, 236 // looking at its parameter 'mustHaveColumns'. 237 $query_columns = new PhabricatorProjectColumnQuery(); 238 return $query_columns 239 ->setViewer($viewer) 240 ->withProjectPHIDs(array($proj_phid)) 241 ->withIsProxyColumn(false) 242 ->setLimit(1) 243 ->executeOne(); 244 } 245 246}