mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-27 01:02:42 +01:00
Add a basic progress bar for milestones
Summary: Ref T4427. This kind of works. Test Plan: {F1100578} Reviewers: chad Reviewed By: chad Maniphest Tasks: T4427 Differential Revision: https://secure.phabricator.com/D15221
This commit is contained in:
parent
f84130f9cd
commit
0782652a80
12 changed files with 293 additions and 31 deletions
|
@ -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 @@ return array(
|
|||
'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 @@ return array(
|
|||
'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',
|
||||
|
|
|
@ -2933,6 +2933,7 @@ phutil_register_library_map(array(
|
|||
'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 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectOrUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource',
|
||||
'PhabricatorProjectPHIDResolver' => 'PhabricatorPHIDResolver',
|
||||
'PhabricatorProjectPanelController' => 'PhabricatorProjectController',
|
||||
'PhabricatorProjectPointsProfilePanel' => 'PhabricatorProfilePanel',
|
||||
'PhabricatorProjectProfileController' => 'PhabricatorProjectController',
|
||||
'PhabricatorProjectProfilePanelEngine' => 'PhabricatorProfilePanelEngine',
|
||||
'PhabricatorProjectProjectHasMemberEdgeType' => 'PhabricatorEdgeType',
|
||||
|
|
|
@ -307,28 +307,45 @@ final class ManiphestEditEngine
|
|||
// 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
|
||||
|
|
|
@ -86,9 +86,14 @@ final class PhabricatorBoardLayoutEngine extends Phobject {
|
|||
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');
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,10 @@ final class PhabricatorProjectProfilePanelEngine
|
|||
->setBuiltinKey(PhabricatorProject::PANEL_PROFILE)
|
||||
->setPanelKey(PhabricatorProjectDetailsProfilePanel::PANELKEY);
|
||||
|
||||
$panels[] = $this->newPanel()
|
||||
->setBuiltinKey(PhabricatorProject::PANEL_POINTS)
|
||||
->setPanelKey(PhabricatorProjectPointsProfilePanel::PANELKEY);
|
||||
|
||||
$panels[] = $this->newPanel()
|
||||
->setBuiltinKey(PhabricatorProject::PANEL_WORKBOARD)
|
||||
->setPanelKey(PhabricatorProjectWorkboardProfilePanel::PANELKEY);
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectPointsProfilePanel
|
||||
extends PhabricatorProfilePanel {
|
||||
|
||||
const PANELKEY = 'project.points';
|
||||
|
||||
public function getPanelTypeName() {
|
||||
return pht('Project Points');
|
||||
}
|
||||
|
||||
private function getDefaultName() {
|
||||
return pht('Points Bar');
|
||||
}
|
||||
|
||||
public function shouldEnableForObject($object) {
|
||||
$viewer = $this->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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -18,6 +18,18 @@ final class PhabricatorProjectWorkboardProfilePanel
|
|||
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 @@ final class PhabricatorProjectWorkboardProfilePanel
|
|||
|
||||
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();
|
||||
|
|
|
@ -48,6 +48,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||
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';
|
||||
|
|
|
@ -236,6 +236,11 @@ abstract class PhabricatorProfilePanelEngine extends Phobject {
|
|||
->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 @@ abstract class PhabricatorProfilePanelEngine extends Phobject {
|
|||
}
|
||||
}
|
||||
|
||||
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 @@ abstract class PhabricatorProfilePanelEngine extends Phobject {
|
|||
$builtins = $this->getBuiltinProfilePanels($object);
|
||||
|
||||
$panels = PhabricatorProfilePanel::getAllPanels();
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$order = 1;
|
||||
$map = array();
|
||||
|
@ -339,6 +339,9 @@ abstract class PhabricatorProfilePanelEngine extends Phobject {
|
|||
$panel_key));
|
||||
}
|
||||
|
||||
$panel = clone $panel;
|
||||
$panel->setViewer($viewer);
|
||||
|
||||
$builtin
|
||||
->setProfilePHID($object->getPHID())
|
||||
->attachPanel($panel)
|
||||
|
|
|
@ -5,6 +5,7 @@ final class PHUISegmentBarSegmentView extends AphrontTagView {
|
|||
private $width;
|
||||
private $color;
|
||||
private $position;
|
||||
private $tooltip;
|
||||
|
||||
public function setWidth($width) {
|
||||
$this->width = $width;
|
||||
|
@ -25,6 +26,11 @@ final class PHUISegmentBarSegmentView extends AphrontTagView {
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function setTooltip($tooltip) {
|
||||
$this->tooltip = $tooltip;
|
||||
return $this;
|
||||
}
|
||||
|
||||
protected function canAppendChild() {
|
||||
return false;
|
||||
}
|
||||
|
@ -48,9 +54,25 @@ final class PHUISegmentBarSegmentView extends AphrontTagView {
|
|||
$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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue