@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<?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}