1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-19 03:50:54 +01:00

Allow workboards to be filtered with ApplicationSearch

Summary:
Ref T4673.

IMPORTANT: I had to break one thing (see TODO) to get this working. Not sure how you want to deal with that. I might be able to put the element //inside// the workboard, or I could write some JS. But I figured I'd get feedback first.

General areas for improvement:

  - It would be nice to give you some feedback that you have a filter applied.
  - It would be nice to let you save and quickly select common filters.
  - These would probably both be covered by a dropdown menu instead of a button, but that's more JS than I want to sign up for right now.
  - Managing custom filters is also a significant amount of extra UI to build.
  - Also, maybe these filters should be sticky per-board? Or across all boards? Or have a "make this my default view"? I tend to dislike implicit stickiness.

Test Plan:
Before:

{F157543}

Apply Filter:

{F157544}

Filtered:

{F157545}

Reviewers: chad, btrahan

Reviewed By: btrahan

Subscribers: qgil, swisspol, epriestley

Maniphest Tasks: T4673

Differential Revision: https://secure.phabricator.com/D9211
This commit is contained in:
epriestley 2014-05-20 11:42:05 -07:00
parent 8a429c51ac
commit f0147fd8ad
10 changed files with 297 additions and 45 deletions

View file

@ -407,6 +407,7 @@ return array(
'rsrc/js/application/policy/behavior-policy-control.js' => '71b4cbcc',
'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '263aeb8c',
'rsrc/js/application/ponder/behavior-votebox.js' => '327dbe61',
'rsrc/js/application/projects/behavior-boards-filter.js' => '22f113af',
'rsrc/js/application/projects/behavior-project-boards.js' => 'd8e135db',
'rsrc/js/application/projects/behavior-project-create.js' => '065227cc',
'rsrc/js/application/releeph/releeph-preview-branch.js' => '9eb2cedb',
@ -544,6 +545,7 @@ return array(
'javelin-behavior-audio-source' => '59b251eb',
'javelin-behavior-audit-preview' => 'be81801d',
'javelin-behavior-balanced-payment-form' => '3b3e1664',
'javelin-behavior-boards-filter' => '22f113af',
'javelin-behavior-config-reorder-fields' => '938aed89',
'javelin-behavior-conpherence-menu' => '7ee23816',
'javelin-behavior-conpherence-pontificate' => '53f6f2dd',
@ -1004,6 +1006,13 @@ return array(
5 => 'javelin-json',
6 => 'phabricator-prefab',
),
'22f113af' =>
array(
0 => 'javelin-behavior',
1 => 'javelin-dom',
2 => 'javelin-stratcom',
3 => 'phuix-dropdown-menu',
),
'263aeb8c' =>
array(
0 => 'javelin-behavior',

View file

@ -95,6 +95,22 @@ final class ManiphestTaskQuery
return $this;
}
/**
* Add an additional "all projects" constraint to existing filters.
*
* This is used by boards to supplement queries.
*
* @param list<phid> List of project PHIDs to add to any existing constriant.
* @return this
*/
public function addWithAllProjects(array $projects) {
if ($this->projectPHIDs === null) {
$this->projectPHIDs = array();
}
return $this->withAllProjects(array_merge($this->projectPHIDs, $projects));
}
public function withoutProjects(array $projects) {
$this->xprojectPHIDs = $projects;
return $this;

View file

@ -4,6 +4,26 @@ final class ManiphestTaskSearchEngine
extends PhabricatorApplicationSearchEngine {
private $showBatchControls;
private $baseURI;
private $isBoardView;
public function setIsBoardView($is_board_view) {
$this->isBoardView = $is_board_view;
return $this;
}
public function getIsBoardView() {
return $this->isBoardView;
}
public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function getBaseURI() {
return $this->baseURI;
}
public function setShowBatchControls($show_batch_controls) {
$this->showBatchControls = $show_batch_controls;
@ -301,14 +321,20 @@ final class ManiphestTaskSearchEngine
->setDatasource('/typeahead/common/projects/')
->setName('allProjects')
->setLabel(pht('In All Projects'))
->setValue($all_project_handles))
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'withNoProject',
1,
pht('Show only tasks with no projects.'),
$with_no_projects))
->setValue($all_project_handles));
if (!$this->getIsBoardView()) {
$form
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox(
'withNoProject',
1,
pht('Show only tasks with no projects.'),
$with_no_projects));
}
$form
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/projects/')
@ -340,19 +366,25 @@ final class ManiphestTaskSearchEngine
->setLabel(pht('Subscribers'))
->setValue($subscriber_handles))
->appendChild($status_control)
->appendChild($priority_control)
->appendChild(
id(new AphrontFormSelectControl())
->setName('group')
->setLabel(pht('Group By'))
->setValue($saved->getParameter('group'))
->setOptions($this->getGroupOptions()))
->appendChild(
id(new AphrontFormSelectControl())
->setName('order')
->setLabel(pht('Order By'))
->setValue($saved->getParameter('order'))
->setOptions($this->getOrderOptions()))
->appendChild($priority_control);
if (!$this->getIsBoardView()) {
$form
->appendChild(
id(new AphrontFormSelectControl())
->setName('group')
->setLabel(pht('Group By'))
->setValue($saved->getParameter('group'))
->setOptions($this->getGroupOptions()))
->appendChild(
id(new AphrontFormSelectControl())
->setName('order')
->setLabel(pht('Order By'))
->setValue($saved->getParameter('order'))
->setOptions($this->getOrderOptions()));
}
$form
->appendChild(
id(new AphrontFormTextControl())
->setName('fulltext')
@ -382,15 +414,20 @@ final class ManiphestTaskSearchEngine
'modifiedEnd',
pht('Updated Before'));
$form
->appendChild(
id(new AphrontFormTextControl())
->setName('limit')
->setLabel(pht('Page Size'))
->setValue($saved->getParameter('limit', 100)));
if (!$this->getIsBoardView()) {
$form
->appendChild(
id(new AphrontFormTextControl())
->setName('limit')
->setLabel(pht('Page Size'))
->setValue($saved->getParameter('limit', 100)));
}
}
protected function getURI($path) {
if ($this->baseURI) {
return $this->baseURI.$path;
}
return '/maniphest/'.$path;
}

