1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-23 07:12:41 +01:00

Allow Maniphest reports to be sorted, filtered by project, and have adjustable window sizes

Summary: Minor UI enhancement requests from Quora.

Test Plan: Filtered / sorted / window'd reports.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran, epriestley

Maniphest Tasks: T994

Differential Revision: https://secure.phabricator.com/D1976
This commit is contained in:
epriestley 2012-03-21 16:58:52 -07:00
parent 620e936cba
commit 33bce27718
2 changed files with 168 additions and 43 deletions

View file

@ -35,8 +35,12 @@ final class ManiphestReportController extends ManiphestController {
$uri = $request->getRequestURI(); $uri = $request->getRequestURI();
$project = head($request->getArr('set_project')); $project = head($request->getArr('set_project'));
$project = nonempty($project, null);
$uri = $uri->alter('project', $project); $uri = $uri->alter('project', $project);
$window = $request->getStr('set_window');
$uri = $uri->alter('window', $window);
return id(new AphrontRedirectResponse())->setURI($uri); return id(new AphrontRedirectResponse())->setURI($uri);
} }
@ -281,21 +285,7 @@ final class ManiphestReportController extends ManiphestController {
); );
} }
$form = id(new AphrontFormView()) $filter = $this->renderReportFilters($tokens, $has_window = false);
->setUser($user)
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/projects/')
->setLabel('Project')
->setLimit(1)
->setName('set_project')
->setValue($tokens))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Filter By Project'));
$filter = new AphrontListFilterView();
$filter->appendChild($form);
$id = celerity_generate_unique_node_id(); $id = celerity_generate_unique_node_id();
$chart = phutil_render_tag( $chart = phutil_render_tag(
@ -327,6 +317,45 @@ final class ManiphestReportController extends ManiphestController {
return array($filter, $chart, $panel); return array($filter, $chart, $panel);
} }
private function renderReportFilters(array $tokens, $has_window) {
$request = $this->getRequest();
$user = $request->getUser();
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/searchproject/')
->setLabel('Project')
->setLimit(1)
->setName('set_project')
->setValue($tokens));
if ($has_window) {
list($window_str, $ignored, $window_error) = $this->getWindow();
$form
->appendChild(
id(new AphrontFormTextControl())
->setLabel('"Recently" Means')
->setName('set_window')
->setCaption(
'Configure the cutoff for the "Recently Closed" column.')
->setValue($window_str)
->setError($window_error));
}
$form
->appendChild(
id(new AphrontFormSubmitControl())
->setValue('Filter By Project'));
$filter = new AphrontListFilterView();
$filter->appendChild($form);
return $filter;
}
private function buildSeries(array $data) { private function buildSeries(array $data) {
$out = array(); $out = array();
@ -366,9 +395,20 @@ final class ManiphestReportController extends ManiphestController {
$request = $this->getRequest(); $request = $this->getRequest();
$user = $request->getUser(); $user = $request->getUser();
$query = id(new ManiphestTaskQuery()) $query = id(new ManiphestTaskQuery())
->withStatus(ManiphestTaskQuery::STATUS_OPEN); ->withStatus(ManiphestTaskQuery::STATUS_OPEN);
$project_phid = $request->getStr('project');
$project_handle = null;
if ($project_phid) {
$phids = array($project_phid);
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
$project_handle = $handles[$project_phid];
$query->withProjects($phids);
}
$tasks = $query->execute(); $tasks = $query->execute();
$recently_closed = $this->loadRecentlyClosedTasks(); $recently_closed = $this->loadRecentlyClosedTasks();
@ -439,6 +479,7 @@ final class ManiphestReportController extends ManiphestController {
$handles = msort($handles, 'getName'); $handles = msort($handles, 'getName');
$order = $request->getStr('order', 'name'); $order = $request->getStr('order', 'name');
list($order, $reverse) = AphrontTableView::parseSort($order);
require_celerity_resource('aphront-tooltip-css'); require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips', array()); Javelin::initBehavior('phabricator-tooltips', array());
@ -447,6 +488,12 @@ final class ManiphestReportController extends ManiphestController {
$pri_total = array(); $pri_total = array();
foreach (array_merge($handles, array(null)) as $handle) { foreach (array_merge($handles, array(null)) as $handle) {
if ($handle) { if ($handle) {
if (($project_handle) &&
($project_handle->getPHID() == $handle->getPHID())) {
// If filtering by, e.g., "bugs", don't show a "bugs" group.
continue;
}
$tasks = idx($result, $handle->getPHID(), array()); $tasks = idx($result, $handle->getPHID(), array());
$name = phutil_render_tag( $name = phutil_render_tag(
'a', 'a',
@ -478,7 +525,8 @@ final class ManiphestReportController extends ManiphestController {
} }
$row[] = number_format($total); $row[] = number_format($total);
$row[] = $this->renderOldest($taskv); list($link, $oldest_all) = $this->renderOldest($taskv);
$row[] = $link;
$normal_or_better = array(); $normal_or_better = array();
foreach ($taskv as $id => $task) { foreach ($taskv as $id => $task) {
@ -488,7 +536,8 @@ final class ManiphestReportController extends ManiphestController {
$normal_or_better[$id] = $task; $normal_or_better[$id] = $task;
} }
$row[] = $this->renderOldest($normal_or_better); list($link, $oldest_pri) = $this->renderOldest($normal_or_better);
$row[] = $link;
if ($closed) { if ($closed) {
$task_ids = implode(',', mpull($closed, 'getID')); $task_ids = implode(',', mpull($closed, 'getID'));
@ -507,6 +556,15 @@ final class ManiphestReportController extends ManiphestController {
case 'total': case 'total':
$row['sort'] = $total; $row['sort'] = $total;
break; break;
case 'oldest-all':
$row['sort'] = $oldest_all;
break;
case 'oldest-pri':
$row['sort'] = $oldest_pri;
break;
case 'closed':
$row['sort'] = count($closed);
break;
case 'name': case 'name':
default: default:
$row['sort'] = $handle ? $handle->getName() : '~'; $row['sort'] = $handle ? $handle->getName() : '~';
@ -520,6 +578,9 @@ final class ManiphestReportController extends ManiphestController {
foreach ($rows as $k => $row) { foreach ($rows as $k => $row) {
unset($rows[$k]['sort']); unset($rows[$k]['sort']);
} }
if ($reverse) {
$rows = array_reverse($rows);
}
$cname = array($col_header); $cname = array($col_header);
$cclass = array('pri right wide'); $cclass = array('pri right wide');
@ -553,33 +614,53 @@ final class ManiphestReportController extends ManiphestController {
), ),
'Oldest (Pri)'); 'Oldest (Pri)');
$cclass[] = 'n'; $cclass[] = 'n';
$cname[] = 'Closed Last 7d';
list($ignored, $window_epoch) = $this->getWindow();
$cname[] = javelin_render_tag(
'span',
array(
'sigil' => 'has-tooltip',
'meta' => array(
'tip' => 'Closed after '.phabricator_datetime($window_epoch, $user),
'size' => 260
),
),
'Recently Closed');
$cclass[] = 'n'; $cclass[] = 'n';
$table = new AphrontTableView($rows); $table = new AphrontTableView($rows);
$table->setHeaders($cname); $table->setHeaders($cname);
$table->setColumnClasses($cclass); $table->setColumnClasses($cclass);
$table->makeSortable(
$request->getRequestURI(),
'order',
$order,
$reverse,
array(
'name',
null,
null,
null,
null,
null,
null,
'total',
'oldest-all',
'oldest-pri',
'closed',
));
$panel = new AphrontPanelView(); $panel = new AphrontPanelView();
$panel->setHeader($header); $panel->setHeader($header);
$panel->appendChild($table); $panel->appendChild($table);
$form = id(new AphrontFormView()) $tokens = array();
->setUser($user) if ($project_handle) {
->appendChild( $tokens = array(
id(new AphrontFormToggleButtonsControl()) $project_handle->getPHID() => $project_handle->getFullName(),
->setLabel('Order') );
->setValue($order) }
->setBaseURI($request->getRequestURI(), 'order') $filter = $this->renderReportFilters($tokens, $has_window = true);
->setButtons(
array(
'name' => 'Name',
'total' => 'Total',
)));
$filter = new AphrontListFilterView();
$filter->appendChild($form);
return array($filter, $panel); return array($filter, $panel);
} }
@ -589,7 +670,7 @@ final class ManiphestReportController extends ManiphestController {
* Load all the tasks that have been recently closed. * Load all the tasks that have been recently closed.
*/ */
private function loadRecentlyClosedTasks() { private function loadRecentlyClosedTasks() {
$recent_window = (60 * 60 * 24 * 7); list($ignored, $window_epoch) = $this->getWindow();
$table = new ManiphestTask(); $table = new ManiphestTask();
$xtable = new ManiphestTransaction(); $xtable = new ManiphestTransaction();
@ -613,12 +694,55 @@ final class ManiphestReportController extends ManiphestController {
json_encode((int)ManiphestTaskStatus::STATUS_OPEN), json_encode((int)ManiphestTaskStatus::STATUS_OPEN),
json_encode((string)ManiphestTaskStatus::STATUS_OPEN), json_encode((string)ManiphestTaskStatus::STATUS_OPEN),
(time() - $recent_window), $window_epoch,
(time() - $recent_window)); $window_epoch);
return id(new ManiphestTask())->loadAllFromArray($tasks); return id(new ManiphestTask())->loadAllFromArray($tasks);
} }
/**
* Parse the "Recently Means" filter into:
*
* - A string representation, like "12 AM 7 days ago" (default);
* - a locale-aware epoch representation; and
* - a possible error.
*/
private function getWindow() {
$request = $this->getRequest();
$user = $request->getUser();
$window_str = $this->getRequest()->getStr('window', '12 AM 7 days ago');
$error = null;
$window_epoch = null;
// Do locale-aware parsing so that the user's timezone is assumed for
// time windows like "3 PM", rather than assuming the server timezone.
$timezone = new DateTimeZone($user->getTimezoneIdentifier());
try {
$date = new DateTime($window_str, $timezone);
$window_epoch = $date->format('U');
} catch (Exception $e) {
$error = 'Invalid';
$window_epoch = time() - (60 * 60 * 24 * 7);
}
// If the time ends up in the future, convert it to the corresponding time
// and equal distance in the past. This is so users can type "6 days" (which
// means "6 days from now") and get the behavior of "6 days ago", rather
// than no results (because the window epoch is in the future). This might
// be a little confusing because it casues "tomorrow" to mean "yesterday"
// and "2022" (or whatever) to mean "ten years ago", but these inputs are
// nonsense anyway.
if ($window_epoch > time()) {
$window_epoch = time() - ($window_epoch - time());
}
return array($window_str, $window_epoch, $error);
}
private function renderOldest(array $tasks) { private function renderOldest(array $tasks) {
$oldest = null; $oldest = null;
foreach ($tasks as $id => $task) { foreach ($tasks as $id => $task) {
@ -634,11 +758,10 @@ final class ManiphestReportController extends ManiphestController {
$oldest = $tasks[$oldest]; $oldest = $tasks[$oldest];
$age = (time() - $oldest->getDateCreated()); $raw_age = (time() - $oldest->getDateCreated());
$age = number_format($age / (24 * 60 * 60)).' d'; $age = number_format($raw_age / (24 * 60 * 60)).' d';
$age = phutil_escape_html($age);
return javelin_render_tag( $link = javelin_render_tag(
'a', 'a',
array( array(
'href' => '/T'.$oldest->getID(), 'href' => '/T'.$oldest->getID(),
@ -648,7 +771,9 @@ final class ManiphestReportController extends ManiphestController {
), ),
'target' => '_blank', 'target' => '_blank',
), ),
$age); phutil_escape_html($age));
return array($link, $raw_age);
} }
} }

View file

@ -25,7 +25,7 @@ phutil_require_module('phabricator', 'storage/queryfx');
phutil_require_module('phabricator', 'view/control/table'); phutil_require_module('phabricator', 'view/control/table');
phutil_require_module('phabricator', 'view/form/base'); phutil_require_module('phabricator', 'view/form/base');
phutil_require_module('phabricator', 'view/form/control/submit'); phutil_require_module('phabricator', 'view/form/control/submit');
phutil_require_module('phabricator', 'view/form/control/togglebuttons'); phutil_require_module('phabricator', 'view/form/control/text');
phutil_require_module('phabricator', 'view/form/control/tokenizer'); phutil_require_module('phabricator', 'view/form/control/tokenizer');
phutil_require_module('phabricator', 'view/layout/listfilter'); phutil_require_module('phabricator', 'view/layout/listfilter');
phutil_require_module('phabricator', 'view/layout/panel'); phutil_require_module('phabricator', 'view/layout/panel');