mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-27 09: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:
parent
620e936cba
commit
33bce27718
2 changed files with 168 additions and 43 deletions
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
Loading…
Reference in a new issue