@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 upstream/main 601 lines 18 kB view raw
1<?php 2 3final class PhabricatorBoardLayoutEngine extends Phobject { 4 5 private $viewer; 6 private $boardPHIDs; 7 private $objectPHIDs = array(); 8 private $boards; 9 private $columnMap = array(); 10 private $objectColumnMap = array(); 11 private $boardLayout = array(); 12 private $fetchAllBoards; 13 14 private $remQueue = array(); 15 private $addQueue = array(); 16 17 public function setViewer(PhabricatorUser $viewer) { 18 $this->viewer = $viewer; 19 return $this; 20 } 21 22 public function getViewer() { 23 return $this->viewer; 24 } 25 26 public function setBoardPHIDs(array $board_phids) { 27 $this->boardPHIDs = array_fuse($board_phids); 28 return $this; 29 } 30 31 public function getBoardPHIDs() { 32 return $this->boardPHIDs; 33 } 34 35 public function setObjectPHIDs(array $object_phids) { 36 $this->objectPHIDs = array_fuse($object_phids); 37 return $this; 38 } 39 40 public function getObjectPHIDs() { 41 return $this->objectPHIDs; 42 } 43 44 /** 45 * Fetch all boards, even if the board is disabled. 46 */ 47 public function setFetchAllBoards($fetch_all) { 48 $this->fetchAllBoards = $fetch_all; 49 return $this; 50 } 51 52 public function getFetchAllBoards() { 53 return $this->fetchAllBoards; 54 } 55 56 public function executeLayout() { 57 $viewer = $this->getViewer(); 58 59 $boards = $this->loadBoards(); 60 if (!$boards) { 61 return $this; 62 } 63 64 $columns = $this->loadColumns($boards); 65 $positions = $this->loadPositions($boards); 66 67 foreach ($boards as $board_phid => $board) { 68 $board_columns = idx($columns, $board_phid); 69 70 // Don't layout boards with no columns. These boards need to be formally 71 // created first. 72 if (!$columns) { 73 continue; 74 } 75 76 $board_positions = idx($positions, $board_phid, array()); 77 78 $this->layoutBoard($board, $board_columns, $board_positions); 79 } 80 81 return $this; 82 } 83 84 public function getColumns($board_phid) { 85 $columns = idx($this->boardLayout, $board_phid, array()); 86 return array_select_keys($this->columnMap, array_keys($columns)); 87 } 88 89 public function getColumnObjectPositions($board_phid, $column_phid) { 90 $columns = idx($this->boardLayout, $board_phid, array()); 91 return idx($columns, $column_phid, array()); 92 } 93 94 95 public function getColumnObjectPHIDs($board_phid, $column_phid) { 96 $positions = $this->getColumnObjectPositions($board_phid, $column_phid); 97 return mpull($positions, 'getObjectPHID'); 98 } 99 100 public function getObjectColumns($board_phid, $object_phid) { 101 $board_map = idx($this->objectColumnMap, $board_phid, array()); 102 103 $column_phids = idx($board_map, $object_phid); 104 if (!$column_phids) { 105 return array(); 106 } 107 108 return array_select_keys($this->columnMap, $column_phids); 109 } 110 111 public function queueRemovePosition( 112 $board_phid, 113 $column_phid, 114 $object_phid) { 115 116 $board_layout = idx($this->boardLayout, $board_phid, array()); 117 $positions = idx($board_layout, $column_phid, array()); 118 $position = idx($positions, $object_phid); 119 120 if ($position) { 121 $this->remQueue[] = $position; 122 123 // If this position hasn't been saved yet, get it out of the add queue. 124 if (!$position->getID()) { 125 foreach ($this->addQueue as $key => $add_position) { 126 if ($add_position === $position) { 127 unset($this->addQueue[$key]); 128 } 129 } 130 } 131 } 132 133 unset($this->boardLayout[$board_phid][$column_phid][$object_phid]); 134 135 return $this; 136 } 137 138 public function queueAddPosition( 139 $board_phid, 140 $column_phid, 141 $object_phid, 142 array $after_phids, 143 array $before_phids) { 144 145 $board_layout = idx($this->boardLayout, $board_phid, array()); 146 $positions = idx($board_layout, $column_phid, array()); 147 148 // Check if the object is already in the column, and remove it if it is. 149 $object_position = idx($positions, $object_phid); 150 unset($positions[$object_phid]); 151 152 if (!$object_position) { 153 $object_position = id(new PhabricatorProjectColumnPosition()) 154 ->setBoardPHID($board_phid) 155 ->setColumnPHID($column_phid) 156 ->setObjectPHID($object_phid); 157 } 158 159 if (!$positions) { 160 $object_position->setSequence(0); 161 } else { 162 // The user's view of the board may fall out of date, so they might 163 // try to drop a card under a different card which is no longer where 164 // they thought it was. 165 166 // When this happens, we perform the move anyway, since this is almost 167 // certainly what users want when interacting with the UI. We'l try to 168 // fall back to another nearby card if the client provided us one. If 169 // we don't find any of the cards the client specified in the column, 170 // we'll just move the card to the default position. 171 172 $search_phids = array(); 173 foreach ($after_phids as $after_phid) { 174 $search_phids[] = array($after_phid, false); 175 } 176 177 foreach ($before_phids as $before_phid) { 178 $search_phids[] = array($before_phid, true); 179 } 180 181 // This makes us fall back to the default position if we fail every 182 // candidate position. The default position counts as a "before" position 183 // because we want to put the new card at the top of the column. 184 $search_phids[] = array(null, true); 185 186 $found = false; 187 foreach ($search_phids as $search_position) { 188 list($relative_phid, $is_before) = $search_position; 189 foreach ($positions as $position) { 190 if (!$found) { 191 if ($relative_phid === null) { 192 $is_match = true; 193 } else { 194 $position_phid = $position->getObjectPHID(); 195 $is_match = ($relative_phid === $position_phid); 196 } 197 198 if ($is_match) { 199 $found = true; 200 201 $sequence = $position->getSequence(); 202 203 if (!$is_before) { 204 $sequence++; 205 } 206 207 $object_position->setSequence($sequence++); 208 209 if (!$is_before) { 210 // If we're inserting after this position, continue the loop so 211 // we don't update it. 212 continue; 213 } 214 } 215 } 216 217 if ($found) { 218 $position->setSequence($sequence++); 219 $this->addQueue[] = $position; 220 } 221 } 222 223 if ($found) { 224 break; 225 } 226 } 227 } 228 229 $this->addQueue[] = $object_position; 230 231 $positions[$object_phid] = $object_position; 232 $positions = msortv($positions, 'newColumnPositionOrderVector'); 233 234 $this->boardLayout[$board_phid][$column_phid] = $positions; 235 236 return $this; 237 } 238 239 public function applyPositionUpdates() { 240 foreach ($this->remQueue as $position) { 241 if ($position->getID()) { 242 $position->delete(); 243 } 244 } 245 $this->remQueue = array(); 246 247 $adds = array(); 248 $updates = array(); 249 250 foreach ($this->addQueue as $position) { 251 $id = $position->getID(); 252 if ($id) { 253 $updates[$id] = $position; 254 } else { 255 $adds[] = $position; 256 } 257 } 258 $this->addQueue = array(); 259 260 $table = new PhabricatorProjectColumnPosition(); 261 $conn_w = $table->establishConnection('w'); 262 263 $pairs = array(); 264 foreach ($updates as $id => $position) { 265 // This is ugly because MySQL gets upset with us if it is configured 266 // strictly and we attempt inserts which can't work. We'll never actually 267 // do these inserts since they'll always collide (triggering the ON 268 // DUPLICATE KEY logic), so we just provide dummy values in order to get 269 // there. 270 271 $pairs[] = qsprintf( 272 $conn_w, 273 '(%d, %d, "", "", "")', 274 $id, 275 $position->getSequence()); 276 } 277 278 if ($pairs) { 279 queryfx( 280 $conn_w, 281 'INSERT INTO %T (id, sequence, boardPHID, columnPHID, objectPHID) 282 VALUES %LQ ON DUPLICATE KEY UPDATE sequence = VALUES(sequence)', 283 $table->getTableName(), 284 $pairs); 285 } 286 287 foreach ($adds as $position) { 288 $position->save(); 289 } 290 291 return $this; 292 } 293 294 private function loadBoards() { 295 $viewer = $this->getViewer(); 296 $board_phids = $this->getBoardPHIDs(); 297 298 $boards = id(new PhabricatorObjectQuery()) 299 ->setViewer($viewer) 300 ->withPHIDs($board_phids) 301 ->execute(); 302 $boards = mpull($boards, null, 'getPHID'); 303 304 foreach ($boards as $key => $board) { 305 if (!($board instanceof PhabricatorWorkboardInterface)) { 306 unset($boards[$key]); 307 } 308 } 309 310 if (!$this->fetchAllBoards) { 311 foreach ($boards as $key => $board) { 312 if (!$board->getHasWorkboard()) { 313 unset($boards[$key]); 314 } 315 } 316 } 317 318 return $boards; 319 } 320 321 private function loadColumns(array $boards) { 322 $viewer = $this->getViewer(); 323 324 $columns = id(new PhabricatorProjectColumnQuery()) 325 ->setViewer($viewer) 326 ->withProjectPHIDs(array_keys($boards)) 327 ->needTriggers(true) 328 ->execute(); 329 $columns = msort($columns, 'getOrderingKey'); 330 $columns = mpull($columns, null, 'getPHID'); 331 332 $need_children = array(); 333 foreach ($boards as $phid => $board) { 334 if ($board->getHasMilestones() || $board->getHasSubprojects()) { 335 $need_children[] = $phid; 336 } 337 } 338 339 if ($need_children) { 340 $children = id(new PhabricatorProjectQuery()) 341 ->setViewer($viewer) 342 ->withParentProjectPHIDs($need_children) 343 ->execute(); 344 $children = mpull($children, null, 'getPHID'); 345 $children = mgroup($children, 'getParentProjectPHID'); 346 } else { 347 $children = array(); 348 } 349 350 $columns = mgroup($columns, 'getProjectPHID'); 351 foreach ($boards as $board_phid => $board) { 352 $board_columns = idx($columns, $board_phid, array()); 353 354 // If the project has milestones, create any missing columns. 355 if ($board->getHasMilestones() || $board->getHasSubprojects()) { 356 $child_projects = idx($children, $board_phid, array()); 357 358 if ($board_columns) { 359 $next_sequence = last($board_columns)->getSequence() + 1; 360 } else { 361 $next_sequence = 1; 362 } 363 364 $proxy_columns = mpull($board_columns, null, 'getProxyPHID'); 365 foreach ($child_projects as $child_phid => $child) { 366 if (isset($proxy_columns[$child_phid])) { 367 continue; 368 } 369 370 $new_column = PhabricatorProjectColumn::initializeNewColumn($viewer) 371 ->attachProject($board) 372 ->attachProxy($child) 373 ->setSequence($next_sequence++) 374 ->setProjectPHID($board_phid) 375 ->setProxyPHID($child_phid); 376 377 $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 378 $new_column->save(); 379 unset($unguarded); 380 381 $board_columns[$new_column->getPHID()] = $new_column; 382 } 383 } 384 385 $board_columns = msort($board_columns, 'getOrderingKey'); 386 387 $columns[$board_phid] = $board_columns; 388 } 389 390 foreach ($columns as $board_phid => $board_columns) { 391 foreach ($board_columns as $board_column) { 392 $column_phid = $board_column->getPHID(); 393 $this->columnMap[$column_phid] = $board_column; 394 } 395 } 396 397 return $columns; 398 } 399 400 private function loadPositions(array $boards) { 401 $viewer = $this->getViewer(); 402 403 $object_phids = $this->getObjectPHIDs(); 404 if (!$object_phids) { 405 return array(); 406 } 407 408 $positions = id(new PhabricatorProjectColumnPositionQuery()) 409 ->setViewer($viewer) 410 ->withBoardPHIDs(array_keys($boards)) 411 ->withObjectPHIDs($object_phids) 412 ->execute(); 413 $positions = msortv($positions, 'newColumnPositionOrderVector'); 414 $positions = mgroup($positions, 'getBoardPHID'); 415 416 return $positions; 417 } 418 419 private function layoutBoard( 420 $board, 421 array $columns, 422 array $positions) { 423 424 $viewer = $this->getViewer(); 425 426 $board_phid = $board->getPHID(); 427 $position_groups = mgroup($positions, 'getObjectPHID'); 428 429 $layout = array(); 430 $default_phid = null; 431 foreach ($columns as $column) { 432 $column_phid = $column->getPHID(); 433 $layout[$column_phid] = array(); 434 435 if ($column->isDefaultColumn()) { 436 $default_phid = $column_phid; 437 } 438 } 439 440 // Find all the columns which are proxies for other objects. 441 $proxy_map = array(); 442 foreach ($columns as $column) { 443 $proxy_phid = $column->getProxyPHID(); 444 if ($proxy_phid) { 445 $proxy_map[$proxy_phid] = $column->getPHID(); 446 } 447 } 448 449 $object_phids = $this->getObjectPHIDs(); 450 451 // If we have proxies, we need to force cards into the correct proxy 452 // columns. 453 if ($proxy_map && $object_phids) { 454 $edge_query = id(new PhabricatorEdgeQuery()) 455 ->withSourcePHIDs($object_phids) 456 ->withEdgeTypes( 457 array( 458 PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, 459 )); 460 $edge_query->execute(); 461 462 $project_phids = $edge_query->getDestinationPHIDs(); 463 $project_phids = array_fuse($project_phids); 464 } else { 465 $project_phids = array(); 466 } 467 468 if ($project_phids) { 469 $projects = id(new PhabricatorProjectQuery()) 470 ->setViewer($viewer) 471 ->withPHIDs($project_phids) 472 ->execute(); 473 $projects = mpull($projects, null, 'getPHID'); 474 } else { 475 $projects = array(); 476 } 477 478 // Build a map from every project that any task is tagged with to the 479 // ancestor project which has a column on this board, if one exists. 480 $ancestor_map = array(); 481 foreach ($projects as $phid => $project) { 482 if (isset($proxy_map[$phid])) { 483 $ancestor_map[$phid] = $proxy_map[$phid]; 484 } else { 485 $seen = array($phid); 486 foreach ($project->getAncestorProjects() as $ancestor) { 487 $ancestor_phid = $ancestor->getPHID(); 488 $seen[] = $ancestor_phid; 489 if (isset($proxy_map[$ancestor_phid])) { 490 foreach ($seen as $project_phid) { 491 $ancestor_map[$project_phid] = $proxy_map[$ancestor_phid]; 492 } 493 } 494 } 495 } 496 } 497 498 $view_sequence = 1; 499 foreach ($object_phids as $object_phid) { 500 $positions = idx($position_groups, $object_phid, array()); 501 502 // First, check for objects that have corresponding proxy columns. We're 503 // going to overwrite normal column positions if a tag belongs to a proxy 504 // column, since you can't be in normal columns if you're in proxy 505 // columns. 506 $proxy_hits = array(); 507 if ($proxy_map) { 508 $object_project_phids = $edge_query->getDestinationPHIDs( 509 array( 510 $object_phid, 511 )); 512 513 foreach ($object_project_phids as $project_phid) { 514 if (isset($ancestor_map[$project_phid])) { 515 $proxy_hits[] = $ancestor_map[$project_phid]; 516 } 517 } 518 } 519 520 if ($proxy_hits) { 521 // TODO: For now, only one column hit is permissible. 522 $proxy_hits = array_slice($proxy_hits, 0, 1); 523 524 $proxy_hits = array_fuse($proxy_hits); 525 526 // Check the object positions: we hope to find a position in each 527 // column the object should be part of. We're going to drop any 528 // invalid positions and create new positions where positions are 529 // missing. 530 foreach ($positions as $key => $position) { 531 $column_phid = $position->getColumnPHID(); 532 if (isset($proxy_hits[$column_phid])) { 533 // Valid column, mark the position as found. 534 unset($proxy_hits[$column_phid]); 535 } else { 536 // Invalid column, ignore the position. 537 unset($positions[$key]); 538 } 539 } 540 541 // Create new positions for anything we haven't found. 542 foreach ($proxy_hits as $proxy_hit) { 543 $new_position = id(new PhabricatorProjectColumnPosition()) 544 ->setBoardPHID($board_phid) 545 ->setColumnPHID($proxy_hit) 546 ->setObjectPHID($object_phid) 547 ->setSequence(0) 548 ->setViewSequence($view_sequence++); 549 550 $this->addQueue[] = $new_position; 551 552 $positions[] = $new_position; 553 } 554 } else { 555 // Ignore any positions in columns which no longer exist. We don't 556 // actively destory them because the rest of the code ignores them and 557 // there's no real need to destroy the data. 558 foreach ($positions as $key => $position) { 559 $column_phid = $position->getColumnPHID(); 560 if (empty($columns[$column_phid])) { 561 unset($positions[$key]); 562 } 563 } 564 565 // If the object has no position, put it on the default column if 566 // one exists. 567 if (!$positions && $default_phid) { 568 $new_position = id(new PhabricatorProjectColumnPosition()) 569 ->setBoardPHID($board_phid) 570 ->setColumnPHID($default_phid) 571 ->setObjectPHID($object_phid) 572 ->setSequence(0) 573 ->setViewSequence($view_sequence++); 574 575 $this->addQueue[] = $new_position; 576 577 $positions = array( 578 $new_position, 579 ); 580 } 581 } 582 583 foreach ($positions as $position) { 584 $column_phid = $position->getColumnPHID(); 585 $layout[$column_phid][$object_phid] = $position; 586 } 587 } 588 589 foreach ($layout as $column_phid => $map) { 590 $map = msortv($map, 'newColumnPositionOrderVector'); 591 $layout[$column_phid] = $map; 592 593 foreach ($map as $object_phid => $position) { 594 $this->objectColumnMap[$board_phid][$object_phid][] = $column_phid; 595 } 596 } 597 598 $this->boardLayout[$board_phid] = $layout; 599 } 600 601}