@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+297 -45
+9
resources/celerity/map.php
··· 407 407 'rsrc/js/application/policy/behavior-policy-control.js' => '71b4cbcc', 408 408 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '263aeb8c', 409 409 'rsrc/js/application/ponder/behavior-votebox.js' => '327dbe61', 410 + 'rsrc/js/application/projects/behavior-boards-filter.js' => '22f113af', 410 411 'rsrc/js/application/projects/behavior-project-boards.js' => 'd8e135db', 411 412 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 412 413 'rsrc/js/application/releeph/releeph-preview-branch.js' => '9eb2cedb', ··· 544 545 'javelin-behavior-audio-source' => '59b251eb', 545 546 'javelin-behavior-audit-preview' => 'be81801d', 546 547 'javelin-behavior-balanced-payment-form' => '3b3e1664', 548 + 'javelin-behavior-boards-filter' => '22f113af', 547 549 'javelin-behavior-config-reorder-fields' => '938aed89', 548 550 'javelin-behavior-conpherence-menu' => '7ee23816', 549 551 'javelin-behavior-conpherence-pontificate' => '53f6f2dd', ··· 1003 1005 4 => 'javelin-stratcom', 1004 1006 5 => 'javelin-json', 1005 1007 6 => 'phabricator-prefab', 1008 + ), 1009 + '22f113af' => 1010 + array( 1011 + 0 => 'javelin-behavior', 1012 + 1 => 'javelin-dom', 1013 + 2 => 'javelin-stratcom', 1014 + 3 => 'phuix-dropdown-menu', 1006 1015 ), 1007 1016 '263aeb8c' => 1008 1017 array(
+16
src/applications/maniphest/query/ManiphestTaskQuery.php
··· 95 95 return $this; 96 96 } 97 97 98 + /** 99 + * Add an additional "all projects" constraint to existing filters. 100 + * 101 + * This is used by boards to supplement queries. 102 + * 103 + * @param list<phid> List of project PHIDs to add to any existing constriant. 104 + * @return this 105 + */ 106 + public function addWithAllProjects(array $projects) { 107 + if ($this->projectPHIDs === null) { 108 + $this->projectPHIDs = array(); 109 + } 110 + 111 + return $this->withAllProjects(array_merge($this->projectPHIDs, $projects)); 112 + } 113 + 98 114 public function withoutProjects(array $projects) { 99 115 $this->xprojectPHIDs = $projects; 100 116 return $this;
+64 -27
src/applications/maniphest/query/ManiphestTaskSearchEngine.php
··· 4 4 extends PhabricatorApplicationSearchEngine { 5 5 6 6 private $showBatchControls; 7 + private $baseURI; 8 + private $isBoardView; 9 + 10 + public function setIsBoardView($is_board_view) { 11 + $this->isBoardView = $is_board_view; 12 + return $this; 13 + } 14 + 15 + public function getIsBoardView() { 16 + return $this->isBoardView; 17 + } 18 + 19 + public function setBaseURI($base_uri) { 20 + $this->baseURI = $base_uri; 21 + return $this; 22 + } 23 + 24 + public function getBaseURI() { 25 + return $this->baseURI; 26 + } 7 27 8 28 public function setShowBatchControls($show_batch_controls) { 9 29 $this->showBatchControls = $show_batch_controls; ··· 301 321 ->setDatasource('/typeahead/common/projects/') 302 322 ->setName('allProjects') 303 323 ->setLabel(pht('In All Projects')) 304 - ->setValue($all_project_handles)) 305 - ->appendChild( 306 - id(new AphrontFormCheckboxControl()) 307 - ->addCheckbox( 308 - 'withNoProject', 309 - 1, 310 - pht('Show only tasks with no projects.'), 311 - $with_no_projects)) 324 + ->setValue($all_project_handles)); 325 + 326 + if (!$this->getIsBoardView()) { 327 + $form 328 + ->appendChild( 329 + id(new AphrontFormCheckboxControl()) 330 + ->addCheckbox( 331 + 'withNoProject', 332 + 1, 333 + pht('Show only tasks with no projects.'), 334 + $with_no_projects)); 335 + } 336 + 337 + $form 312 338 ->appendChild( 313 339 id(new AphrontFormTokenizerControl()) 314 340 ->setDatasource('/typeahead/common/projects/') ··· 340 366 ->setLabel(pht('Subscribers')) 341 367 ->setValue($subscriber_handles)) 342 368 ->appendChild($status_control) 343 - ->appendChild($priority_control) 344 - ->appendChild( 345 - id(new AphrontFormSelectControl()) 346 - ->setName('group') 347 - ->setLabel(pht('Group By')) 348 - ->setValue($saved->getParameter('group')) 349 - ->setOptions($this->getGroupOptions())) 350 - ->appendChild( 351 - id(new AphrontFormSelectControl()) 352 - ->setName('order') 353 - ->setLabel(pht('Order By')) 354 - ->setValue($saved->getParameter('order')) 355 - ->setOptions($this->getOrderOptions())) 369 + ->appendChild($priority_control); 370 + 371 + if (!$this->getIsBoardView()) { 372 + $form 373 + ->appendChild( 374 + id(new AphrontFormSelectControl()) 375 + ->setName('group') 376 + ->setLabel(pht('Group By')) 377 + ->setValue($saved->getParameter('group')) 378 + ->setOptions($this->getGroupOptions())) 379 + ->appendChild( 380 + id(new AphrontFormSelectControl()) 381 + ->setName('order') 382 + ->setLabel(pht('Order By')) 383 + ->setValue($saved->getParameter('order')) 384 + ->setOptions($this->getOrderOptions())); 385 + } 386 + 387 + $form 356 388 ->appendChild( 357 389 id(new AphrontFormTextControl()) 358 390 ->setName('fulltext') ··· 382 414 'modifiedEnd', 383 415 pht('Updated Before')); 384 416 385 - $form 386 - ->appendChild( 387 - id(new AphrontFormTextControl()) 388 - ->setName('limit') 389 - ->setLabel(pht('Page Size')) 390 - ->setValue($saved->getParameter('limit', 100))); 417 + if (!$this->getIsBoardView()) { 418 + $form 419 + ->appendChild( 420 + id(new AphrontFormTextControl()) 421 + ->setName('limit') 422 + ->setLabel(pht('Page Size')) 423 + ->setValue($saved->getParameter('limit', 100))); 424 + } 391 425 } 392 426 393 427 protected function getURI($path) { 428 + if ($this->baseURI) { 429 + return $this->baseURI.$path; 430 + } 394 431 return '/maniphest/'.$path; 395 432 } 396 433
+4 -1
src/applications/project/application/PhabricatorApplicationProject.php
··· 51 51 'picture/(?P<id>[1-9]\d*)/' => 52 52 'PhabricatorProjectEditPictureController', 53 53 'create/' => 'PhabricatorProjectCreateController', 54 - 'board/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectBoardViewController', 54 + 'board/(?P<id>[1-9]\d*)/'. 55 + '(?P<filter>filter/)?'. 56 + '(?:query/(?P<queryKey>[^/]+)/)?' => 57 + 'PhabricatorProjectBoardViewController', 55 58 'move/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectMoveController', 56 59 'board/(?P<projectID>[1-9]\d*)/edit/(?:(?P<id>\d+)/)?' 57 60 => 'PhabricatorProjectBoardEditController',
+140 -3
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 5 5 6 6 private $id; 7 7 private $handles; 8 + private $queryKey; 9 + private $filter; 8 10 9 11 public function shouldAllowPublic() { 10 12 return true; ··· 12 14 13 15 public function willProcessRequest(array $data) { 14 16 $this->id = $data['id']; 17 + $this->queryKey = idx($data, 'queryKey'); 18 + $this->filter = (bool)idx($data, 'filter'); 15 19 } 16 20 17 21 public function processRequest() { ··· 50 54 51 55 ksort($columns); 52 56 53 - $tasks = id(new ManiphestTaskQuery()) 57 + $board_uri = $this->getApplicationURI('board/'.$project->getID().'/'); 58 + 59 + $engine = id(new ManiphestTaskSearchEngine()) 54 60 ->setViewer($viewer) 55 - ->withAllProjects(array($project->getPHID())) 56 - ->withStatuses(ManiphestTaskStatus::getOpenStatusConstants()) 61 + ->setBaseURI($board_uri) 62 + ->setIsBoardView(true); 63 + 64 + if ($request->isFormPost()) { 65 + $saved = $engine->buildSavedQueryFromRequest($request); 66 + $engine->saveQuery($saved); 67 + return id(new AphrontRedirectResponse())->setURI( 68 + $engine->getQueryResultsPageURI($saved->getQueryKey())); 69 + } 70 + 71 + $query_key = $this->queryKey; 72 + if (!$query_key) { 73 + $query_key = 'open'; 74 + } 75 + 76 + $custom_query = null; 77 + if ($engine->isBuiltinQuery($query_key)) { 78 + $saved = $engine->buildSavedQueryFromBuiltin($query_key); 79 + } else { 80 + $saved = id(new PhabricatorSavedQueryQuery()) 81 + ->setViewer($viewer) 82 + ->withQueryKeys(array($query_key)) 83 + ->executeOne(); 84 + 85 + if (!$saved) { 86 + return new Aphront404Response(); 87 + } 88 + 89 + $custom_query = $saved; 90 + } 91 + 92 + if ($this->filter) { 93 + $filter_form = id(new AphrontFormView()) 94 + ->setUser($viewer); 95 + $engine->buildSearchForm($filter_form, $saved); 96 + 97 + return $this->newDialog() 98 + ->setWidth(AphrontDialogView::WIDTH_FULL) 99 + ->setTitle(pht('Advanced Filter')) 100 + ->appendChild($filter_form->buildLayoutView()) 101 + ->setSubmitURI($board_uri) 102 + ->addSubmitButton(pht('Apply Filter')) 103 + ->addCancelButton($board_uri); 104 + } 105 + 106 + $task_query = $engine->buildQueryFromSavedQuery($saved); 107 + 108 + $tasks = $task_query 109 + ->addWithAllProjects(array($project->getPHID())) 57 110 ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) 111 + ->setViewer($viewer) 58 112 ->execute(); 113 + 59 114 $tasks = mpull($tasks, null, 'getPHID'); 60 115 $task_phids = array_keys($tasks); 61 116 ··· 166 221 ->setDisabled(!$can_edit) 167 222 ->setWorkflow(!$can_edit); 168 223 224 + Javelin::initBehavior( 225 + 'boards-filter', 226 + array( 227 + )); 228 + 229 + $filter_icon = id(new PHUIIconView()) 230 + ->setIconFont('fa-search-plus bluegrey'); 231 + 232 + $named = array( 233 + 'open' => pht('Open Tasks'), 234 + 'all' => pht('All Tasks'), 235 + ); 236 + 237 + if ($viewer->isLoggedIn()) { 238 + $named['assigned'] = pht('Assigned to Me'); 239 + } 240 + 241 + if ($custom_query) { 242 + $named[$custom_query->getQueryKey()] = pht('Custom Filter'); 243 + } 244 + 245 + $items = array(); 246 + foreach ($named as $key => $name) { 247 + $is_selected = ($key == $query_key); 248 + if ($is_selected) { 249 + $active_filter = $name; 250 + } 251 + 252 + $is_custom = false; 253 + if ($custom_query) { 254 + $is_custom = ($key == $custom_query->getQueryKey()); 255 + } 256 + 257 + $item = id(new PhabricatorActionView()) 258 + ->setIcon('fa-search') 259 + ->setSelected($is_selected) 260 + ->setName($name); 261 + 262 + if ($is_custom) { 263 + $item->setHref( 264 + $this->getApplicationURI( 265 + 'board/'.$this->id.'/filter/query/'.$key.'/')); 266 + $item->setWorkflow(true); 267 + } else { 268 + $item->setHref($engine->getQueryResultsPageURI($key)); 269 + } 270 + 271 + $items[] = $item; 272 + } 273 + 274 + $items[] = id(new PhabricatorActionView()) 275 + ->setIcon('fa-cog') 276 + ->setHref($this->getApplicationURI('board/'.$this->id.'/filter/')) 277 + ->setWorkflow(true) 278 + ->setName(pht('Advanced Filter...')); 279 + 280 + 281 + 282 + $filter_menu = id(new PhabricatorActionListView()) 283 + ->setUser($viewer); 284 + foreach ($items as $item) { 285 + $filter_menu->addAction($item); 286 + } 287 + 288 + $filter_button = id(new PHUIButtonView()) 289 + ->setText(pht('Filter: %s', $active_filter)) 290 + ->setIcon($filter_icon) 291 + ->setTag('a') 292 + ->setHref('#') 293 + ->addSigil('boards-filter-menu') 294 + 295 + /* 296 + TODO: @chad, this looks really gnarly right now, at least in Safari. 297 + ->setDropdown(true) 298 + */ 299 + 300 + ->setMetadata( 301 + array( 302 + 'items' => hsprintf('%s', $filter_menu), 303 + )); 304 + 169 305 $header_link = phutil_tag( 170 306 'a', 171 307 array( ··· 179 315 ->setNoBackground(true) 180 316 ->setImage($project->getProfileImageURI()) 181 317 ->setImageURL($this->getApplicationURI('view/'.$project->getID().'/')) 318 + ->addActionLink($filter_button) 182 319 ->addActionLink($add_button) 183 320 ->setPolicyObject($project); 184 321
+1
src/applications/project/view/ProjectBoardTaskCard.php
··· 53 53 ->setGrippable($can_edit) 54 54 ->setHref('/T'.$task->getID()) 55 55 ->addSigil('project-card') 56 + ->setDisabled($task->isClosed()) 56 57 ->setMetadata( 57 58 array( 58 59 'objectPHID' => $task->getPHID(),
+2 -14
src/applications/search/controller/PhabricatorApplicationSearchController.php
··· 90 90 91 91 if ($request->isFormPost()) { 92 92 $saved_query = $engine->buildSavedQueryFromRequest($request); 93 - $this->saveQuery($saved_query); 93 + $engine->saveQuery($saved_query); 94 94 return id(new AphrontRedirectResponse())->setURI( 95 95 $engine->getQueryResultsPageURI($saved_query->getQueryKey()).'#R'); 96 96 } ··· 145 145 146 146 // Save the query to generate a query key, so "Save Custom Query..." and 147 147 // other features like Maniphest's "Export..." work correctly. 148 - $this->saveQuery($saved_query); 148 + $engine->saveQuery($saved_query); 149 149 } 150 150 151 151 $nav->selectFilter( ··· 351 351 'title' => pht("Saved Queries"), 352 352 'device' => true, 353 353 )); 354 - } 355 - 356 - private function saveQuery(PhabricatorSavedQuery $query) { 357 - $query->setEngineClassName(get_class($this->getSearchEngine())); 358 - 359 - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 360 - try { 361 - $query->save(); 362 - } catch (AphrontQueryDuplicateKeyException $ex) { 363 - // Ignore, this is just a repeated search. 364 - } 365 - unset($unguarded); 366 354 } 367 355 368 356 protected function buildApplicationMenu() {
+12
src/applications/search/engine/PhabricatorApplicationSearchEngine.php
··· 35 35 return $this->viewer; 36 36 } 37 37 38 + public function saveQuery(PhabricatorSavedQuery $query) { 39 + $query->setEngineClassName(get_class($this)); 40 + 41 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 42 + try { 43 + $query->save(); 44 + } catch (AphrontQueryDuplicateKeyException $ex) { 45 + // Ignore, this is just a repeated search. 46 + } 47 + unset($unguarded); 48 + } 49 + 38 50 /** 39 51 * Create a saved query object from the request. 40 52 *
+14
src/view/layout/PhabricatorActionView.php
··· 12 12 private $objectURI; 13 13 private $sigils = array(); 14 14 private $metadata; 15 + private $selected; 16 + 17 + public function setSelected($selected) { 18 + $this->selected = $selected; 19 + return $this; 20 + } 21 + 22 + public function getSelected() { 23 + return $this->selected; 24 + } 15 25 16 26 public function setMetadata($metadata) { 17 27 $this->metadata = $metadata; ··· 165 175 $classes[] = 'phabricator-action-view'; 166 176 if ($this->disabled) { 167 177 $classes[] = 'phabricator-action-view-disabled'; 178 + } 179 + 180 + if ($this->selected) { 181 + $classes[] = 'phabricator-action-view-selected'; 168 182 } 169 183 170 184 return phutil_tag(
+35
webroot/rsrc/js/application/projects/behavior-boards-filter.js
··· 1 + /** 2 + * @provides javelin-behavior-boards-filter 3 + * @requires javelin-behavior 4 + * javelin-dom 5 + * javelin-stratcom 6 + * phuix-dropdown-menu 7 + */ 8 + 9 + JX.behavior('boards-filter', function(config) { 10 + 11 + JX.Stratcom.listen('click', 'boards-filter-menu', function(e) { 12 + var data = e.getNodeData('boards-filter-menu'); 13 + if (data.menu) { 14 + return; 15 + } 16 + 17 + e.kill(); 18 + 19 + var list = JX.$H(data.items).getFragment().firstChild; 20 + 21 + var button = e.getNode('boards-filter-menu'); 22 + data.menu = new JX.PHUIXDropdownMenu(button); 23 + data.menu.setContent(list); 24 + data.menu.open(); 25 + 26 + JX.DOM.listen(list, 'click', 'tag:a', function(e) { 27 + if (!e.isNormalClick()) { 28 + return; 29 + } 30 + data.menu.close(); 31 + }); 32 + }); 33 + 34 + 35 + });