mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-20 20:40:56 +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:
parent
8a429c51ac
commit
f0147fd8ad
10 changed files with 297 additions and 45 deletions
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
Loading…
Reference in a new issue