1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-27 01:02:42 +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();
$project = head($request->getArr('set_project'));
$project = nonempty($project, null);
$uri = $uri->alter('project', $project);
$window = $request->getStr('set_window');
$uri = $uri->alter('window', $window);
return id(new AphrontRedirectResponse())->setURI($uri);
}
@ -281,21 +285,7 @@ final class ManiphestReportController extends ManiphestController {
);
}
$form = id(new AphrontFormView())
->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);
$filter = $this->renderReportFilters($tokens, $has_window = false);
$id = celerity_generate_unique_node_id();
$chart = phutil_render_tag(
@ -327,6 +317,45 @@ final class ManiphestReportController extends ManiphestController {
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) {
$out = array();
@ -366,9 +395,20 @@ final class ManiphestReportController extends ManiphestController {
$request = $this->getRequest();
$user = $request->getUser();
$query = id(new ManiphestTaskQuery())
->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();
$recently_closed = $this->loadRecentlyClosedTasks();
@ -439,6 +479,7 @@ final class ManiphestReportController extends ManiphestController {
$handles = msort($handles, 'getName');
$order = $request->getStr('order', 'name');
list($order, $reverse) = AphrontTableView::parseSort($order);
require_celerity_resource('aphront-tooltip-css');
Javelin::initBehavior('phabricator-tooltips', array());
@ -447,6 +488,12 @@ final class ManiphestReportController extends ManiphestController {
$pri_total = array();
foreach (array_merge($handles, array(null)) as $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());
$name = phutil_render_tag(
'a',
@ -478,7 +525,8 @@ final class ManiphestReportController extends ManiphestController {
}
$row[] = number_format($total);
$row[] = $this->renderOldest($taskv);
list($link, $oldest_all) = $this->renderOldest($taskv);
$row[] = $link;
$normal_or_better = array();
foreach ($taskv as $id => $task) {
@ -488,7 +536,8 @@ final class ManiphestReportController extends ManiphestController {
$normal_or_better[$id] = $task;
}
$row[] = $this->renderOldest($normal_or_better);
list($link, $oldest_pri) = $this->renderOldest($normal_or_better);
$row[] = $link;
if ($closed) {
$task_ids = implode(',', mpull($closed, 'getID'));
@ -507,6 +556,15 @@ final class ManiphestReportController extends ManiphestController {
case 'total':
$row['sort'] = $total;
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':
default:
$row['sort'] = $handle ? $handle->getName() : '~';
@ -520,6 +578,9 @@ final class ManiphestReportController extends ManiphestController {
foreach ($rows as $k => $row) {
unset($rows[$k]['sort']);
}
if ($reverse) {
$rows = array_reverse($rows);
}
$cname = array($col_header);
$cclass = array('pri right wide');
@ -553,33 +614,53 @@ final class ManiphestReportController extends ManiphestController {
),
'Oldest (Pri)');
$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';
$table = new AphrontTableView($rows);
$table->setHeaders($cname);
$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->setHeader($header);
$panel->appendChild($table);
$form = id(new AphrontFormView())
->setUser($user)
->appendChild(
id(new AphrontFormToggleButtonsControl())
->setLabel('Order')
->setValue($order)
->setBaseURI($request->getRequestURI(), 'order')
->setButtons(
array(
'name' => 'Name',
'total' => 'Total',
)));
$filter = new AphrontListFilterView();
$filter->appendChild($form);
$tokens = array();
if ($project_handle) {
$tokens = array(
$project_handle->getPHID() => $project_handle->getFullName(),
);
}
$filter = $this->renderReportFilters($tokens, $has_window = true);
return array($filter, $panel);
}
@ -589,7 +670,7 @@ final class ManiphestReportController extends ManiphestController {
* Load all the tasks that have been recently closed.
*/
private function loadRecentlyClosedTasks() {
$recent_window = (60 * 60 * 24 * 7);
list($ignored, $window_epoch) = $this->getWindow();
$table = new ManiphestTask();
$xtable = new ManiphestTransaction();
@ -613,12 +694,55 @@ final class ManiphestReportController extends ManiphestController {
json_encode((int)ManiphestTaskStatus::STATUS_OPEN),
json_encode((string)ManiphestTaskStatus::STATUS_OPEN),
(time() - $recent_window),
(time() - $recent_window));
$window_epoch,
$window_epoch);
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) {
$oldest = null;
foreach ($tasks as $id => $task) {
@ -634,11 +758,10 @@ final class ManiphestReportController extends ManiphestController {
$oldest = $tasks[$oldest];
$age = (time() - $oldest->getDateCreated());
$age = number_format($age / (24 * 60 * 60)).' d';
$age = phutil_escape_html($age);
$raw_age = (time() - $oldest->getDateCreated());
$age = number_format($raw_age / (24 * 60 * 60)).' d';
return javelin_render_tag(
$link = javelin_render_tag(
'a',
array(
'href' => '/T'.$oldest->getID(),
@ -648,7 +771,9 @@ final class ManiphestReportController extends ManiphestController {
),
'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/form/base');
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/layout/listfilter');
phutil_require_module('phabricator', 'view/layout/panel');