@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 316 lines 9.9 kB view raw
1<?php 2 3final class PhabricatorProjectColumnBulkMoveController 4 extends PhabricatorProjectBoardController { 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 // See T13316. If we're operating in "column" mode, we're going to skip 15 // the prompt for a project and just have the user select a target column. 16 // In "project" mode, we prompt them for a project first. 17 $is_column_mode = ($request->getURIData('mode') === 'column'); 18 19 $src_project = $this->getProject(); 20 $state = $this->getViewState(); 21 $board_uri = $state->newWorkboardURI(); 22 23 $layout_engine = $state->getLayoutEngine(); 24 25 $board_phid = $src_project->getPHID(); 26 $columns = $layout_engine->getColumns($board_phid); 27 $columns = mpull($columns, null, 'getID'); 28 29 $column_id = $request->getURIData('columnID'); 30 $src_column = idx($columns, $column_id); 31 if (!$src_column) { 32 return new Aphront404Response(); 33 } 34 35 $move_task_phids = $layout_engine->getColumnObjectPHIDs( 36 $board_phid, 37 $src_column->getPHID()); 38 39 $tasks = $state->getObjects(); 40 41 $move_tasks = array_select_keys($tasks, $move_task_phids); 42 43 $can_bulk_edit = PhabricatorPolicyFilter::hasCapability( 44 $viewer, 45 PhabricatorApplication::getByClass( 46 PhabricatorManiphestApplication::class), 47 ManiphestBulkEditCapability::CAPABILITY); 48 49 if (!$can_bulk_edit) { 50 return $this->newDialog() 51 ->setTitle(pht('No Movable Tasks')) 52 ->appendParagraph( 53 pht( 54 'You do not have permission to bulk edit tasks.')) 55 ->addCancelButton($board_uri); 56 } 57 58 $move_tasks = id(new PhabricatorPolicyFilter()) 59 ->setViewer($viewer) 60 ->requireCapabilities(array(PhabricatorPolicyCapability::CAN_EDIT)) 61 ->apply($move_tasks); 62 63 if (!$move_tasks) { 64 return $this->newDialog() 65 ->setTitle(pht('No Movable Tasks')) 66 ->appendParagraph( 67 pht( 68 'The selected column contains no visible tasks which you '. 69 'have permission to move.')) 70 ->addCancelButton($board_uri); 71 } 72 73 $dst_project_phid = null; 74 $dst_project = null; 75 $has_project = false; 76 if ($is_column_mode) { 77 $has_project = true; 78 $dst_project_phid = $src_project->getPHID(); 79 } else { 80 if ($request->isFormOrHiSecPost()) { 81 $has_project = $request->getStr('hasProject'); 82 if ($has_project) { 83 // We may read this from a tokenizer input as an array, or from a 84 // hidden input as a string. 85 $dst_project_phid = head($request->getArr('dstProjectPHID')); 86 if (!$dst_project_phid) { 87 $dst_project_phid = $request->getStr('dstProjectPHID'); 88 } 89 } 90 } 91 } 92 93 $errors = array(); 94 $hidden = array(); 95 96 if ($has_project) { 97 if (!$dst_project_phid) { 98 $errors[] = pht('Choose a project to move tasks to.'); 99 } else { 100 $dst_project = id(new PhabricatorProjectQuery()) 101 ->setViewer($viewer) 102 ->withPHIDs(array($dst_project_phid)) 103 ->executeOne(); 104 if (!$dst_project) { 105 $errors[] = pht('Choose a valid project to move tasks to.'); 106 } 107 108 if (!$dst_project->getHasWorkboard()) { 109 $errors[] = pht('You must choose a project with a workboard.'); 110 $dst_project = null; 111 } 112 } 113 } 114 115 if ($dst_project) { 116 $same_project = ($src_project->getID() === $dst_project->getID()); 117 118 $layout_engine = id(new PhabricatorBoardLayoutEngine()) 119 ->setViewer($viewer) 120 ->setBoardPHIDs(array($dst_project->getPHID())) 121 ->setFetchAllBoards(true) 122 ->executeLayout(); 123 124 $dst_columns = $layout_engine->getColumns($dst_project->getPHID()); 125 $dst_columns = mpull($dst_columns, null, 'getPHID'); 126 127 // Prevent moves to milestones or subprojects by selecting their 128 // columns, since the implications aren't obvious and this doesn't 129 // work the same way as normal column moves. 130 foreach ($dst_columns as $key => $dst_column) { 131 if ($dst_column->getProxyPHID()) { 132 unset($dst_columns[$key]); 133 } 134 } 135 136 $has_column = false; 137 $dst_column = null; 138 139 // If we're performing a move on the same board, default the 140 // control value to the current column. 141 if ($same_project) { 142 $dst_column_phid = $src_column->getPHID(); 143 } else { 144 $dst_column_phid = null; 145 } 146 147 if ($request->isFormOrHiSecPost()) { 148 $has_column = $request->getStr('hasColumn'); 149 if ($has_column) { 150 $dst_column_phid = $request->getStr('dstColumnPHID'); 151 } 152 } 153 154 if ($has_column) { 155 $dst_column = idx($dst_columns, $dst_column_phid); 156 if (!$dst_column) { 157 $errors[] = pht('Choose a column to move tasks to.'); 158 } else { 159 if ($dst_column->isHidden()) { 160 $errors[] = pht('You can not move tasks to a hidden column.'); 161 $dst_column = null; 162 } else if ($dst_column->getPHID() === $src_column->getPHID()) { 163 $errors[] = pht('You can not move tasks from a column to itself.'); 164 $dst_column = null; 165 } 166 } 167 } 168 169 if ($dst_column) { 170 foreach ($move_tasks as $move_task) { 171 $xactions = array(); 172 173 // If we're switching projects, get out of the old project first 174 // and move to the new project. 175 if (!$same_project) { 176 $xactions[] = id(new ManiphestTransaction()) 177 ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) 178 ->setMetadataValue( 179 'edge:type', 180 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST) 181 ->setNewValue( 182 array( 183 '-' => array( 184 $src_project->getPHID() => $src_project->getPHID(), 185 ), 186 '+' => array( 187 $dst_project->getPHID() => $dst_project->getPHID(), 188 ), 189 )); 190 } 191 192 $xactions[] = id(new ManiphestTransaction()) 193 ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) 194 ->setNewValue( 195 array( 196 array( 197 'columnPHID' => $dst_column->getPHID(), 198 ), 199 )); 200 201 $editor = id(new ManiphestTransactionEditor()) 202 ->setActor($viewer) 203 ->setContinueOnMissingFields(true) 204 ->setContinueOnNoEffect(true) 205 ->setContentSourceFromRequest($request) 206 ->setCancelURI($board_uri); 207 208 $editor->applyTransactions($move_task, $xactions); 209 } 210 211 // If we did a move on the same workboard, redirect and preserve the 212 // state parameters. If we moved to a different workboard, go there 213 // with clean default state. 214 if ($same_project) { 215 $done_uri = $board_uri; 216 } else { 217 $done_uri = $dst_project->getWorkboardURI(); 218 } 219 220 return id(new AphrontRedirectResponse())->setURI($done_uri); 221 } 222 223 $title = pht('Move Tasks to Column'); 224 225 $form = id(new AphrontFormView()) 226 ->setViewer($viewer); 227 228 // If we're moving between projects, add a reminder about which project 229 // you selected in the previous step. 230 if (!$is_column_mode) { 231 $form->appendControl( 232 id(new AphrontFormStaticControl()) 233 ->setLabel(pht('Project')) 234 ->setValue($dst_project->getDisplayName())); 235 } 236 237 $column_options = array( 238 'visible' => array(), 239 'hidden' => array(), 240 ); 241 242 $any_hidden = false; 243 foreach ($dst_columns as $column) { 244 if (!$column->isHidden()) { 245 $group = 'visible'; 246 } else { 247 $group = 'hidden'; 248 } 249 250 $phid = $column->getPHID(); 251 $display_name = $column->getDisplayName(); 252 253 $column_options[$group][$phid] = $display_name; 254 } 255 256 if ($column_options['hidden']) { 257 $column_options = array( 258 pht('Visible Columns') => $column_options['visible'], 259 pht('Hidden Columns') => $column_options['hidden'], 260 ); 261 } else { 262 $column_options = $column_options['visible']; 263 } 264 265 $form->appendControl( 266 id(new AphrontFormSelectControl()) 267 ->setName('dstColumnPHID') 268 ->setLabel(pht('Move to Column')) 269 ->setValue($dst_column_phid) 270 ->setOptions($column_options)); 271 272 $submit = pht('Move Tasks'); 273 274 $hidden['dstProjectPHID'] = $dst_project->getPHID(); 275 $hidden['hasColumn'] = true; 276 $hidden['hasProject'] = true; 277 } else { 278 $title = pht('Move Tasks to Project'); 279 280 if ($dst_project_phid) { 281 $dst_project_phid_value = array($dst_project_phid); 282 } else { 283 $dst_project_phid_value = array(); 284 } 285 286 $form = id(new AphrontFormView()) 287 ->setViewer($viewer) 288 ->appendControl( 289 id(new AphrontFormTokenizerControl()) 290 ->setName('dstProjectPHID') 291 ->setLimit(1) 292 ->setLabel(pht('Move to Project')) 293 ->setValue($dst_project_phid_value) 294 ->setDatasource(new PhabricatorProjectDatasource())); 295 296 $submit = pht('Continue'); 297 298 $hidden['hasProject'] = true; 299 } 300 301 $dialog = $this->newWorkboardDialog() 302 ->setWidth(AphrontDialogView::WIDTH_FORM) 303 ->setTitle($title) 304 ->setErrors($errors) 305 ->appendForm($form) 306 ->addSubmitButton($submit) 307 ->addCancelButton($board_uri); 308 309 foreach ($hidden as $key => $value) { 310 $dialog->addHiddenInput($key, $value); 311 } 312 313 return $dialog; 314 } 315 316}