View file

@ -51,7 +51,10 @@ final class PhabricatorApplicationProject extends PhabricatorApplication {
'picture/(?P<id>[1-9]\d*)/' =>
'PhabricatorProjectEditPictureController',
'create/' => 'PhabricatorProjectCreateController',
'board/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectBoardViewController',
'board/(?P<id>[1-9]\d*)/'.
'(?P<filter>filter/)?'.
'(?:query/(?P<queryKey>[^/]+)/)?' =>
'PhabricatorProjectBoardViewController',
'move/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectMoveController',
'board/(?P<projectID>[1-9]\d*)/edit/(?:(?P<id>\d+)/)?'
=> 'PhabricatorProjectBoardEditController',

View file

@ -5,6 +5,8 @@ final class PhabricatorProjectBoardViewController
private $id;
private $handles;
private $queryKey;
private $filter;
public function shouldAllowPublic() {
return true;
@ -12,6 +14,8 @@ final class PhabricatorProjectBoardViewController
public function willProcessRequest(array $data) {
$this->id = $data['id'];
$this->queryKey = idx($data, 'queryKey');
$this->filter = (bool)idx($data, 'filter');
}
public function processRequest() {
@ -50,12 +54,63 @@ final class PhabricatorProjectBoardViewController
ksort($columns);
$tasks = id(new ManiphestTaskQuery())
$board_uri = $this->getApplicationURI('board/'.$project->getID().'/');
$engine = id(new ManiphestTaskSearchEngine())
->setViewer($viewer)
->withAllProjects(array($project->getPHID()))
->withStatuses(ManiphestTaskStatus::getOpenStatusConstants())
->setBaseURI($board_uri)
->setIsBoardView(true);
if ($request->isFormPost()) {
$saved = $engine->buildSavedQueryFromRequest($request);
$engine->saveQuery($saved);
return id(new AphrontRedirectResponse())->setURI(
$engine->getQueryResultsPageURI($saved->getQueryKey()));
}
$query_key = $this->queryKey;
if (!$query_key) {
$query_key = 'open';
}
$custom_query = null;
if ($engine->isBuiltinQuery($query_key)) {
$saved = $engine->buildSavedQueryFromBuiltin($query_key);
} else {
$saved = id(new PhabricatorSavedQueryQuery())
->setViewer($viewer)
->withQueryKeys(array($query_key))
->executeOne();
if (!$saved) {
return new Aphront404Response();
}
$custom_query = $saved;
}
if ($this->filter) {
$filter_form = id(new AphrontFormView())
->setUser($viewer);
$engine->buildSearchForm($filter_form, $saved);
return $this->newDialog()
->setWidth(AphrontDialogView::WIDTH_FULL)
->setTitle(pht('Advanced Filter'))
->appendChild($filter_form->buildLayoutView())
->setSubmitURI($board_uri)
->addSubmitButton(pht('Apply Filter'))
->addCancelButton($board_uri);
}
$task_query = $engine->buildQueryFromSavedQuery($saved);
$tasks = $task_query
->addWithAllProjects(array($project->getPHID()))
->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY)
->setViewer($viewer)
->execute();
$tasks = mpull($tasks, null, 'getPHID');
$task_phids = array_keys($tasks);
@ -166,6 +221,87 @@ final class PhabricatorProjectBoardViewController
->setDisabled(!$can_edit)
->setWorkflow(!$can_edit);
Javelin::initBehavior(
'boards-filter',
array(
));
$filter_icon = id(new PHUIIconView())
->setIconFont('fa-search-plus bluegrey');
$named = array(
'open' => pht('Open Tasks'),
'all' => pht('All Tasks'),
);
if ($viewer->isLoggedIn()) {
$named['assigned'] = pht('Assigned to Me');
}
if ($custom_query) {
$named[$custom_query->getQueryKey()] = pht('Custom Filter');
}
$items = array();
foreach ($named as $key => $name) {
$is_selected = ($key == $query_key);
if ($is_selected) {
$active_filter = $name;
}
$is_custom = false;
if ($custom_query) {
$is_custom = ($key == $custom_query->getQueryKey());
}
$item = id(new PhabricatorActionView())
->setIcon('fa-search')
->setSelected($is_selected)
->setName($name);
if ($is_custom) {
$item->setHref(
$this->getApplicationURI(
'board/'.$this->id.'/filter/query/'.$key.'/'));
$item->setWorkflow(true);
} else {
$item->setHref($engine->getQueryResultsPageURI($key));
}
$items[] = $item;
}
$items[] = id(new PhabricatorActionView())
->setIcon('fa-cog')
->setHref($this->getApplicationURI('board/'.$this->id.'/filter/'))
->setWorkflow(true)
->setName(pht('Advanced Filter...'));
$filter_menu = id(new PhabricatorActionListView())
->setUser($viewer);
foreach ($items as $item) {
$filter_menu->addAction($item);
}
$filter_button = id(new PHUIButtonView())
->setText(pht('Filter: %s', $active_filter))
->setIcon($filter_icon)
->setTag('a')
->setHref('#')
->addSigil('boards-filter-menu')
/*
TODO: @chad, this looks really gnarly right now, at least in Safari.
->setDropdown(true)
*/
->setMetadata(
array(
'items' => hsprintf('%s', $filter_menu),
));
$header_link = phutil_tag(
'a',
array(
@ -179,6 +315,7 @@ final class PhabricatorProjectBoardViewController
->setNoBackground(true)
->setImage($project->getProfileImageURI())
->setImageURL($this->getApplicationURI('view/'.$project->getID().'/'))
->addActionLink($filter_button)
->addActionLink($add_button)
->setPolicyObject($project);

View file

@ -53,6 +53,7 @@ final class ProjectBoardTaskCard {
->setGrippable($can_edit)
->setHref('/T'.$task->getID())
->addSigil('project-card')
->setDisabled($task->isClosed())
->setMetadata(
array(
'objectPHID' => $task->getPHID(),

View file

@ -90,7 +90,7 @@ final class PhabricatorApplicationSearchController
if ($request->isFormPost()) {
$saved_query = $engine->buildSavedQueryFromRequest($request);
$this->saveQuery($saved_query);
$engine->saveQuery($saved_query);
return id(new AphrontRedirectResponse())->setURI(
$engine->getQueryResultsPageURI($saved_query->getQueryKey()).'#R');
}
@ -145,7 +145,7 @@ final class PhabricatorApplicationSearchController
// Save the query to generate a query key, so "Save Custom Query..." and
// other features like Maniphest's "Export..." work correctly.
$this->saveQuery($saved_query);
$engine->saveQuery($saved_query);
}
$nav->selectFilter(
@ -353,18 +353,6 @@ final class PhabricatorApplicationSearchController
));
}
private function saveQuery(PhabricatorSavedQuery $query) {
$query->setEngineClassName(get_class($this->getSearchEngine()));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
$query->save();
} catch (AphrontQueryDuplicateKeyException $ex) {
// Ignore, this is just a repeated search.
}
unset($unguarded);
}
protected function buildApplicationMenu() {
return $this->getDelegatingController()->buildApplicationMenu();
}

View file

@ -35,6 +35,18 @@ abstract class PhabricatorApplicationSearchEngine {
return $this->viewer;
}
public function saveQuery(PhabricatorSavedQuery $query) {
$query->setEngineClassName(get_class($this));
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
try {
$query->save();
} catch (AphrontQueryDuplicateKeyException $ex) {
// Ignore, this is just a repeated search.
}
unset($unguarded);
}
/**
* Create a saved query object from the request.
*

View file

@ -12,6 +12,16 @@ final class PhabricatorActionView extends AphrontView {
private $objectURI;
private $sigils = array();
private $metadata;
private $selected;
public function setSelected($selected) {
$this->selected = $selected;
return $this;
}
public function getSelected() {
return $this->selected;
}
public function setMetadata($metadata) {
$this->metadata = $metadata;
@ -167,6 +177,10 @@ final class PhabricatorActionView extends AphrontView {
$classes[] = 'phabricator-action-view-disabled';
}
if ($this->selected) {
$classes[] = 'phabricator-action-view-selected';
}
return phutil_tag(
'li',
array(

View file

@ -0,0 +1,35 @@
/**
* @provides javelin-behavior-boards-filter
* @requires javelin-behavior
* javelin-dom
* javelin-stratcom
* phuix-dropdown-menu
*/
JX.behavior('boards-filter', function(config) {
JX.Stratcom.listen('click', 'boards-filter-menu', function(e) {
var data = e.getNodeData('boards-filter-menu');
if (data.menu) {
return;
}
e.kill();
var list = JX.$H(data.items).getFragment().firstChild;
var button = e.getNode('boards-filter-menu');
data.menu = new JX.PHUIXDropdownMenu(button);
data.menu.setContent(list);
data.menu.open();
JX.DOM.listen(list, 'click', 'tag:a', function(e) {
if (!e.isNormalClick()) {
return;
}
data.menu.close();
});
});
});