Path: blob/master/src/applications/maniphest/controller/ManiphestTaskDetailController.php
12262 views
<?php12final class ManiphestTaskDetailController extends ManiphestController {34public function shouldAllowPublic() {5return true;6}78public function handleRequest(AphrontRequest $request) {9$viewer = $this->getViewer();10$id = $request->getURIData('id');1112$task = id(new ManiphestTaskQuery())13->setViewer($viewer)14->withIDs(array($id))15->needSubscriberPHIDs(true)16->executeOne();17if (!$task) {18return new Aphront404Response();19}2021$field_list = PhabricatorCustomField::getObjectFields(22$task,23PhabricatorCustomField::ROLE_VIEW);24$field_list25->setViewer($viewer)26->readFieldsFromStorage($task);2728$edit_engine = id(new ManiphestEditEngine())29->setViewer($viewer)30->setTargetObject($task);3132$edge_types = array(33ManiphestTaskHasCommitEdgeType::EDGECONST,34ManiphestTaskHasRevisionEdgeType::EDGECONST,35ManiphestTaskHasMockEdgeType::EDGECONST,36PhabricatorObjectMentionedByObjectEdgeType::EDGECONST,37PhabricatorObjectMentionsObjectEdgeType::EDGECONST,38ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST,39);4041$phid = $task->getPHID();4243$query = id(new PhabricatorEdgeQuery())44->withSourcePHIDs(array($phid))45->withEdgeTypes($edge_types);46$edges = idx($query->execute(), $phid);47$phids = array_fill_keys($query->getDestinationPHIDs(), true);4849if ($task->getOwnerPHID()) {50$phids[$task->getOwnerPHID()] = true;51}52$phids[$task->getAuthorPHID()] = true;5354$phids = array_keys($phids);55$handles = $viewer->loadHandles($phids);5657$timeline = $this->buildTransactionTimeline(58$task,59new ManiphestTransactionQuery());6061$monogram = $task->getMonogram();62$crumbs = $this->buildApplicationCrumbs()63->addTextCrumb($monogram)64->setBorder(true);6566$header = $this->buildHeaderView($task);67$details = $this->buildPropertyView($task, $field_list, $edges, $handles);68$description = $this->buildDescriptionView($task);69$curtain = $this->buildCurtain($task, $edit_engine);7071$title = pht('%s %s', $monogram, $task->getTitle());7273$comment_view = $edit_engine74->buildEditEngineCommentView($task);7576$timeline->setQuoteRef($monogram);77$comment_view->setTransactionTimeline($timeline);7879$related_tabs = array();80$graph_menu = null;8182$graph_limit = 200;83$overflow_message = null;84$task_graph = id(new ManiphestTaskGraph())85->setViewer($viewer)86->setSeedPHID($task->getPHID())87->setLimit($graph_limit)88->loadGraph();89if (!$task_graph->isEmpty()) {90$parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;91$subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST;92$parent_map = $task_graph->getEdges($parent_type);93$subtask_map = $task_graph->getEdges($subtask_type);94$parent_list = idx($parent_map, $task->getPHID(), array());95$subtask_list = idx($subtask_map, $task->getPHID(), array());96$has_parents = (bool)$parent_list;97$has_subtasks = (bool)$subtask_list;9899// First, get a count of direct parent tasks and subtasks. If there100// are too many of these, we just don't draw anything. You can use101// the search button to browse tasks with the search UI instead.102$direct_count = count($parent_list) + count($subtask_list);103104if ($direct_count > $graph_limit) {105$overflow_message = pht(106'This task is directly connected to more than %s other tasks. '.107'Use %s to browse parents or subtasks, or %s to show more of the '.108'graph.',109new PhutilNumber($graph_limit),110phutil_tag('strong', array(), pht('Search...')),111phutil_tag('strong', array(), pht('View Standalone Graph')));112113$graph_table = null;114} else {115// If there aren't too many direct tasks, but there are too many total116// tasks, we'll only render directly connected tasks.117if ($task_graph->isOverLimit()) {118$task_graph->setRenderOnlyAdjacentNodes(true);119120$overflow_message = pht(121'This task is connected to more than %s other tasks. '.122'Only direct parents and subtasks are shown here. Use '.123'%s to show more of the graph.',124new PhutilNumber($graph_limit),125phutil_tag('strong', array(), pht('View Standalone Graph')));126}127128$graph_table = $task_graph->newGraphTable();129}130131if ($overflow_message) {132$overflow_view = $this->newTaskGraphOverflowView(133$task,134$overflow_message,135true);136137$graph_table = array(138$overflow_view,139$graph_table,140);141}142143$graph_menu = $this->newTaskGraphDropdownMenu(144$task,145$has_parents,146$has_subtasks,147true);148149$related_tabs[] = id(new PHUITabView())150->setName(pht('Task Graph'))151->setKey('graph')152->appendChild($graph_table);153}154155$related_tabs[] = $this->newMocksTab($task, $query);156$related_tabs[] = $this->newMentionsTab($task, $query);157$related_tabs[] = $this->newDuplicatesTab($task, $query);158159$tab_view = null;160161$related_tabs = array_filter($related_tabs);162if ($related_tabs) {163$tab_group = new PHUITabGroupView();164foreach ($related_tabs as $tab) {165$tab_group->addTab($tab);166}167168$related_header = id(new PHUIHeaderView())169->setHeader(pht('Related Objects'));170171if ($graph_menu) {172$related_header->addActionLink($graph_menu);173}174175$tab_view = id(new PHUIObjectBoxView())176->setHeader($related_header)177->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)178->addTabGroup($tab_group);179}180181$changes_view = $this->newChangesView($task, $edges);182183$view = id(new PHUITwoColumnView())184->setHeader($header)185->setCurtain($curtain)186->setMainColumn(187array(188$changes_view,189$tab_view,190$timeline,191$comment_view,192))193->addPropertySection(pht('Description'), $description)194->addPropertySection(pht('Details'), $details);195196197return $this->newPage()198->setTitle($title)199->setCrumbs($crumbs)200->setPageObjectPHIDs(201array(202$task->getPHID(),203))204->appendChild($view);205206}207208private function buildHeaderView(ManiphestTask $task) {209$view = id(new PHUIHeaderView())210->setHeader($task->getTitle())211->setUser($this->getRequest()->getUser())212->setPolicyObject($task);213214$priority_name = ManiphestTaskPriority::getTaskPriorityName(215$task->getPriority());216$priority_color = ManiphestTaskPriority::getTaskPriorityColor(217$task->getPriority());218219$status = $task->getStatus();220$status_name = ManiphestTaskStatus::renderFullDescription(221$status, $priority_name);222$view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name);223224$view->setHeaderIcon(ManiphestTaskStatus::getStatusIcon(225$task->getStatus()).' '.$priority_color);226227if (ManiphestTaskPoints::getIsEnabled()) {228$points = $task->getPoints();229if ($points !== null) {230$points_name = pht('%s %s',231$task->getPoints(),232ManiphestTaskPoints::getPointsLabel());233$tag = id(new PHUITagView())234->setName($points_name)235->setColor(PHUITagView::COLOR_BLUE)236->setType(PHUITagView::TYPE_SHADE);237238$view->addTag($tag);239}240}241242$subtype = $task->newSubtypeObject();243if ($subtype && $subtype->hasTagView()) {244$subtype_tag = $subtype->newTagView();245$view->addTag($subtype_tag);246}247248return $view;249}250251252private function buildCurtain(253ManiphestTask $task,254PhabricatorEditEngine $edit_engine) {255$viewer = $this->getViewer();256257$id = $task->getID();258$phid = $task->getPHID();259260$can_edit = PhabricatorPolicyFilter::hasCapability(261$viewer,262$task,263PhabricatorPolicyCapability::CAN_EDIT);264265$can_interact = PhabricatorPolicyFilter::canInteract($viewer, $task);266267// We expect a policy dialog if you can't edit the task, and expect a268// lock override dialog if you can't interact with it.269$workflow_edit = (!$can_edit || !$can_interact);270271$curtain = $this->newCurtainView($task);272273$curtain->addAction(274id(new PhabricatorActionView())275->setName(pht('Edit Task'))276->setIcon('fa-pencil')277->setHref($this->getApplicationURI("/task/edit/{$id}/"))278->setDisabled(!$can_edit)279->setWorkflow($workflow_edit));280281$subtype_map = $task->newEditEngineSubtypeMap();282$subtask_options = $subtype_map->getCreateFormsForSubtype(283$edit_engine,284$task);285286// If no forms are available, we want to show the user an error.287// If one form is available, we take them user directly to the form.288// If two or more forms are available, we give the user a choice.289290// The "subtask" controller handles the first case (no forms) and the291// third case (more than one form). In the case of one form, we link292// directly to the form.293$subtask_uri = "/task/subtask/{$id}/";294$subtask_workflow = true;295296if (count($subtask_options) == 1) {297$subtask_form = head($subtask_options);298$form_key = $subtask_form->getIdentifier();299$subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/"))300->replaceQueryParam('parent', $id)301->replaceQueryParam('template', $id)302->replaceQueryParam('status', ManiphestTaskStatus::getDefaultStatus());303$subtask_workflow = false;304}305306$subtask_uri = $this->getApplicationURI($subtask_uri);307308$subtask_item = id(new PhabricatorActionView())309->setName(pht('Create Subtask'))310->setHref($subtask_uri)311->setIcon('fa-level-down')312->setDisabled(!$subtask_options)313->setWorkflow($subtask_workflow);314315$relationship_list = PhabricatorObjectRelationshipList::newForObject(316$viewer,317$task);318319$submenu_actions = array(320$subtask_item,321ManiphestTaskHasParentRelationship::RELATIONSHIPKEY,322ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY,323ManiphestTaskMergeInRelationship::RELATIONSHIPKEY,324ManiphestTaskCloseAsDuplicateRelationship::RELATIONSHIPKEY,325);326327$task_submenu = $relationship_list->newActionSubmenu($submenu_actions)328->setName(pht('Edit Related Tasks...'))329->setIcon('fa-anchor');330331$curtain->addAction($task_submenu);332333$relationship_submenu = $relationship_list->newActionMenu();334if ($relationship_submenu) {335$curtain->addAction($relationship_submenu);336}337338$viewer_phid = $viewer->getPHID();339$owner_phid = $task->getOwnerPHID();340$author_phid = $task->getAuthorPHID();341$handles = $viewer->loadHandles(array($owner_phid, $author_phid));342343$assigned_refs = id(new PHUICurtainObjectRefListView())344->setViewer($viewer)345->setEmptyMessage(pht('None'));346347if ($owner_phid) {348$assigned_ref = $assigned_refs->newObjectRefView()349->setHandle($handles[$owner_phid])350->setHighlighted($owner_phid === $viewer_phid);351}352353$curtain->newPanel()354->setHeaderText(pht('Assigned To'))355->appendChild($assigned_refs);356357$author_refs = id(new PHUICurtainObjectRefListView())358->setViewer($viewer);359360$author_ref = $author_refs->newObjectRefView()361->setHandle($handles[$author_phid])362->setEpoch($task->getDateCreated())363->setHighlighted($author_phid === $viewer_phid);364365$curtain->newPanel()366->setHeaderText(pht('Authored By'))367->appendChild($author_refs);368369return $curtain;370}371372private function buildPropertyView(373ManiphestTask $task,374PhabricatorCustomFieldList $field_list,375array $edges,376$handles) {377378$viewer = $this->getRequest()->getUser();379$view = id(new PHUIPropertyListView())380->setUser($viewer);381382$source = $task->getOriginalEmailSource();383if ($source) {384$subject = '[T'.$task->getID().'] '.$task->getTitle();385$view->addProperty(386pht('From Email'),387phutil_tag(388'a',389array(390'href' => 'mailto:'.$source.'?subject='.$subject,391),392$source));393}394395$field_list->appendFieldsToPropertyList(396$task,397$viewer,398$view);399400if ($view->hasAnyProperties()) {401return $view;402}403404return null;405}406407private function buildDescriptionView(ManiphestTask $task) {408$viewer = $this->getViewer();409410$section = null;411412$description = $task->getDescription();413if (strlen($description)) {414$section = new PHUIPropertyListView();415$section->addTextContent(416phutil_tag(417'div',418array(419'class' => 'phabricator-remarkup',420),421id(new PHUIRemarkupView($viewer, $description))422->setContextObject($task)));423}424425return $section;426}427428private function newMocksTab(429ManiphestTask $task,430PhabricatorEdgeQuery $edge_query) {431432$mock_type = ManiphestTaskHasMockEdgeType::EDGECONST;433$mock_phids = $edge_query->getDestinationPHIDs(array(), array($mock_type));434if (!$mock_phids) {435return null;436}437438$viewer = $this->getViewer();439$handles = $viewer->loadHandles($mock_phids);440441// TODO: It would be nice to render this as pinboard-style thumbnails,442// similar to "{M123}", instead of a list of links.443444$view = id(new PHUIPropertyListView())445->addProperty(pht('Mocks'), $handles->renderList());446447return id(new PHUITabView())448->setName(pht('Mocks'))449->setKey('mocks')450->appendChild($view);451}452453private function newMentionsTab(454ManiphestTask $task,455PhabricatorEdgeQuery $edge_query) {456457$in_type = PhabricatorObjectMentionedByObjectEdgeType::EDGECONST;458$out_type = PhabricatorObjectMentionsObjectEdgeType::EDGECONST;459460$in_phids = $edge_query->getDestinationPHIDs(array(), array($in_type));461$out_phids = $edge_query->getDestinationPHIDs(array(), array($out_type));462463// Filter out any mentioned users from the list. These are not generally464// very interesting to show in a relationship summary since they usually465// end up as subscribers anyway.466467$user_type = PhabricatorPeopleUserPHIDType::TYPECONST;468foreach ($out_phids as $key => $out_phid) {469if (phid_get_type($out_phid) == $user_type) {470unset($out_phids[$key]);471}472}473474if (!$in_phids && !$out_phids) {475return null;476}477478$viewer = $this->getViewer();479$in_handles = $viewer->loadHandles($in_phids);480$out_handles = $viewer->loadHandles($out_phids);481482$in_handles = $this->getCompleteHandles($in_handles);483$out_handles = $this->getCompleteHandles($out_handles);484485if (!count($in_handles) && !count($out_handles)) {486return null;487}488489$view = new PHUIPropertyListView();490491if (count($in_handles)) {492$view->addProperty(pht('Mentioned In'), $in_handles->renderList());493}494495if (count($out_handles)) {496$view->addProperty(pht('Mentioned Here'), $out_handles->renderList());497}498499return id(new PHUITabView())500->setName(pht('Mentions'))501->setKey('mentions')502->appendChild($view);503}504505private function newDuplicatesTab(506ManiphestTask $task,507PhabricatorEdgeQuery $edge_query) {508509$in_type = ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST;510$in_phids = $edge_query->getDestinationPHIDs(array(), array($in_type));511512$viewer = $this->getViewer();513$in_handles = $viewer->loadHandles($in_phids);514$in_handles = $this->getCompleteHandles($in_handles);515516$view = new PHUIPropertyListView();517518if (!count($in_handles)) {519return null;520}521522$view->addProperty(523pht('Duplicates Merged Here'), $in_handles->renderList());524525return id(new PHUITabView())526->setName(pht('Duplicates'))527->setKey('duplicates')528->appendChild($view);529}530531private function getCompleteHandles(PhabricatorHandleList $handles) {532$phids = array();533534foreach ($handles as $phid => $handle) {535if (!$handle->isComplete()) {536continue;537}538$phids[] = $phid;539}540541return $handles->newSublist($phids);542}543544private function newChangesView(ManiphestTask $task, array $edges) {545$viewer = $this->getViewer();546547$revision_type = ManiphestTaskHasRevisionEdgeType::EDGECONST;548$commit_type = ManiphestTaskHasCommitEdgeType::EDGECONST;549550$revision_phids = idx($edges, $revision_type, array());551$revision_phids = array_keys($revision_phids);552$revision_phids = array_fuse($revision_phids);553554$commit_phids = idx($edges, $commit_type, array());555$commit_phids = array_keys($commit_phids);556$commit_phids = array_fuse($commit_phids);557558if (!$revision_phids && !$commit_phids) {559return null;560}561562if ($commit_phids) {563$link_type = DiffusionCommitHasRevisionEdgeType::EDGECONST;564$link_query = id(new PhabricatorEdgeQuery())565->withSourcePHIDs($commit_phids)566->withEdgeTypes(array($link_type));567$link_query->execute();568569$commits = id(new DiffusionCommitQuery())570->setViewer($viewer)571->withPHIDs($commit_phids)572->execute();573$commits = mpull($commits, null, 'getPHID');574} else {575$commits = array();576}577578if ($revision_phids) {579$revisions = id(new DifferentialRevisionQuery())580->setViewer($viewer)581->withPHIDs($revision_phids)582->execute();583$revisions = mpull($revisions, null, 'getPHID');584} else {585$revisions = array();586}587588$handle_phids = array();589$any_linked = false;590$any_status = false;591592$idx = 0;593$objects = array();594foreach ($commit_phids as $commit_phid) {595$handle_phids[] = $commit_phid;596597$link_phids = $link_query->getDestinationPHIDs(array($commit_phid));598foreach ($link_phids as $link_phid) {599$handle_phids[] = $link_phid;600unset($revision_phids[$link_phid]);601$any_linked = true;602}603604$commit = idx($commits, $commit_phid);605if ($commit) {606$repository_phid = $commit->getRepository()->getPHID();607$handle_phids[] = $repository_phid;608} else {609$repository_phid = null;610}611612$status_view = null;613if ($commit) {614$status = $commit->getAuditStatusObject();615if (!$status->isNoAudit()) {616$status_view = id(new PHUITagView())617->setType(PHUITagView::TYPE_SHADE)618->setIcon($status->getIcon())619->setColor($status->getColor())620->setName($status->getName());621}622}623624$object_link = null;625if ($commit) {626$commit_monogram = $commit->getDisplayName();627$commit_monogram = phutil_tag(628'span',629array(630'class' => 'object-name',631),632$commit_monogram);633634$commit_link = javelin_tag(635'a',636array(637'href' => $commit->getURI(),638'sigil' => 'hovercard',639'meta' => array(640'hovercardSpec' => array(641'objectPHID' => $commit->getPHID(),642),643),644),645$commit->getSummary());646647$object_link = array(648$commit_monogram,649' ',650$commit_link,651);652}653654$objects[] = array(655'objectPHID' => $commit_phid,656'objectLink' => $object_link,657'repositoryPHID' => $repository_phid,658'revisionPHIDs' => $link_phids,659'status' => $status_view,660'order' => id(new PhutilSortVector())661->addInt($repository_phid ? 1 : 0)662->addString((string)$repository_phid)663->addInt(1)664->addInt($idx++),665);666}667668foreach ($revision_phids as $revision_phid) {669$handle_phids[] = $revision_phid;670671$revision = idx($revisions, $revision_phid);672if ($revision) {673$repository_phid = $revision->getRepositoryPHID();674$handle_phids[] = $repository_phid;675} else {676$repository_phid = null;677}678679if ($revision) {680$icon = $revision->getStatusIcon();681$color = $revision->getStatusIconColor();682$name = $revision->getStatusDisplayName();683684$status_view = id(new PHUITagView())685->setType(PHUITagView::TYPE_SHADE)686->setIcon($icon)687->setColor($color)688->setName($name);689} else {690$status_view = null;691}692693$object_link = null;694if ($revision) {695$revision_monogram = $revision->getMonogram();696$revision_monogram = phutil_tag(697'span',698array(699'class' => 'object-name',700),701$revision_monogram);702703$revision_link = javelin_tag(704'a',705array(706'href' => $revision->getURI(),707'sigil' => 'hovercard',708'meta' => array(709'hovercardSpec' => array(710'objectPHID' => $revision->getPHID(),711),712),713),714$revision->getTitle());715716$object_link = array(717$revision_monogram,718' ',719$revision_link,720);721}722723$objects[] = array(724'objectPHID' => $revision_phid,725'objectLink' => $object_link,726'repositoryPHID' => $repository_phid,727'revisionPHIDs' => array(),728'status' => $status_view,729'order' => id(new PhutilSortVector())730->addInt($repository_phid ? 1 : 0)731->addString((string)$repository_phid)732->addInt(0)733->addInt($idx++),734);735}736737$handles = $viewer->loadHandles($handle_phids);738739$order = ipull($objects, 'order');740$order = msortv($order, 'getSelf');741$objects = array_select_keys($objects, array_keys($order));742743$last_repository = false;744$rows = array();745$rowd = array();746foreach ($objects as $object) {747$repository_phid = $object['repositoryPHID'];748if ($repository_phid !== $last_repository) {749$repository_link = null;750if ($repository_phid) {751$repository_handle = $handles[$repository_phid];752$rows[] = array(753$repository_handle->renderLink(),754);755$rowd[] = true;756}757758$last_repository = $repository_phid;759}760761$object_phid = $object['objectPHID'];762$handle = $handles[$object_phid];763764$object_link = $object['objectLink'];765if ($object_link === null) {766$object_link = $handle->renderLink();767}768769$object_icon = id(new PHUIIconView())770->setIcon($handle->getIcon());771772$status_view = $object['status'];773if ($status_view) {774$any_status = true;775}776777$revision_tags = array();778foreach ($object['revisionPHIDs'] as $link_phid) {779$revision_handle = $handles[$link_phid];780781$revision_name = $revision_handle->getName();782$revision_tags[] = $revision_handle783->renderHovercardLink($revision_name);784}785$revision_tags = phutil_implode_html(786phutil_tag('br'),787$revision_tags);788789$rowd[] = false;790$rows[] = array(791$object_icon,792$status_view,793$revision_tags,794$object_link,795);796}797798$changes_table = id(new AphrontTableView($rows))799->setNoDataString(pht('This task has no related commits or revisions.'))800->setRowDividers($rowd)801->setColumnClasses(802array(803'indent center',804null,805null,806'wide pri object-link',807))808->setColumnVisibility(809array(810true,811$any_status,812$any_linked,813true,814))815->setDeviceVisibility(816array(817false,818$any_status,819false,820true,821));822823$changes_header = id(new PHUIHeaderView())824->setHeader(pht('Revisions and Commits'));825826$changes_view = id(new PHUIObjectBoxView())827->setHeader($changes_header)828->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)829->setTable($changes_table);830831return $changes_view;832}833834835}836837838