@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 882 lines 26 kB view raw
1<?php 2 3final class ManiphestTaskDetailController extends ManiphestController { 4 5 public function shouldAllowPublic() { 6 return true; 7 } 8 9 public function handleRequest(AphrontRequest $request) { 10 $viewer = $this->getViewer(); 11 $id = $request->getURIData('id'); 12 13 $task = id(new ManiphestTaskQuery()) 14 ->setViewer($viewer) 15 ->withIDs(array($id)) 16 ->needSubscriberPHIDs(true) 17 ->executeOne(); 18 if (!$task) { 19 return new Aphront404Response(); 20 } 21 22 $field_list = PhabricatorCustomField::getObjectFields( 23 $task, 24 PhabricatorCustomField::ROLE_VIEW); 25 $field_list 26 ->setViewer($viewer) 27 ->readFieldsFromStorage($task); 28 29 $edit_engine = id(new ManiphestEditEngine()) 30 ->setViewer($viewer) 31 ->setTargetObject($task); 32 33 $edge_types = array( 34 ManiphestTaskHasCommitEdgeType::EDGECONST, 35 ManiphestTaskHasRevisionEdgeType::EDGECONST, 36 ManiphestTaskHasMockEdgeType::EDGECONST, 37 PhabricatorObjectMentionedByObjectEdgeType::EDGECONST, 38 PhabricatorObjectMentionsObjectEdgeType::EDGECONST, 39 ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST, 40 ); 41 42 $phid = $task->getPHID(); 43 44 $query = id(new PhabricatorEdgeQuery()) 45 ->withSourcePHIDs(array($phid)) 46 ->withEdgeTypes($edge_types); 47 $edges = idx($query->execute(), $phid); 48 $phids = array_fill_keys($query->getDestinationPHIDs(), true); 49 50 if ($task->getOwnerPHID()) { 51 $phids[$task->getOwnerPHID()] = true; 52 } 53 $phids[$task->getAuthorPHID()] = true; 54 55 $phids = array_keys($phids); 56 $handles = $viewer->loadHandles($phids); 57 58 $timeline = $this->buildTransactionTimeline( 59 $task, 60 new ManiphestTransactionQuery()); 61 62 $monogram = $task->getMonogram(); 63 $crumbs = $this->buildApplicationCrumbs() 64 ->addTextCrumb($monogram) 65 ->setBorder(true); 66 67 $header = $this->buildHeaderView($task); 68 $details = $this->buildPropertyView($task, $field_list, $edges, $handles); 69 $description = $this->buildDescriptionView($task); 70 $curtain = $this->buildCurtain($task, $edit_engine); 71 72 $title = pht('%s %s', $monogram, $task->getTitle()); 73 74 $comment_view = $edit_engine 75 ->buildEditEngineCommentView($task); 76 77 $timeline->setQuoteRef($monogram); 78 $comment_view->setTransactionTimeline($timeline); 79 80 $related_tabs = array(); 81 $graph_menu = null; 82 83 $graph_limit = 200; 84 $graph_error_message = null; 85 $task_graph = id(new ManiphestTaskGraph()) 86 ->setViewer($viewer) 87 ->setSeedPHID($task->getPHID()) 88 ->setLimit($graph_limit) 89 ->loadGraph(); 90 if (!$task_graph->isEmpty()) { 91 $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; 92 $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; 93 $parent_map = $task_graph->getEdges($parent_type); 94 $subtask_map = $task_graph->getEdges($subtask_type); 95 $parent_list = idx($parent_map, $task->getPHID(), array()); 96 $subtask_list = idx($subtask_map, $task->getPHID(), array()); 97 $has_parents = (bool)$parent_list; 98 $has_subtasks = (bool)$subtask_list; 99 100 // First, get a count of direct parent tasks and subtasks. If there 101 // are too many of these, we just don't draw anything. You can use 102 // the search button to browse tasks with the search UI instead. 103 $direct_count = count($parent_list) + count($subtask_list); 104 105 $graph_table = null; 106 if ($direct_count > $graph_limit) { 107 $graph_error_message = pht( 108 'This task is directly connected to more than %s other tasks. '. 109 'Use %s to browse parents or subtasks, or %s to show more of the '. 110 'graph.', 111 new PhutilNumber($graph_limit), 112 phutil_tag('strong', array(), pht('Search...')), 113 phutil_tag('strong', array(), pht('View Standalone Graph'))); 114 115 } else { 116 // If there aren't too many direct tasks, but there are too many total 117 // tasks, we'll only render directly connected tasks. 118 if ($task_graph->isOverLimit()) { 119 $task_graph->setRenderOnlyAdjacentNodes(true); 120 121 $graph_error_message = pht( 122 'This task is connected to more than %s other tasks. '. 123 'Only direct parents and subtasks are shown here. Use '. 124 '%s to show more of the graph.', 125 new PhutilNumber($graph_limit), 126 phutil_tag('strong', array(), pht('View Standalone Graph'))); 127 } 128 129 try { 130 $graph_table = $task_graph->newGraphTable(); 131 } catch (Throwable $ex) { 132 phlog($ex); 133 $graph_error_message = pht( 134 'There was an unexpected error displaying the task graph. '. 135 'Use %s to browse parents or subtasks, or %s to show the graph.', 136 phutil_tag('strong', array(), pht('Search...')), 137 phutil_tag('strong', array(), pht('View Standalone Graph'))); 138 } 139 } 140 141 if ($graph_error_message) { 142 $overflow_view = $this->newTaskGraphOverflowView( 143 $task, 144 $graph_error_message, 145 true); 146 147 $graph_table = array( 148 $overflow_view, 149 $graph_table, 150 ); 151 } 152 153 $graph_menu = $this->newTaskGraphDropdownMenu( 154 $task, 155 $has_parents, 156 $has_subtasks, 157 true); 158 159 $related_tabs[] = id(new PHUITabView()) 160 ->setName(pht('Task Graph')) 161 ->setKey('graph') 162 ->appendChild($graph_table); 163 } 164 165 $related_tabs[] = $this->newMocksTab($task, $query); 166 $related_tabs[] = $this->newMentionsTab($query); 167 $related_tabs[] = $this->newDuplicatesTab($task, $query); 168 169 $tab_view = null; 170 171 $related_tabs = array_filter($related_tabs); 172 if ($related_tabs) { 173 $tab_group = new PHUITabGroupView(); 174 foreach ($related_tabs as $tab) { 175 $tab_group->addTab($tab); 176 } 177 178 $related_header = id(new PHUIHeaderView()) 179 ->setHeader(pht('Related Objects')); 180 181 if ($graph_menu) { 182 $related_header->addActionLink($graph_menu); 183 } 184 185 $tab_view = id(new PHUIObjectBoxView()) 186 ->setHeader($related_header) 187 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 188 ->addTabGroup($tab_group); 189 } 190 191 $changes_view = $this->newChangesView($task, $edges); 192 193 $view = id(new PHUITwoColumnView()) 194 ->setHeader($header) 195 ->setCurtain($curtain) 196 ->setMainColumn( 197 array( 198 $changes_view, 199 $tab_view, 200 $timeline, 201 $comment_view, 202 )) 203 ->addPropertySection(pht('Description'), $description) 204 ->addPropertySection(pht('Details'), $details); 205 206 $page = $this->newPage() 207 ->setTitle($title) 208 ->setCrumbs($crumbs) 209 ->setPageObjectPHIDs( 210 array( 211 $task->getPHID(), 212 )) 213 ->appendChild($view); 214 215 if ($this->getIncludeOpenGraphMetadata($viewer, $task)) { 216 $page = $this->addOpenGraphProtocolMetadataTags($page, $task); 217 } 218 219 return $page; 220 } 221 222 /** 223 * Whether the page should include Open Graph metadata tags 224 * @param PhabricatorUser $viewer Viewer of the object 225 * @param object $object 226 * @return bool True if the page should serve Open Graph metadata tags 227 */ 228 private function getIncludeOpenGraphMetadata(PhabricatorUser $viewer, 229 $object) { 230 // Don't waste time adding OpenGraph metadata for logged-in users 231 if ($viewer->getIsStandardUser()) { 232 return false; 233 } 234 // Include OpenGraph tags only for public objects 235 return $object->getViewPolicy() === PhabricatorPolicies::POLICY_PUBLIC; 236 } 237 238 /** 239 * Get Open Graph Protocol metadata values 240 * @param ManiphestTask $task 241 * @return array Map of Open Graph property => value 242 */ 243 private function getOpenGraphProtocolMetadataValues($task) { 244 $viewer = $this->getViewer(); 245 246 $v = array(); 247 $v['og:site_name'] = PlatformSymbols::getPlatformServerName(); 248 $v['og:type'] = 'website'; 249 $v['og:url'] = PhabricatorEnv::getProductionURI($task->getURI()); 250 $v['og:title'] = $task->getMonogram().' '.$task->getTitle(); 251 252 $desc = $task->getDescription(); 253 if (phutil_nonempty_string($desc)) { 254 $v['og:description'] = 255 PhabricatorMarkupEngine::summarizeSentence($desc); 256 } 257 258 $v['og:image'] = 259 PhabricatorCustomLogoConfigType::getLogoURI($viewer); 260 261 $v['og:image:height'] = 64; 262 $v['og:image:width'] = 64; 263 264 return $v; 265 } 266 267 /** 268 * Add Open Graph Protocol metadata tags to Maniphest task page 269 * @param PhabricatorStandardPageView $page 270 * @param ManiphestTask $task 271 * @return PhabricatorStandardPageView with additional OGP <meta> tags 272 */ 273 private function addOpenGraphProtocolMetadataTags($page, $task) { 274 foreach ($this->getOpenGraphProtocolMetadataValues($task) as $k => $v) { 275 $page->addHeadItem(phutil_tag( 276 'meta', 277 array( 278 'property' => $k, 279 'content' => $v, 280 ))); 281 } 282 return $page; 283 } 284 285 private function buildHeaderView(ManiphestTask $task) { 286 $view = id(new PHUIHeaderView()) 287 ->setHeader($task->getTitle()) 288 ->setViewer($this->getRequest()->getUser()) 289 ->setPolicyObject($task); 290 291 $priority_name = ManiphestTaskPriority::getTaskPriorityName( 292 $task->getPriority()); 293 $priority_color = ManiphestTaskPriority::getTaskPriorityColor( 294 $task->getPriority()); 295 296 $status = $task->getStatus(); 297 $status_name = ManiphestTaskStatus::renderFullDescription( 298 $status, $priority_name); 299 $view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name); 300 301 $view->setHeaderIcon(ManiphestTaskStatus::getStatusIcon( 302 $task->getStatus()).' '.$priority_color); 303 304 if (ManiphestTaskPoints::getIsEnabled()) { 305 $points = $task->getPoints(); 306 if ($points !== null) { 307 $points_name = pht('%s %s', 308 $task->getPoints(), 309 ManiphestTaskPoints::getPointsLabel()); 310 $tag = id(new PHUITagView()) 311 ->setName($points_name) 312 ->setColor(PHUITagView::COLOR_BLUE) 313 ->setType(PHUITagView::TYPE_SHADE); 314 315 $view->addTag($tag); 316 } 317 } 318 319 $subtype = $task->newSubtypeObject(); 320 if ($subtype && $subtype->hasTagView()) { 321 $subtype_tag = $subtype->newTagView(); 322 $view->addTag($subtype_tag); 323 } 324 325 return $view; 326 } 327 328 329 private function buildCurtain( 330 ManiphestTask $task, 331 PhabricatorEditEngine $edit_engine) { 332 $viewer = $this->getViewer(); 333 334 $id = $task->getID(); 335 $phid = $task->getPHID(); 336 337 $can_edit = PhabricatorPolicyFilter::hasCapability( 338 $viewer, 339 $task, 340 PhabricatorPolicyCapability::CAN_EDIT); 341 342 $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $task); 343 344 // We expect a policy dialog if you can't edit the task, and expect a 345 // lock override dialog if you can't interact with it. 346 $workflow_edit = (!$can_edit || !$can_interact); 347 348 $curtain = $this->newCurtainView($task); 349 350 $curtain->addAction( 351 id(new PhabricatorActionView()) 352 ->setName(pht('Edit Task')) 353 ->setIcon('fa-pencil') 354 ->setHref($this->getApplicationURI("/task/edit/{$id}/")) 355 ->setDisabled(!$can_edit) 356 ->setWorkflow($workflow_edit)); 357 358 $subtype_map = $task->newEditEngineSubtypeMap(); 359 $subtask_options = $subtype_map->getCreateFormsForSubtype( 360 $edit_engine, 361 $task); 362 363 // If no forms are available, we want to show the user an error. 364 // If one form is available, we take them user directly to the form. 365 // If two or more forms are available, we give the user a choice. 366 367 // The "subtask" controller handles the first case (no forms) and the 368 // third case (more than one form). In the case of one form, we link 369 // directly to the form. 370 $subtask_uri = "/task/subtask/{$id}/"; 371 $subtask_workflow = true; 372 373 if (count($subtask_options) == 1) { 374 $subtask_form = head($subtask_options); 375 $form_key = $subtask_form->getIdentifier(); 376 $subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) 377 ->replaceQueryParam('parent', $id) 378 ->replaceQueryParam('template', $id) 379 ->replaceQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); 380 $subtask_workflow = false; 381 } 382 383 $subtask_uri = $this->getApplicationURI($subtask_uri); 384 385 $subtask_item = id(new PhabricatorActionView()) 386 ->setName(pht('Create Subtask')) 387 ->setHref($subtask_uri) 388 ->setIcon('fa-level-down') 389 ->setDisabled(!$subtask_options) 390 ->setWorkflow($subtask_workflow); 391 392 $relationship_list = PhabricatorObjectRelationshipList::newForObject( 393 $viewer, 394 $task); 395 396 $submenu_actions = array( 397 $subtask_item, 398 ManiphestTaskHasParentRelationship::RELATIONSHIPKEY, 399 ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY, 400 ManiphestTaskMergeInRelationship::RELATIONSHIPKEY, 401 ManiphestTaskCloseAsDuplicateRelationship::RELATIONSHIPKEY, 402 ); 403 404 $task_submenu = $relationship_list->newActionSubmenu($submenu_actions) 405 ->setName(pht('Edit Related Tasks...')) 406 ->setIcon('fa-anchor'); 407 408 $curtain->addAction($task_submenu); 409 410 $relationship_submenu = $relationship_list->newActionMenu(); 411 if ($relationship_submenu) { 412 $curtain->addAction($relationship_submenu); 413 } 414 415 $viewer_phid = $viewer->getPHID(); 416 $owner_phid = $task->getOwnerPHID(); 417 $author_phid = $task->getAuthorPHID(); 418 if ($owner_phid) { 419 $handles = $viewer->loadHandles(array($owner_phid, $author_phid)); 420 } else { 421 $handles = $viewer->loadHandles(array($author_phid)); 422 } 423 424 $assigned_refs = id(new PHUICurtainObjectRefListView()) 425 ->setViewer($viewer) 426 ->setEmptyMessage(pht('None')); 427 428 if ($owner_phid) { 429 $assigned_ref = $assigned_refs->newObjectRefView() 430 ->setHandle($handles[$owner_phid]) 431 ->setHighlighted($owner_phid === $viewer_phid); 432 } 433 434 $curtain->newPanel() 435 ->setHeaderText(pht('Assigned To')) 436 ->appendChild($assigned_refs); 437 438 $author_refs = id(new PHUICurtainObjectRefListView()) 439 ->setViewer($viewer); 440 441 $author_ref = $author_refs->newObjectRefView() 442 ->setHandle($handles[$author_phid]) 443 ->setEpoch($task->getDateCreated()) 444 ->setHighlighted($author_phid === $viewer_phid); 445 446 $curtain->newPanel() 447 ->setHeaderText(pht('Authored By')) 448 ->appendChild($author_refs); 449 450 return $curtain; 451 } 452 453 private function buildPropertyView( 454 ManiphestTask $task, 455 PhabricatorCustomFieldList $field_list, 456 array $edges, 457 $handles) { 458 459 $viewer = $this->getRequest()->getUser(); 460 $view = id(new PHUIPropertyListView()) 461 ->setViewer($viewer); 462 463 $source = $task->getOriginalEmailSource(); 464 if ($source) { 465 $subject = '[T'.$task->getID().'] '.$task->getTitle(); 466 $view->addProperty( 467 pht('From Email'), 468 phutil_tag( 469 'a', 470 array( 471 'href' => 'mailto:'.$source.'?subject='.$subject, 472 ), 473 $source)); 474 } 475 476 $field_list->appendFieldsToPropertyList( 477 $task, 478 $viewer, 479 $view); 480 481 if ($view->hasAnyProperties()) { 482 return $view; 483 } 484 485 return null; 486 } 487 488 private function buildDescriptionView(ManiphestTask $task) { 489 $viewer = $this->getViewer(); 490 491 $section = null; 492 493 $description = $task->getDescription(); 494 if (strlen($description)) { 495 $section = new PHUIPropertyListView(); 496 $section->addTextContent( 497 phutil_tag( 498 'div', 499 array( 500 'class' => 'phabricator-remarkup', 501 ), 502 id(new PHUIRemarkupView($viewer, $description)) 503 ->setContextObject($task))); 504 } 505 506 return $section; 507 } 508 509 private function newMocksTab( 510 ManiphestTask $task, 511 PhabricatorEdgeQuery $edge_query) { 512 513 $mock_type = ManiphestTaskHasMockEdgeType::EDGECONST; 514 $mock_phids = $edge_query->getDestinationPHIDs(array(), array($mock_type)); 515 if (!$mock_phids) { 516 return null; 517 } 518 519 $viewer = $this->getViewer(); 520 $handles = $viewer->loadHandles($mock_phids); 521 522 // TODO: It would be nice to render this as pinboard-style thumbnails, 523 // similar to "{M123}", instead of a list of links. 524 525 $view = id(new PHUIPropertyListView()) 526 ->addProperty(pht('Mocks'), $handles->renderList()); 527 528 return id(new PHUITabView()) 529 ->setName(pht('Mocks')) 530 ->setKey('mocks') 531 ->appendChild($view); 532 } 533 534 private function newMentionsTab( 535 PhabricatorEdgeQuery $edge_query) { 536 537 $view = (new PhorgeApplicationMentionsListView()) 538 ->setEdgeQuery($edge_query) 539 ->setViewer($this->getViewer()) 540 ->getMentionsView(); 541 542 if (!$view ) { 543 return null; 544 } 545 546 return id(new PHUITabView()) 547 ->setName(pht('Mentions')) 548 ->setKey('mentions') 549 ->appendChild($view); 550 } 551 552 private function newDuplicatesTab( 553 ManiphestTask $task, 554 PhabricatorEdgeQuery $edge_query) { 555 556 $in_type = ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST; 557 $in_phids = $edge_query->getDestinationPHIDs(array(), array($in_type)); 558 559 $viewer = $this->getViewer(); 560 $in_handles = $viewer->loadHandles($in_phids); 561 $in_handles = $this->getCompleteHandles($in_handles); 562 563 $view = new PHUIPropertyListView(); 564 565 if (!count($in_handles)) { 566 return null; 567 } 568 569 $view->addProperty( 570 pht('Duplicates Merged Here'), $in_handles->renderList()); 571 572 return id(new PHUITabView()) 573 ->setName(pht('Duplicates')) 574 ->setKey('duplicates') 575 ->appendChild($view); 576 } 577 578 private function getCompleteHandles(PhabricatorHandleList $handles) { 579 $phids = array(); 580 581 foreach ($handles as $phid => $handle) { 582 if (!$handle->isComplete()) { 583 continue; 584 } 585 $phids[] = $phid; 586 } 587 588 return $handles->newSublist($phids); 589 } 590 591 private function newChangesView(ManiphestTask $task, array $edges) { 592 $viewer = $this->getViewer(); 593 594 $revision_type = ManiphestTaskHasRevisionEdgeType::EDGECONST; 595 $commit_type = ManiphestTaskHasCommitEdgeType::EDGECONST; 596 597 $revision_phids = idx($edges, $revision_type, array()); 598 $revision_phids = array_keys($revision_phids); 599 $revision_phids = array_fuse($revision_phids); 600 601 $commit_phids = idx($edges, $commit_type, array()); 602 $commit_phids = array_keys($commit_phids); 603 $commit_phids = array_fuse($commit_phids); 604 605 if (!$revision_phids && !$commit_phids) { 606 return null; 607 } 608 609 if ($commit_phids) { 610 $link_type = DiffusionCommitHasRevisionEdgeType::EDGECONST; 611 $link_query = id(new PhabricatorEdgeQuery()) 612 ->withSourcePHIDs($commit_phids) 613 ->withEdgeTypes(array($link_type)); 614 $link_query->execute(); 615 616 $commits = id(new DiffusionCommitQuery()) 617 ->setViewer($viewer) 618 ->withPHIDs($commit_phids) 619 ->execute(); 620 $commits = mpull($commits, null, 'getPHID'); 621 } else { 622 $commits = array(); 623 } 624 625 if ($revision_phids) { 626 $revisions = id(new DifferentialRevisionQuery()) 627 ->setViewer($viewer) 628 ->withPHIDs($revision_phids) 629 ->execute(); 630 $revisions = mpull($revisions, null, 'getPHID'); 631 } else { 632 $revisions = array(); 633 } 634 635 $handle_phids = array(); 636 $any_linked = false; 637 $any_status = false; 638 639 $idx = 0; 640 $objects = array(); 641 foreach ($commit_phids as $commit_phid) { 642 $handle_phids[] = $commit_phid; 643 644 $link_phids = $link_query->getDestinationPHIDs(array($commit_phid)); 645 foreach ($link_phids as $link_phid) { 646 $handle_phids[] = $link_phid; 647 unset($revision_phids[$link_phid]); 648 $any_linked = true; 649 } 650 651 $commit = idx($commits, $commit_phid); 652 if ($commit) { 653 $repository_phid = $commit->getRepository()->getPHID(); 654 $handle_phids[] = $repository_phid; 655 } else { 656 $repository_phid = null; 657 } 658 659 $status_view = null; 660 if ($commit) { 661 $status = $commit->getAuditStatusObject(); 662 if (!$status->isNoAudit()) { 663 $status_view = id(new PHUITagView()) 664 ->setType(PHUITagView::TYPE_SHADE) 665 ->setIcon($status->getIcon()) 666 ->setColor($status->getColor()) 667 ->setName($status->getName()); 668 } 669 } 670 671 $object_link = null; 672 if ($commit) { 673 $commit_monogram = $commit->getDisplayName(); 674 $commit_monogram = phutil_tag( 675 'span', 676 array( 677 'class' => 'object-name', 678 ), 679 $commit_monogram); 680 681 $commit_link = javelin_tag( 682 'a', 683 array( 684 'href' => $commit->getURI(), 685 'sigil' => 'hovercard', 686 'meta' => array( 687 'hovercardSpec' => array( 688 'objectPHID' => $commit->getPHID(), 689 ), 690 ), 691 ), 692 $commit->getSummary()); 693 694 $object_link = array( 695 $commit_monogram, 696 ' ', 697 $commit_link, 698 ); 699 } 700 701 $objects[] = array( 702 'objectPHID' => $commit_phid, 703 'objectLink' => $object_link, 704 'repositoryPHID' => $repository_phid, 705 'revisionPHIDs' => $link_phids, 706 'status' => $status_view, 707 'order' => id(new PhutilSortVector()) 708 ->addInt($repository_phid ? 1 : 0) 709 ->addString((string)$repository_phid) 710 ->addInt(1) 711 ->addInt($idx++), 712 ); 713 } 714 715 foreach ($revision_phids as $revision_phid) { 716 $handle_phids[] = $revision_phid; 717 718 $revision = idx($revisions, $revision_phid); 719 if ($revision) { 720 $repository_phid = $revision->getRepositoryPHID(); 721 $handle_phids[] = $repository_phid; 722 } else { 723 $repository_phid = null; 724 } 725 726 if ($revision) { 727 $icon = $revision->getStatusIcon(); 728 $color = $revision->getStatusIconColor(); 729 $name = $revision->getStatusDisplayName(); 730 731 $status_view = id(new PHUITagView()) 732 ->setType(PHUITagView::TYPE_SHADE) 733 ->setIcon($icon) 734 ->setColor($color) 735 ->setName($name); 736 } else { 737 $status_view = null; 738 } 739 740 $object_link = null; 741 if ($revision) { 742 $revision_monogram = $revision->getMonogram(); 743 $revision_monogram = phutil_tag( 744 'span', 745 array( 746 'class' => 'object-name', 747 ), 748 $revision_monogram); 749 750 $revision_link = javelin_tag( 751 'a', 752 array( 753 'href' => $revision->getURI(), 754 'sigil' => 'hovercard', 755 'meta' => array( 756 'hovercardSpec' => array( 757 'objectPHID' => $revision->getPHID(), 758 ), 759 ), 760 ), 761 $revision->getTitle()); 762 763 $object_link = array( 764 $revision_monogram, 765 ' ', 766 $revision_link, 767 ); 768 } 769 770 $objects[] = array( 771 'objectPHID' => $revision_phid, 772 'objectLink' => $object_link, 773 'repositoryPHID' => $repository_phid, 774 'revisionPHIDs' => array(), 775 'status' => $status_view, 776 'order' => id(new PhutilSortVector()) 777 ->addInt($repository_phid ? 1 : 0) 778 ->addString((string)$repository_phid) 779 ->addInt(0) 780 ->addInt($idx++), 781 ); 782 } 783 784 $handles = $viewer->loadHandles($handle_phids); 785 786 $order = ipull($objects, 'order'); 787 $order = msortv($order, 'getSelf'); 788 $objects = array_select_keys($objects, array_keys($order)); 789 790 $last_repository = false; 791 $rows = array(); 792 $rowd = array(); 793 foreach ($objects as $object) { 794 $repository_phid = $object['repositoryPHID']; 795 if ($repository_phid !== $last_repository) { 796 $repository_link = null; 797 if ($repository_phid) { 798 $repository_handle = $handles[$repository_phid]; 799 $rows[] = array( 800 $repository_handle->renderLink(), 801 ); 802 $rowd[] = true; 803 } 804 805 $last_repository = $repository_phid; 806 } 807 808 $object_phid = $object['objectPHID']; 809 $handle = $handles[$object_phid]; 810 811 $object_link = $object['objectLink']; 812 if ($object_link === null) { 813 $object_link = $handle->renderLink(); 814 } 815 816 $object_icon = id(new PHUIIconView()) 817 ->setIcon($handle->getIcon()); 818 819 $status_view = $object['status']; 820 if ($status_view) { 821 $any_status = true; 822 } 823 824 $revision_tags = array(); 825 foreach ($object['revisionPHIDs'] as $link_phid) { 826 $revision_handle = $handles[$link_phid]; 827 828 $revision_name = $revision_handle->getName(); 829 $revision_tags[] = $revision_handle 830 ->renderHovercardLink($revision_name); 831 } 832 $revision_tags = phutil_implode_html( 833 phutil_tag('br'), 834 $revision_tags); 835 836 $rowd[] = false; 837 $rows[] = array( 838 $object_icon, 839 $status_view, 840 $revision_tags, 841 $object_link, 842 ); 843 } 844 845 $changes_table = id(new AphrontTableView($rows)) 846 ->setNoDataString(pht('This task has no related commits or revisions.')) 847 ->setRowDividers($rowd) 848 ->setColumnClasses( 849 array( 850 'indent center', 851 null, 852 null, 853 'wide pri object-link', 854 )) 855 ->setColumnVisibility( 856 array( 857 true, 858 $any_status, 859 $any_linked, 860 true, 861 )) 862 ->setDeviceVisibility( 863 array( 864 false, 865 $any_status, 866 false, 867 true, 868 )); 869 870 $changes_header = id(new PHUIHeaderView()) 871 ->setHeader(pht('Revisions and Commits')); 872 873 $changes_view = id(new PHUIObjectBoxView()) 874 ->setHeader($changes_header) 875 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 876 ->setTable($changes_table); 877 878 return $changes_view; 879 } 880 881 882}