diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'b4a7e275', + 'core.pkg.css' => 'bef9c7cb', 'core.pkg.js' => '17380dd3', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -146,10 +146,10 @@ 'rsrc/css/phui/phui-object-item-list-view.css' => '8f443e8b', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', - 'rsrc/css/phui/phui-profile-menu.css' => '4a243229', + 'rsrc/css/phui/phui-profile-menu.css' => '2d5f0c75', 'rsrc/css/phui/phui-property-list-view.css' => '27b2849e', 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', - 'rsrc/css/phui/phui-segment-bar-view.css' => '728e4d19', + 'rsrc/css/phui/phui-segment-bar-view.css' => '52e7e529', 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => '888cedb8', 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', @@ -823,10 +823,10 @@ 'phui-object-item-list-view-css' => '8f443e8b', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', - 'phui-profile-menu-css' => '4a243229', + 'phui-profile-menu-css' => '2d5f0c75', 'phui-property-list-view-css' => '27b2849e', 'phui-remarkup-preview-css' => '1a8f2591', - 'phui-segment-bar-view-css' => '728e4d19', + 'phui-segment-bar-view-css' => '52e7e529', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '888cedb8', 'phui-tag-view-css' => '9d5d4400', 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 @@ -2933,6 +2933,7 @@ 'PhabricatorProjectOrUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserFunctionDatasource.php', 'PhabricatorProjectPHIDResolver' => 'applications/phid/resolver/PhabricatorProjectPHIDResolver.php', 'PhabricatorProjectPanelController' => 'applications/project/controller/PhabricatorProjectPanelController.php', + 'PhabricatorProjectPointsProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectPointsProfilePanel.php', 'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php', 'PhabricatorProjectProfilePanelEngine' => 'applications/project/engine/PhabricatorProjectProfilePanelEngine.php', 'PhabricatorProjectProjectHasMemberEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasMemberEdgeType.php', @@ -7364,6 +7365,7 @@ 'PhabricatorProjectOrUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectPHIDResolver' => 'PhabricatorPHIDResolver', 'PhabricatorProjectPanelController' => 'PhabricatorProjectController', + 'PhabricatorProjectPointsProfilePanel' => 'PhabricatorProfilePanel', 'PhabricatorProjectProfileController' => 'PhabricatorProjectController', 'PhabricatorProjectProfilePanelEngine' => 'PhabricatorProfilePanelEngine', 'PhabricatorProjectProjectHasMemberEdgeType' => 'PhabricatorEdgeType', diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -307,28 +307,45 @@ // currently leave the card where it was but should really move it to the // proper new column. + $board_phid = $column->getProjectPHID(); + $descendant_projects = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withAncestorProjectPHIDs(array($column->getProjectPHID())) ->execute(); $board_phids = mpull($descendant_projects, 'getPHID', 'getPHID'); - $board_phids[$column->getProjectPHID()] = $column->getProjectPHID(); + $board_phids[$board_phid] = $board_phid; $project_map = array_fuse($task->getProjectPHIDs()); $remove_card = !array_intersect_key($board_phids, $project_map); - $positions = id(new PhabricatorProjectColumnPositionQuery()) + // TODO: Maybe the caller should pass a list of visible task PHIDs so we + // know which ones we need to reorder? This is a HUGE overfetch. + $objects = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withEdgeLogicPHIDs( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_ANCESTOR, + array($board_phids)) ->setViewer($viewer) - ->withBoardPHIDs(array($column->getProjectPHID())) - ->withColumnPHIDs(array($column->getPHID())) ->execute(); - $task_phids = mpull($positions, 'getObjectPHID'); + $objects = mpull($objects, null, 'getPHID'); - $column_tasks = id(new ManiphestTaskQuery()) + $layout_engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) - ->withPHIDs($task_phids) - ->needProjectPHIDs(true) - ->execute(); + ->setBoardPHIDs(array($board_phid)) + ->setObjectPHIDs(array_keys($objects)) + ->executeLayout(); + + $positions = $layout_engine->getColumnObjectPositions( + $board_phid, + $column_phid); + + $column_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $column_phid); + + $column_tasks = array_select_keys($objects, $column_phids); if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { // TODO: This is a little bit awkward, because PHP and JS use diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -86,9 +86,14 @@ return array_select_keys($this->columnMap, array_keys($columns)); } - public function getColumnObjectPHIDs($board_phid, $column_phid) { + public function getColumnObjectPositions($board_phid, $column_phid) { $columns = idx($this->boardLayout, $board_phid, array()); - $positions = idx($columns, $column_phid, array()); + return idx($columns, $column_phid, array()); + } + + + public function getColumnObjectPHIDs($board_phid, $column_phid) { + $positions = $this->getColumnObjectPositions($board_phid, $column_phid); return mpull($positions, 'getObjectPHID'); } diff --git a/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php b/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php --- a/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php +++ b/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php @@ -21,6 +21,10 @@ ->setPanelKey(PhabricatorProjectDetailsProfilePanel::PANELKEY); $panels[] = $this->newPanel() + ->setBuiltinKey(PhabricatorProject::PANEL_POINTS) + ->setPanelKey(PhabricatorProjectPointsProfilePanel::PANELKEY); + + $panels[] = $this->newPanel() ->setBuiltinKey(PhabricatorProject::PANEL_WORKBOARD) ->setPanelKey(PhabricatorProjectWorkboardProfilePanel::PANELKEY); diff --git a/src/applications/project/profilepanel/PhabricatorProjectPointsProfilePanel.php b/src/applications/project/profilepanel/PhabricatorProjectPointsProfilePanel.php new file mode 100644 --- /dev/null +++ b/src/applications/project/profilepanel/PhabricatorProjectPointsProfilePanel.php @@ -0,0 +1,192 @@ +getViewer(); + + // Only render this element for milestones. + if (!$object->isMilestone()) { + return false; + } + + // Don't show if points aren't configured. + if (!ManiphestTaskPoints::getIsEnabled()) { + return false; + } + + // Points are only available if Maniphest is installed. + $class = 'PhabricatorManiphestApplication'; + if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + return false; + } + + return true; + } + + public function getDisplayName( + PhabricatorProfilePanelConfiguration $config) { + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfilePanelConfiguration $config) { + return array( + id(new PhabricatorInstructionsEditField()) + ->setValue( + pht( + 'This is a progress bar which shows how many points of work '. + 'are complete within the milestone. It has no configurable '. + 'settings.')), + ); + } + + protected function newNavigationMenuItems( + PhabricatorProfilePanelConfiguration $config) { + $viewer = $this->getViewer(); + $project = $config->getProfileObject(); + + $limit = 250; + + $tasks = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withEdgeLogicPHIDs( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_AND, + array($project->getPHID())) + ->setLimit($limit + 1) + ->execute(); + + if (count($tasks) > $limit) { + return $this->renderError( + pht( + 'Too many tasks to compute statistics for (more than %s).', + new PhutilNumber($limit))); + } + + if (!$tasks) { + return $this->renderError( + pht( + 'This milestone has no tasks yet.')); + } + + $statuses = array(); + $points_done = 0; + $points_total = 0; + $no_points = 0; + foreach ($tasks as $task) { + $points = $task->getPoints(); + + if ($points === null) { + $no_points++; + continue; + } + + if (!$points) { + continue; + } + + $status = $task->getStatus(); + if (empty($statuses[$status])) { + $statuses[$status] = 0; + } + $statuses[$status] += $points; + + if (ManiphestTaskStatus::isClosedStatus($status)) { + $points_done += $points; + } + + $points_total += $points; + } + + if ($no_points == count($tasks)) { + return $this->renderError( + pht('No tasks have assigned point values.')); + } + + + if (!$points_total) { + return $this->renderError( + pht('All tasks with assigned point values are worth zero points.')); + } + + $label = pht( + '%s of %s %s', + new PhutilNumber($points_done), + new PhutilNumber($points_total), + ManiphestTaskPoints::getPointsLabel()); + + $bar = id(new PHUISegmentBarView()) + ->setLabel($label); + + $map = ManiphestTaskStatus::getTaskStatusMap(); + $statuses = array_select_keys($statuses, array_keys($map)); + + foreach ($statuses as $status => $points) { + if (!$points) { + continue; + } + + if (!ManiphestTaskStatus::isClosedStatus($status)) { + continue; + } + + $color = ManiphestTaskStatus::getStatusColor($status); + if (!$color) { + $color = 'sky'; + } + + $tooltip = pht( + '%s %s', + new PhutilNumber($points), + ManiphestTaskStatus::getTaskStatusName($status)); + + $bar->newSegment() + ->setWidth($points / $points_total) + ->setColor($color) + ->setTooltip($tooltip); + } + + $bar = phutil_tag( + 'div', + array( + 'class' => 'phui-profile-segment-bar', + ), + $bar); + + $item = $this->newItem() + ->appendChild($bar); + + return array( + $item, + ); + } + + private function renderError($message) { + $message = phutil_tag( + 'div', + array( + 'class' => 'phui-profile-menu-error', + ), + $message); + + $item = $this->newItem() + ->appendChild($message); + + return array( + $item, + ); + } + +} diff --git a/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php b/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php --- a/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php +++ b/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php @@ -18,6 +18,18 @@ return true; } + public function shouldEnableForObject($object) { + $viewer = $this->getViewer(); + + // Workboards are only available if Maniphest is installed. + $class = 'PhabricatorManiphestApplication'; + if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + return false; + } + + return true; + } + public function getDisplayName( PhabricatorProfilePanelConfiguration $config) { $name = $config->getPanelProperty('name'); @@ -42,14 +54,6 @@ protected function newNavigationMenuItems( PhabricatorProfilePanelConfiguration $config) { - $viewer = $this->getViewer(); - - // Workboards are only available if Maniphest is installed. - $class = 'PhabricatorManiphestApplication'; - if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { - return array(); - } - $project = $config->getProfileObject(); $has_workboard = $project->getHasWorkboard(); diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -48,6 +48,7 @@ const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken'; const PANEL_PROFILE = 'project.profile'; + const PANEL_POINTS = 'project.points'; const PANEL_WORKBOARD = 'project.workboard'; const PANEL_MEMBERS = 'project.members'; const PANEL_MANAGE = 'project.manage'; diff --git a/src/applications/search/engine/PhabricatorProfilePanelEngine.php b/src/applications/search/engine/PhabricatorProfilePanelEngine.php --- a/src/applications/search/engine/PhabricatorProfilePanelEngine.php +++ b/src/applications/search/engine/PhabricatorProfilePanelEngine.php @@ -236,6 +236,11 @@ ->withProfilePHIDs(array($object->getPHID())) ->execute(); + foreach ($stored_panels as $stored_panel) { + $impl = $stored_panel->getPanel(); + $impl->setViewer($viewer); + } + // Merge the stored panels into the builtin panels. If a builtin panel has // a stored version, replace the defaults with the stored changes. foreach ($stored_panels as $stored_panel) { @@ -259,12 +264,6 @@ } } - foreach ($panels as $panel) { - $impl = $panel->getPanel(); - - $impl->setViewer($viewer); - } - $panels = msort($panels, 'getSortKey'); // Normalize keys since callers shouldn't rely on this array being @@ -306,6 +305,7 @@ $builtins = $this->getBuiltinProfilePanels($object); $panels = PhabricatorProfilePanel::getAllPanels(); + $viewer = $this->getViewer(); $order = 1; $map = array(); @@ -339,6 +339,9 @@ $panel_key)); } + $panel = clone $panel; + $panel->setViewer($viewer); + $builtin ->setProfilePHID($object->getPHID()) ->attachPanel($panel) diff --git a/src/view/phui/PHUISegmentBarSegmentView.php b/src/view/phui/PHUISegmentBarSegmentView.php --- a/src/view/phui/PHUISegmentBarSegmentView.php +++ b/src/view/phui/PHUISegmentBarSegmentView.php @@ -5,6 +5,7 @@ private $width; private $color; private $position; + private $tooltip; public function setWidth($width) { $this->width = $width; @@ -25,6 +26,11 @@ return $this; } + public function setTooltip($tooltip) { + $this->tooltip = $tooltip; + return $this; + } + protected function canAppendChild() { return false; } @@ -48,9 +54,25 @@ $left = floor(100 * $left) / 100; $left = sprintf('%.2f%%', $left); + $tooltip = $this->tooltip; + if (strlen($tooltip)) { + Javelin::initBehavior('phabricator-tooltips'); + + $sigil = 'has-tooltip'; + $meta = array( + 'tip' => $tooltip, + 'align' => 'E', + ); + } else { + $sigil = null; + $meta = null; + } + return array( 'class' => implode(' ', $classes), 'style' => "left: {$left}; width: {$width};", + 'sigil' => $sigil, + 'meta' => $meta, ); } diff --git a/webroot/rsrc/css/phui/phui-profile-menu.css b/webroot/rsrc/css/phui/phui-profile-menu.css --- a/webroot/rsrc/css/phui/phui-profile-menu.css +++ b/webroot/rsrc/css/phui/phui-profile-menu.css @@ -149,6 +149,18 @@ color: {$menu.profile.text}; } +.phui-profile-menu .phabricator-side-menu .phui-profile-menu-error { + color: {$greytext}; + font-size: {$smallerfontsize}; + padding: 18px 15px; +} + +.phui-profile-menu .phabricator-side-menu .phui-profile-segment-bar { + color: {$menu.profile.text}; + padding: 12px 15px 18px; +} + + .phui-profile-menu .phabricator-side-menu .phui-profile-menu-spacer { box-sizing: border-box; height: {$menu.profile.item.height}; diff --git a/webroot/rsrc/css/phui/phui-segment-bar-view.css b/webroot/rsrc/css/phui/phui-segment-bar-view.css --- a/webroot/rsrc/css/phui/phui-segment-bar-view.css +++ b/webroot/rsrc/css/phui/phui-segment-bar-view.css @@ -20,7 +20,7 @@ position: absolute; top: 0; bottom: 0; - margin-left: -4px; + margin-left: -5px; border-right: 5px solid; border-radius: 0 4px 4px 0; }