@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.

Separate workboard view state (ordering, filtering, hidden columns) from the View controller

Summary:
Depends on D20627. Ref T4900. If a user orders a board by "Sort by Title", then toggles the visibility of hidden columns, we want to keep the board sorted by title. To accomplish this, we pass the board state around to all the workflows here.

Pull the "bag of state properties" code out of the View controller. This class basically:

- reads state from a request (order, hidden, filter);
- manages defaults;
- provides the application with the current settings; and
- generates URIs with "?order=X&hidden=Y&filter=Z" to preserve state.

This is still a little questionable/transitional since some of the controllers need more cleanup.

Test Plan:
Toggled state, order, filters, clicked around various workflows and saw the filters preserved.

A lot of these workflows are pretty serious edge cases. For example, here's a feature this implements:

- Changed workboard order to "Title".
- Selected "Bulk Edit Tasks..." in an empty column and command-clicked it to open the link in a new window.
- Hovered over "Cancel".
- Saw the link properly generate with "?order=title", preserving the order.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T4900

Differential Revision: https://secure.phabricator.com/D20628

+195 -106
+2
src/__phutil_library_map__.php
··· 4949 4949 'PhabricatorWeekStartDaySetting' => 'applications/settings/setting/PhabricatorWeekStartDaySetting.php', 4950 4950 'PhabricatorWildConfigType' => 'applications/config/type/PhabricatorWildConfigType.php', 4951 4951 'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php', 4952 + 'PhabricatorWorkboardViewState' => 'applications/project/state/PhabricatorWorkboardViewState.php', 4952 4953 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', 4953 4954 'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php', 4954 4955 'PhabricatorWorkerActiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerActiveTaskQuery.php', ··· 11356 11357 'PhabricatorWeekStartDaySetting' => 'PhabricatorSelectSetting', 11357 11358 'PhabricatorWildConfigType' => 'PhabricatorJSONConfigType', 11358 11359 'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider', 11360 + 'PhabricatorWorkboardViewState' => 'Phobject', 11359 11361 'PhabricatorWorker' => 'Phobject', 11360 11362 'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask', 11361 11363 'PhabricatorWorkerActiveTaskQuery' => 'PhabricatorWorkerTaskQuery',
+22 -1
src/applications/project/controller/PhabricatorProjectBoardController.php
··· 1 1 <?php 2 2 3 3 abstract class PhabricatorProjectBoardController 4 - extends PhabricatorProjectController {} 4 + extends PhabricatorProjectController { 5 + 6 + private $viewState; 7 + 8 + final protected function getViewState() { 9 + if ($this->viewState === null) { 10 + $this->viewState = $this->newViewState(); 11 + } 12 + 13 + return $this->viewState; 14 + } 15 + 16 + final private function newViewState() { 17 + $project = $this->getProject(); 18 + $request = $this->getRequest(); 19 + 20 + return id(new PhabricatorWorkboardViewState()) 21 + ->setProject($project) 22 + ->readFromRequest($request); 23 + } 24 + 25 + }
+21 -105
src/applications/project/controller/PhabricatorProjectBoardViewController.php
··· 5 5 6 6 const BATCH_EDIT_ALL = 'all'; 7 7 8 - private $queryKey; 9 - private $sortKey; 10 - private $showHidden; 11 - 12 8 public function shouldAllowPublic() { 13 9 return true; 14 10 } ··· 22 18 } 23 19 24 20 $project = $this->getProject(); 25 - 26 - $this->readRequestState(); 27 - 28 - $board_uri = $this->getApplicationURI('board/'.$project->getID().'/'); 21 + $state = $this->getViewState(); 22 + $board_uri = $project->getWorkboardURI(); 29 23 30 24 $search_engine = id(new ManiphestTaskSearchEngine()) 31 25 ->setViewer($viewer) ··· 51 45 ->addSubmitButton(pht('Apply Filter')) 52 46 ->addCancelButton($board_uri); 53 47 } 54 - return id(new AphrontRedirectResponse())->setURI( 55 - $this->getURIWithState( 56 - $search_engine->getQueryResultsPageURI($saved->getQueryKey()))); 57 - } 58 48 59 - $query_key = $this->getDefaultFilter($project); 60 - 61 - $request_query = $request->getStr('filter'); 62 - if (strlen($request_query)) { 63 - $query_key = $request_query; 64 - } 49 + $query_key = $saved->getQueryKey(); 50 + $results_uri = $search_engine->getQueryResultsPageURI($query_key); 51 + $results_uri = $state->newURI($results_uri); 65 52 66 - $uri_query = $request->getURIData('queryKey'); 67 - if (strlen($uri_query)) { 68 - $query_key = $uri_query; 53 + return id(new AphrontRedirectResponse())->setURI($results_uri); 69 54 } 70 55 71 - $this->queryKey = $query_key; 56 + $query_key = $state->getQueryKey(); 72 57 73 58 $custom_query = null; 74 59 if ($search_engine->isBuiltinQuery($query_key)) { ··· 260 245 } 261 246 262 247 if (!$batch_tasks) { 263 - $cancel_uri = $this->getURIWithState($board_uri); 248 + $cancel_uri = $state->newWorkboardURI(); 264 249 return $this->newDialog() 265 250 ->setTitle(pht('No Editable Tasks')) 266 251 ->appendParagraph( ··· 308 293 } 309 294 310 295 $move_tasks = array_select_keys($tasks, $move_task_phids); 311 - $cancel_uri = $this->getURIWithState($board_uri); 296 + $cancel_uri = $state->newWorkboardURI(); 312 297 313 298 if (!$move_tasks) { 314 299 return $this->newDialog() ··· 504 489 $column_phids = array(); 505 490 $visible_phids = array(); 506 491 foreach ($columns as $column) { 507 - if (!$this->showHidden) { 492 + if (!$state->getShowHidden()) { 508 493 if ($column->isHidden()) { 509 494 continue; 510 495 } ··· 649 634 ); 650 635 } 651 636 652 - $order_key = $this->sortKey; 637 + $order_key = $state->getOrder(); 653 638 654 639 $ordering_map = PhabricatorProjectColumnOrder::getEnabledOrders(); 655 640 $ordering = id(clone $ordering_map[$order_key]) ··· 683 668 'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(), 684 669 685 670 'boardPHID' => $project->getPHID(), 686 - 'order' => $this->sortKey, 671 + 'order' => $state->getOrder(), 687 672 'orders' => $order_maps, 688 673 'headers' => $headers, 689 674 'headerKeys' => $header_keys, ··· 701 686 $sort_menu = $this->buildSortMenu( 702 687 $viewer, 703 688 $project, 704 - $this->sortKey, 689 + $state->getOrder(), 705 690 $ordering_map); 706 691 707 692 $filter_menu = $this->buildFilterMenu( ··· 711 696 $search_engine, 712 697 $query_key); 713 698 714 - $manage_menu = $this->buildManageMenu($project, $this->showHidden); 699 + $manage_menu = $this->buildManageMenu($project, $state->getShowHidden()); 715 700 716 701 $header_link = phutil_tag( 717 702 'a', ··· 775 760 return $page; 776 761 } 777 762 778 - private function readRequestState() { 779 - $request = $this->getRequest(); 780 - $project = $this->getProject(); 781 - 782 - $this->showHidden = $request->getBool('hidden'); 783 - 784 - $sort_key = $this->getDefaultSort($project); 785 - 786 - $request_sort = $request->getStr('order'); 787 - if ($this->isValidSort($request_sort)) { 788 - $sort_key = $request_sort; 789 - } 790 - 791 - $this->sortKey = $sort_key; 792 - } 793 - 794 - private function getDefaultSort(PhabricatorProject $project) { 795 - $default_sort = $project->getDefaultWorkboardSort(); 796 - 797 - if ($this->isValidSort($default_sort)) { 798 - return $default_sort; 799 - } 800 - 801 - return PhabricatorProjectColumnNaturalOrder::ORDERKEY; 802 - } 803 - 804 - private function getDefaultFilter(PhabricatorProject $project) { 805 - $default_filter = $project->getDefaultWorkboardFilter(); 806 - 807 - if (strlen($default_filter)) { 808 - return $default_filter; 809 - } 810 - 811 - return 'open'; 812 - } 813 - 814 - private function isValidSort($sort) { 815 - $map = PhabricatorProjectColumnOrder::getEnabledOrders(); 816 - return isset($map[$sort]); 817 - } 818 - 819 763 private function buildSortMenu( 820 764 PhabricatorUser $viewer, 821 765 PhabricatorProject $project, 822 766 $sort_key, 823 767 array $ordering_map) { 824 768 825 - $base_uri = $this->getURIWithState(); 769 + $state = $this->getViewState(); 770 + $base_uri = $state->newWorkboardURI(); 826 771 827 772 $items = array(); 828 773 foreach ($ordering_map as $key => $ordering) { ··· 997 942 998 943 $request = $this->getRequest(); 999 944 $viewer = $request->getUser(); 945 + $state = $this->getViewState(); 1000 946 1001 947 $id = $project->getID(); 1002 948 ··· 1026 972 ->setWorkflow(true); 1027 973 1028 974 if ($show_hidden) { 1029 - $hidden_uri = $this->getURIWithState() 975 + $hidden_uri = $state->newWorkboardURI() 1030 976 ->removeQueryParam('hidden'); 1031 977 $hidden_icon = 'fa-eye-slash'; 1032 978 $hidden_text = pht('Hide Hidden Columns'); 1033 979 } else { 1034 - $hidden_uri = $this->getURIWithState() 980 + $hidden_uri = $state->newWorkboardURI() 1035 981 ->replaceQueryParam('hidden', 'true'); 1036 982 $hidden_icon = 'fa-eye'; 1037 983 $hidden_text = pht('Show Hidden Columns'); ··· 1307 1253 * @return PhutilURI URI with state parameters. 1308 1254 */ 1309 1255 private function getURIWithState($base = null, $force = false) { 1310 - $project = $this->getProject(); 1311 - 1312 1256 if ($base === null) { 1313 - $base = $this->getRequest()->getPath(); 1314 - } 1315 - 1316 - $base = new PhutilURI($base); 1317 - 1318 - if ($force || ($this->sortKey != $this->getDefaultSort($project))) { 1319 - if ($this->sortKey !== null) { 1320 - $base->replaceQueryParam('order', $this->sortKey); 1321 - } else { 1322 - $base->removeQueryParam('order'); 1323 - } 1324 - } else { 1325 - $base->removeQueryParam('order'); 1326 - } 1327 - 1328 - if ($force || ($this->queryKey != $this->getDefaultFilter($project))) { 1329 - if ($this->queryKey !== null) { 1330 - $base->replaceQueryParam('filter', $this->queryKey); 1331 - } else { 1332 - $base->removeQueryParam('filter'); 1333 - } 1334 - } else { 1335 - $base->removeQueryParam('filter'); 1257 + $base = $this->getProject()->getWorkboardURI(); 1336 1258 } 1337 1259 1338 - if ($this->showHidden) { 1339 - $base->replaceQueryParam('hidden', 'true'); 1340 - } else { 1341 - $base->removeQueryParam('hidden'); 1342 - } 1343 - 1344 - return $base; 1260 + return $this->getViewState()->newURI($base, $force); 1345 1261 } 1346 1262 1347 1263 private function buildInitializeContent(PhabricatorProject $project) {
+150
src/applications/project/state/PhabricatorWorkboardViewState.php
··· 1 + <?php 2 + 3 + final class PhabricatorWorkboardViewState 4 + extends Phobject { 5 + 6 + private $project; 7 + private $requestState = array(); 8 + 9 + public function setProject(PhabricatorProject $project) { 10 + $this->project = $project; 11 + return $this; 12 + } 13 + 14 + public function getProject() { 15 + return $this->project; 16 + } 17 + 18 + public function readFromRequest(AphrontRequest $request) { 19 + if ($request->getExists('hidden')) { 20 + $this->requestState['hidden'] = $request->getBool('hidden'); 21 + } 22 + 23 + if ($request->getExists('order')) { 24 + $this->requestState['order'] = $request->getStr('order'); 25 + } 26 + 27 + // On some pathways, the search engine query key may be specified with 28 + // either a "?filter=X" query parameter or with a "/query/X/" URI 29 + // component. If both are present, the URI component is controlling. 30 + 31 + // In particular, the "queryKey" URI parameter is used by 32 + // "buildSavedQueryFromRequest()" when we are building custom board filters 33 + // by invoking SearchEngine code. 34 + 35 + if ($request->getExists('filter')) { 36 + $this->requestState['filter'] = $request->getStr('filter'); 37 + } 38 + 39 + if (strlen($request->getURIData('queryKey'))) { 40 + $this->requestState['filter'] = $request->getURIData('queryKey'); 41 + } 42 + 43 + return $this; 44 + } 45 + 46 + public function newWorkboardURI($path = null) { 47 + $project = $this->getProject(); 48 + $uri = urisprintf('%p%p', $project->getWorkboardURI(), $path); 49 + return $this->newURI($uri); 50 + } 51 + 52 + public function newURI($path, $force = false) { 53 + $project = $this->getProject(); 54 + $uri = new PhutilURI($path); 55 + 56 + $request_order = $this->getOrder(); 57 + $default_order = $this->getDefaultOrder(); 58 + if ($force || ($request_order !== $default_order)) { 59 + $request_value = idx($this->requestState, 'order'); 60 + if ($request_value !== null) { 61 + $uri->replaceQueryParam('order', $request_value); 62 + } else { 63 + $uri->removeQueryParam('order'); 64 + } 65 + } else { 66 + $uri->removeQueryParam('order'); 67 + } 68 + 69 + $request_query = $this->getQueryKey(); 70 + $default_query = $this->getDefaultQueryKey(); 71 + if ($force || ($request_query !== $default_query)) { 72 + $request_value = idx($this->requestState, 'filter'); 73 + if ($request_value !== null) { 74 + $uri->replaceQueryParam('filter', $request_value); 75 + } else { 76 + $uri->removeQueryParam('filter'); 77 + } 78 + } else { 79 + $uri->removeQueryParam('filter'); 80 + } 81 + 82 + if ($this->getShowHidden()) { 83 + $uri->replaceQueryParam('hidden', 'true'); 84 + } else { 85 + $uri->removeQueryParam('hidden'); 86 + } 87 + 88 + return $uri; 89 + } 90 + 91 + public function getShowHidden() { 92 + $request_show = idx($this->requestState, 'hidden'); 93 + 94 + if ($request_show !== null) { 95 + return $request_show; 96 + } 97 + 98 + return false; 99 + } 100 + 101 + public function getOrder() { 102 + $request_order = idx($this->requestState, 'order'); 103 + if ($request_order !== null) { 104 + if ($this->isValidOrder($request_order)) { 105 + return $request_order; 106 + } 107 + } 108 + 109 + return $this->getDefaultOrder(); 110 + } 111 + 112 + public function getQueryKey() { 113 + $request_query = idx($this->requestState, 'filter'); 114 + if (strlen($request_query)) { 115 + return $request_query; 116 + } 117 + 118 + return $this->getDefaultQueryKey(); 119 + } 120 + 121 + private function isValidOrder($order) { 122 + $map = PhabricatorProjectColumnOrder::getEnabledOrders(); 123 + return isset($map[$order]); 124 + } 125 + 126 + private function getDefaultOrder() { 127 + $project = $this->getProject(); 128 + 129 + $default_order = $project->getDefaultWorkboardSort(); 130 + 131 + if ($this->isValidOrder($default_order)) { 132 + return $default_order; 133 + } 134 + 135 + return PhabricatorProjectColumnNaturalOrder::ORDERKEY; 136 + } 137 + 138 + private function getDefaultQueryKey() { 139 + $project = $this->getProject(); 140 + 141 + $default_query = $project->getDefaultWorkboardFilter(); 142 + 143 + if (strlen($default_query)) { 144 + return $default_query; 145 + } 146 + 147 + return 'open'; 148 + } 149 + 150 + }