diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '4ed8ce1f', + 'core.pkg.css' => '85a1da99', 'core.pkg.js' => '5c737607', 'differential.pkg.css' => 'b8df73d4', 'differential.pkg.js' => '67c9ea4c', @@ -30,7 +30,7 @@ 'rsrc/css/aphront/notification.css' => '30240bd2', 'rsrc/css/aphront/panel-view.css' => '46923d46', 'rsrc/css/aphront/phabricator-nav-view.css' => 'f8a0c1bf', - 'rsrc/css/aphront/table-view.css' => 'daa1f9df', + 'rsrc/css/aphront/table-view.css' => '205053cd', 'rsrc/css/aphront/tokenizer.css' => 'b52d0668', 'rsrc/css/aphront/tooltip.css' => 'e3f2412f', 'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2', @@ -520,7 +520,7 @@ 'aphront-list-filter-view-css' => 'feb64255', 'aphront-multi-column-view-css' => 'fbc00ba3', 'aphront-panel-view-css' => '46923d46', - 'aphront-table-view-css' => 'daa1f9df', + 'aphront-table-view-css' => '205053cd', 'aphront-tokenizer-control-css' => 'b52d0668', 'aphront-tooltip-css' => 'e3f2412f', 'aphront-typeahead-control-css' => '8779483d', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1714,6 +1714,7 @@ 'ManiphestTaskFerretEngine' => 'applications/maniphest/search/ManiphestTaskFerretEngine.php', 'ManiphestTaskFulltextEngine' => 'applications/maniphest/search/ManiphestTaskFulltextEngine.php', 'ManiphestTaskGraph' => 'infrastructure/graph/ManiphestTaskGraph.php', + 'ManiphestTaskGraphController' => 'applications/maniphest/controller/ManiphestTaskGraphController.php', 'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php', 'ManiphestTaskHasCommitRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php', 'ManiphestTaskHasDuplicateTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php', @@ -7401,6 +7402,7 @@ 'ManiphestTaskFerretEngine' => 'PhabricatorFerretEngine', 'ManiphestTaskFulltextEngine' => 'PhabricatorFulltextEngine', 'ManiphestTaskGraph' => 'PhabricatorObjectGraph', + 'ManiphestTaskGraphController' => 'ManiphestController', 'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasCommitRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHasDuplicateTaskEdgeType' => 'PhabricatorEdgeType', diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php --- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php +++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php @@ -55,6 +55,7 @@ 'subtask/(?P[1-9]\d*)/' => 'ManiphestTaskSubtaskController', ), 'subpriority/' => 'ManiphestSubpriorityController', + 'graph/(?P[1-9]\d*)/' => 'ManiphestTaskGraphController', ), ); } diff --git a/src/applications/maniphest/controller/ManiphestController.php b/src/applications/maniphest/controller/ManiphestController.php --- a/src/applications/maniphest/controller/ManiphestController.php +++ b/src/applications/maniphest/controller/ManiphestController.php @@ -61,4 +61,102 @@ return $view; } + final protected function newTaskGraphDropdownMenu( + ManiphestTask $task, + $has_parents, + $has_subtasks, + $include_standalone) { + $viewer = $this->getViewer(); + + $parents_uri = urisprintf( + '/?subtaskIDs=%d#R', + $task->getID()); + $parents_uri = $this->getApplicationURI($parents_uri); + + $subtasks_uri = urisprintf( + '/?parentIDs=%d#R', + $task->getID()); + $subtasks_uri = $this->getApplicationURI($subtasks_uri); + + $dropdown_menu = id(new PhabricatorActionListView()) + ->setViewer($viewer) + ->addAction( + id(new PhabricatorActionView()) + ->setHref($parents_uri) + ->setName(pht('Search Parent Tasks')) + ->setDisabled(!$has_parents) + ->setIcon('fa-chevron-circle-up')) + ->addAction( + id(new PhabricatorActionView()) + ->setHref($subtasks_uri) + ->setName(pht('Search Subtasks')) + ->setDisabled(!$has_subtasks) + ->setIcon('fa-chevron-circle-down')); + + if ($include_standalone) { + $standalone_uri = urisprintf('/graph/%d/', $task->getID()); + $standalone_uri = $this->getApplicationURI($standalone_uri); + + $dropdown_menu->addAction( + id(new PhabricatorActionView()) + ->setHref($standalone_uri) + ->setName(pht('View Standalone Graph')) + ->setIcon('fa-code-fork')); + } + + $graph_menu = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-search') + ->setText(pht('Search...')) + ->setDropdownMenu($dropdown_menu); + + return $graph_menu; + } + + final protected function newTaskGraphOverflowView( + ManiphestTask $task, + $overflow_message, + $include_standalone) { + + $id = $task->getID(); + + if ($include_standalone) { + $standalone_uri = $this->getApplicationURI("graph/{$id}/"); + + $standalone_link = id(new PHUIButtonView()) + ->setTag('a') + ->setHref($standalone_uri) + ->setColor(PHUIButtonView::GREY) + ->setIcon('fa-code-fork') + ->setText(pht('View Standalone Graph')); + } else { + $standalone_link = null; + } + + $standalone_icon = id(new PHUIIconView()) + ->setIcon('fa-exclamation-triangle', 'yellow') + ->addClass('object-graph-header-icon'); + + $standalone_view = phutil_tag( + 'div', + array( + 'class' => 'object-graph-header', + ), + array( + $standalone_link, + $standalone_icon, + phutil_tag( + 'div', + array( + 'class' => 'object-graph-header-message', + ), + array( + $overflow_message, + )), + )); + + return $standalone_view; + } + + } diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -80,7 +80,8 @@ $related_tabs = array(); $graph_menu = null; - $graph_limit = 100; + $graph_limit = 200; + $overflow_message = null; $task_graph = id(new ManiphestTaskGraph()) ->setViewer($viewer) ->setSeedPHID($task->getPHID()) @@ -96,61 +97,55 @@ $has_parents = (bool)$parent_list; $has_subtasks = (bool)$subtask_list; - $search_text = pht('Search...'); - // First, get a count of direct parent tasks and subtasks. If there // are too many of these, we just don't draw anything. You can use // the search button to browse tasks with the search UI instead. $direct_count = count($parent_list) + count($subtask_list); if ($direct_count > $graph_limit) { - $message = pht( - 'Task graph too large to display (this task is directly connected '. - 'to more than %s other tasks). Use %s to explore connected tasks.', - $graph_limit, - phutil_tag('strong', array(), $search_text)); - $message = phutil_tag('em', array(), $message); - $graph_table = id(new PHUIPropertyListView()) - ->addTextContent($message); + $overflow_message = pht( + 'This task is directly connected to more than %s other tasks. '. + 'Use %s to browse parents or subtasks, or %s to show more of the '. + 'graph.', + new PhutilNumber($graph_limit), + phutil_tag('strong', array(), pht('Search...')), + phutil_tag('strong', array(), pht('View Standalone Graph'))); + + $graph_table = null; } else { // If there aren't too many direct tasks, but there are too many total // tasks, we'll only render directly connected tasks. if ($task_graph->isOverLimit()) { $task_graph->setRenderOnlyAdjacentNodes(true); + + $overflow_message = pht( + 'This task is connected to more than %s other tasks. '. + 'Only direct parents and subtasks are shown here. Use '. + '%s to show more of the graph.', + new PhutilNumber($graph_limit), + phutil_tag('strong', array(), pht('View Standalone Graph'))); } + $graph_table = $task_graph->newGraphTable(); } - $parents_uri = urisprintf( - '/?subtaskIDs=%d#R', - $task->getID()); - $parents_uri = $this->getApplicationURI($parents_uri); - - $subtasks_uri = urisprintf( - '/?parentIDs=%d#R', - $task->getID()); - $subtasks_uri = $this->getApplicationURI($subtasks_uri); - - $dropdown_menu = id(new PhabricatorActionListView()) - ->setViewer($viewer) - ->addAction( - id(new PhabricatorActionView()) - ->setHref($parents_uri) - ->setName(pht('Search Parent Tasks')) - ->setDisabled(!$has_parents) - ->setIcon('fa-chevron-circle-up')) - ->addAction( - id(new PhabricatorActionView()) - ->setHref($subtasks_uri) - ->setName(pht('Search Subtasks')) - ->setDisabled(!$has_subtasks) - ->setIcon('fa-chevron-circle-down')); - - $graph_menu = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-search') - ->setText($search_text) - ->setDropdownMenu($dropdown_menu); + if ($overflow_message) { + $overflow_view = $this->newTaskGraphOverflowView( + $task, + $overflow_message, + true); + + $graph_table = array( + $overflow_view, + $graph_table, + ); + } + + $graph_menu = $this->newTaskGraphDropdownMenu( + $task, + $has_parents, + $has_subtasks, + true); $related_tabs[] = id(new PHUITabView()) ->setName(pht('Task Graph')) diff --git a/src/applications/maniphest/controller/ManiphestTaskGraphController.php b/src/applications/maniphest/controller/ManiphestTaskGraphController.php new file mode 100644 --- /dev/null +++ b/src/applications/maniphest/controller/ManiphestTaskGraphController.php @@ -0,0 +1,125 @@ +getViewer(); + $id = $request->getURIData('id'); + + $task = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$task) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($task->getMonogram(), $task->getURI()) + ->addTextCrumb(pht('Graph')) + ->setBorder(true); + + $graph_limit = 2000; + $overflow_message = null; + $task_graph = id(new ManiphestTaskGraph()) + ->setViewer($viewer) + ->setSeedPHID($task->getPHID()) + ->setLimit($graph_limit) + ->loadGraph(); + if (!$task_graph->isEmpty()) { + $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; + $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; + $parent_map = $task_graph->getEdges($parent_type); + $subtask_map = $task_graph->getEdges($subtask_type); + $parent_list = idx($parent_map, $task->getPHID(), array()); + $subtask_list = idx($subtask_map, $task->getPHID(), array()); + $has_parents = (bool)$parent_list; + $has_subtasks = (bool)$subtask_list; + + // First, get a count of direct parent tasks and subtasks. If there + // are too many of these, we just don't draw anything. You can use + // the search button to browse tasks with the search UI instead. + $direct_count = count($parent_list) + count($subtask_list); + + if ($direct_count > $graph_limit) { + $overflow_message = pht( + 'This task is directly connected to more than %s other tasks, '. + 'which is too many tasks to display. Use %s to browse parents '. + 'or subtasks.', + new PhutilNumber($graph_limit), + phutil_tag('strong', array(), pht('Search...'))); + + $graph_table = null; + } else { + // If there aren't too many direct tasks, but there are too many total + // tasks, we'll only render directly connected tasks. + if ($task_graph->isOverLimit()) { + $task_graph->setRenderOnlyAdjacentNodes(true); + + $overflow_message = pht( + 'This task is connected to more than %s other tasks. '. + 'Only direct parents and subtasks are shown here.', + new PhutilNumber($graph_limit)); + } + + $graph_table = $task_graph->newGraphTable(); + } + + $graph_menu = $this->newTaskGraphDropdownMenu( + $task, + $has_parents, + $has_subtasks, + false); + } else { + $graph_menu = null; + $graph_table = null; + + $overflow_message = pht( + 'This task has no parent tasks and no subtasks, so there is no '. + 'graph to draw.'); + } + + if ($overflow_message) { + $overflow_view = $this->newTaskGraphOverflowView( + $task, + $overflow_message, + false); + + $graph_table = array( + $overflow_view, + $graph_table, + ); + } + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Task Graph')); + + if ($graph_menu) { + $header->addActionLink($graph_menu); + } + + $tab_view = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($graph_table); + + $view = id(new PHUITwoColumnView()) + ->setFooter($tab_view); + + return $this->newPage() + ->setTitle( + array( + $task->getMonogram(), + pht('Graph'), + )) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + +} diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -327,3 +327,40 @@ .phui-object-box .aphront-table-view { border: none; } + +.object-graph-header { + padding: 8px 12px; + overflow: hidden; + background: {$lightyellow}; + border-bottom: 1px solid {$lightblueborder}; + vertical-align: middle; +} + +.object-graph-header .object-graph-header-icon { + float: left; + margin-top: 10px; +} + +.object-graph-header a.button { + float: right; +} + +.object-graph-header-message { + margin: 8px 200px 8px 20px; +} + +.device .object-graph-header .object-graph-header-icon { + display: none; +} + +.device .object-graph-header-message { + clear: both; + margin: 0; +} + +.device .object-graph-header a.button { + margin: 0 auto 12px; + display: block; + width: 180px; + float: none; +}