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

Use ApplicationSearch in Differential

Summary:
Ref T603. Ref T2625. Fixes T3241. Depends on D5451. Depends on D6346.

@wez, this changes the Differential revision list UI substantially and may generate a lot of bikeshedding / who-moved-my-cheese churn. See T3417 for context, for example. The motivations for this change are:

- The list now works on devices, like phones and tablets. This is a requirement to make the rest of Differential work on devices.
- Although ApplicationSearch intentionally presents a simpler interface initially and some options which were one click away before aren't now, it is much more powerful than the search it replaces and allows users to build, save, share, fork, edit, and customize a much wider range of queries. Users who used the old filters frequently can use Advanced Search -> Save Custom Query to create new versions of them, and of any other query. "Edit Queries.." allows users to remove and reorder queries, including builtin queries. Basically, there are like three things which have gone from "1-click" to "a few clicks", and ten trillion things which have gone from "hard/impossible" to "relatively easy".

The local screenshots look a bit iffy, but I think a lot of this is the fakenesss of my test data. If they still feel iffy in production we can tweak them until they feel good, like we did for Maniphest.

Test Plan:
{F48477}

{F48478}

Reviewers: btrahan, chad, wez

Reviewed By: btrahan

CC: aran, s

Maniphest Tasks: T603, T2625, T3241

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

+400 -550
+45 -45
src/__celerity_resource_map__.php
··· 3249 3249 ), 3250 3250 'phabricator-object-item-list-view-css' => 3251 3251 array( 3252 - 'uri' => '/res/d58ecb3c/rsrc/css/layout/phabricator-object-item-list-view.css', 3252 + 'uri' => '/res/4e44cc37/rsrc/css/layout/phabricator-object-item-list-view.css', 3253 3253 'type' => 'css', 3254 3254 'requires' => 3255 3255 array( ··· 4073 4073 ), array( 4074 4074 'packages' => 4075 4075 array( 4076 - '0ad4a08f' => 4076 + '8000c812' => 4077 4077 array( 4078 4078 'name' => 'core.pkg.css', 4079 4079 'symbols' => ··· 4121 4121 40 => 'phabricator-property-list-view-css', 4122 4122 41 => 'phabricator-tag-view-css', 4123 4123 ), 4124 - 'uri' => '/res/pkg/0ad4a08f/core.pkg.css', 4124 + 'uri' => '/res/pkg/8000c812/core.pkg.css', 4125 4125 'type' => 'css', 4126 4126 ), 4127 4127 'f2ad0683' => ··· 4315 4315 'reverse' => 4316 4316 array( 4317 4317 'aphront-attached-file-view-css' => 'adc3c36d', 4318 - 'aphront-dialog-view-css' => '0ad4a08f', 4319 - 'aphront-error-view-css' => '0ad4a08f', 4320 - 'aphront-form-view-css' => '0ad4a08f', 4321 - 'aphront-list-filter-view-css' => '0ad4a08f', 4322 - 'aphront-pager-view-css' => '0ad4a08f', 4323 - 'aphront-panel-view-css' => '0ad4a08f', 4324 - 'aphront-table-view-css' => '0ad4a08f', 4325 - 'aphront-tokenizer-control-css' => '0ad4a08f', 4326 - 'aphront-tooltip-css' => '0ad4a08f', 4327 - 'aphront-typeahead-control-css' => '0ad4a08f', 4318 + 'aphront-dialog-view-css' => '8000c812', 4319 + 'aphront-error-view-css' => '8000c812', 4320 + 'aphront-form-view-css' => '8000c812', 4321 + 'aphront-list-filter-view-css' => '8000c812', 4322 + 'aphront-pager-view-css' => '8000c812', 4323 + 'aphront-panel-view-css' => '8000c812', 4324 + 'aphront-table-view-css' => '8000c812', 4325 + 'aphront-tokenizer-control-css' => '8000c812', 4326 + 'aphront-tooltip-css' => '8000c812', 4327 + 'aphront-typeahead-control-css' => '8000c812', 4328 4328 'differential-changeset-view-css' => 'dd27a69b', 4329 4329 'differential-core-view-css' => 'dd27a69b', 4330 4330 'differential-inline-comment-editor' => '9488bb69', ··· 4338 4338 'differential-table-of-contents-css' => 'dd27a69b', 4339 4339 'diffusion-commit-view-css' => 'c8ce2d88', 4340 4340 'diffusion-icons-css' => 'c8ce2d88', 4341 - 'global-drag-and-drop-css' => '0ad4a08f', 4341 + 'global-drag-and-drop-css' => '8000c812', 4342 4342 'inline-comment-summary-css' => 'dd27a69b', 4343 4343 'javelin-aphlict' => 'f2ad0683', 4344 4344 'javelin-behavior' => 'a9f14d76', ··· 4412 4412 'javelin-util' => 'a9f14d76', 4413 4413 'javelin-vector' => 'a9f14d76', 4414 4414 'javelin-workflow' => 'a9f14d76', 4415 - 'lightbox-attachment-css' => '0ad4a08f', 4415 + 'lightbox-attachment-css' => '8000c812', 4416 4416 'maniphest-task-summary-css' => 'adc3c36d', 4417 4417 'maniphest-transaction-detail-css' => 'adc3c36d', 4418 - 'phabricator-action-list-view-css' => '0ad4a08f', 4419 - 'phabricator-application-launch-view-css' => '0ad4a08f', 4418 + 'phabricator-action-list-view-css' => '8000c812', 4419 + 'phabricator-application-launch-view-css' => '8000c812', 4420 4420 'phabricator-busy' => 'f2ad0683', 4421 4421 'phabricator-content-source-view-css' => 'dd27a69b', 4422 - 'phabricator-core-css' => '0ad4a08f', 4423 - 'phabricator-crumbs-view-css' => '0ad4a08f', 4422 + 'phabricator-core-css' => '8000c812', 4423 + 'phabricator-crumbs-view-css' => '8000c812', 4424 4424 'phabricator-drag-and-drop-file-upload' => '9488bb69', 4425 4425 'phabricator-dropdown-menu' => 'f2ad0683', 4426 4426 'phabricator-file-upload' => 'f2ad0683', 4427 - 'phabricator-filetree-view-css' => '0ad4a08f', 4428 - 'phabricator-flag-css' => '0ad4a08f', 4429 - 'phabricator-form-view-css' => '0ad4a08f', 4430 - 'phabricator-header-view-css' => '0ad4a08f', 4427 + 'phabricator-filetree-view-css' => '8000c812', 4428 + 'phabricator-flag-css' => '8000c812', 4429 + 'phabricator-form-view-css' => '8000c812', 4430 + 'phabricator-header-view-css' => '8000c812', 4431 4431 'phabricator-hovercard' => 'f2ad0683', 4432 - 'phabricator-jump-nav' => '0ad4a08f', 4432 + 'phabricator-jump-nav' => '8000c812', 4433 4433 'phabricator-keyboard-shortcut' => 'f2ad0683', 4434 4434 'phabricator-keyboard-shortcut-manager' => 'f2ad0683', 4435 - 'phabricator-main-menu-view' => '0ad4a08f', 4435 + 'phabricator-main-menu-view' => '8000c812', 4436 4436 'phabricator-menu-item' => 'f2ad0683', 4437 - 'phabricator-nav-view-css' => '0ad4a08f', 4437 + 'phabricator-nav-view-css' => '8000c812', 4438 4438 'phabricator-notification' => 'f2ad0683', 4439 - 'phabricator-notification-css' => '0ad4a08f', 4440 - 'phabricator-notification-menu-css' => '0ad4a08f', 4441 - 'phabricator-object-item-list-view-css' => '0ad4a08f', 4439 + 'phabricator-notification-css' => '8000c812', 4440 + 'phabricator-notification-menu-css' => '8000c812', 4441 + 'phabricator-object-item-list-view-css' => '8000c812', 4442 4442 'phabricator-object-selector-css' => 'dd27a69b', 4443 4443 'phabricator-phtize' => 'f2ad0683', 4444 4444 'phabricator-prefab' => 'f2ad0683', 4445 4445 'phabricator-project-tag-css' => 'adc3c36d', 4446 - 'phabricator-property-list-view-css' => '0ad4a08f', 4447 - 'phabricator-remarkup-css' => '0ad4a08f', 4446 + 'phabricator-property-list-view-css' => '8000c812', 4447 + 'phabricator-remarkup-css' => '8000c812', 4448 4448 'phabricator-shaped-request' => '9488bb69', 4449 - 'phabricator-side-menu-view-css' => '0ad4a08f', 4450 - 'phabricator-standard-page-view' => '0ad4a08f', 4451 - 'phabricator-tag-view-css' => '0ad4a08f', 4449 + 'phabricator-side-menu-view-css' => '8000c812', 4450 + 'phabricator-standard-page-view' => '8000c812', 4451 + 'phabricator-tag-view-css' => '8000c812', 4452 4452 'phabricator-textareautils' => 'f2ad0683', 4453 4453 'phabricator-tooltip' => 'f2ad0683', 4454 - 'phabricator-transaction-view-css' => '0ad4a08f', 4455 - 'phabricator-zindex-css' => '0ad4a08f', 4456 - 'phui-button-css' => '0ad4a08f', 4457 - 'phui-form-css' => '0ad4a08f', 4458 - 'phui-icon-view-css' => '0ad4a08f', 4459 - 'phui-spacing-css' => '0ad4a08f', 4460 - 'sprite-apps-large-css' => '0ad4a08f', 4461 - 'sprite-gradient-css' => '0ad4a08f', 4462 - 'sprite-icons-css' => '0ad4a08f', 4463 - 'sprite-menu-css' => '0ad4a08f', 4464 - 'syntax-highlighting-css' => '0ad4a08f', 4454 + 'phabricator-transaction-view-css' => '8000c812', 4455 + 'phabricator-zindex-css' => '8000c812', 4456 + 'phui-button-css' => '8000c812', 4457 + 'phui-form-css' => '8000c812', 4458 + 'phui-icon-view-css' => '8000c812', 4459 + 'phui-spacing-css' => '8000c812', 4460 + 'sprite-apps-large-css' => '8000c812', 4461 + 'sprite-gradient-css' => '8000c812', 4462 + 'sprite-icons-css' => '8000c812', 4463 + 'sprite-menu-css' => '8000c812', 4464 + 'syntax-highlighting-css' => '8000c812', 4465 4465 ), 4466 4466 ));
+7 -1
src/__phutil_library_map__.php
··· 407 407 'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php', 408 408 'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php', 409 409 'DifferentialRevisionQuery' => 'applications/differential/query/DifferentialRevisionQuery.php', 410 + 'DifferentialRevisionSearchEngine' => 'applications/differential/query/DifferentialRevisionSearchEngine.php', 410 411 'DifferentialRevisionStatsView' => 'applications/differential/view/DifferentialRevisionStatsView.php', 411 412 'DifferentialRevisionStatus' => 'applications/differential/constants/DifferentialRevisionStatus.php', 412 413 'DifferentialRevisionStatusFieldSpecification' => 'applications/differential/field/specification/DifferentialRevisionStatusFieldSpecification.php', ··· 2316 2317 'DifferentialRevisionEditor' => 'PhabricatorEditor', 2317 2318 'DifferentialRevisionIDFieldParserTestCase' => 'PhabricatorTestCase', 2318 2319 'DifferentialRevisionIDFieldSpecification' => 'DifferentialFieldSpecification', 2319 - 'DifferentialRevisionListController' => 'DifferentialController', 2320 + 'DifferentialRevisionListController' => 2321 + array( 2322 + 0 => 'DifferentialController', 2323 + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', 2324 + ), 2320 2325 'DifferentialRevisionListView' => 'AphrontView', 2321 2326 'DifferentialRevisionMailReceiver' => 'PhabricatorObjectMailReceiver', 2322 2327 'DifferentialRevisionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 2328 + 'DifferentialRevisionSearchEngine' => 'PhabricatorApplicationSearchEngine', 2323 2329 'DifferentialRevisionStatsView' => 'AphrontView', 2324 2330 'DifferentialRevisionStatusFieldSpecification' => 'DifferentialFieldSpecification', 2325 2331 'DifferentialRevisionUpdateHistoryView' => 'AphrontView',
+2 -3
src/applications/differential/application/PhabricatorApplicationDifferential.php
··· 39 39 return array( 40 40 '/D(?P<id>[1-9]\d*)' => 'DifferentialRevisionViewController', 41 41 '/differential/' => array( 42 - '' => 'DifferentialRevisionListController', 43 - 'filter/(?P<filter>\w+)/(?:(?P<username>[\w._-]+)/)?' => 44 - 'DifferentialRevisionListController', 42 + '(?:query/(?P<queryKey>[^/]+)/)?' 43 + => 'DifferentialRevisionListController', 45 44 'diff/' => array( 46 45 '(?P<id>[1-9]\d*)/' => 'DifferentialDiffViewController', 47 46 'create/' => 'DifferentialDiffCreateController',
+1 -1
src/applications/differential/controller/DifferentialChangesetViewController.php
··· 253 253 ), 254 254 $detail->render())); 255 255 256 - return $this->buildStandardPageResponse( 256 + return $this->buildApplicationPage( 257 257 array( 258 258 $panel 259 259 ),
+10 -68
src/applications/differential/controller/DifferentialController.php
··· 6 6 return PhabricatorEnv::getEnvConfig('differential.anonymous-access'); 7 7 } 8 8 9 - public function buildStandardPageResponse($view, array $data) { 10 - 11 - require_celerity_resource('differential-core-view-css'); 12 - 13 - $page = $this->buildStandardPageView(); 14 - $page->setApplicationName(pht('Differential')); 15 - $page->setBaseURI('/differential/'); 16 - $page->setTitle(idx($data, 'title')); 17 - $page->setGlyph("\xE2\x9A\x99"); 18 - $page->appendChild($view); 19 - $page->setSearchDefaultScope(PhabricatorSearchScope::SCOPE_OPEN_REVISIONS); 20 - 21 - $response = new AphrontWebpageResponse(); 22 - return $response->setContent($page->render()); 23 - } 24 - 25 9 public function buildApplicationCrumbs() { 26 10 $crumbs = parent::buildApplicationCrumbs(); 27 11 ··· 34 18 return $crumbs; 35 19 } 36 20 37 - public function buildSideNav($filter = null, 38 - $for_app = false, $username = null) { 39 - 40 - $viewer_is_anonymous = !$this->getRequest()->getUser()->isLoggedIn(); 41 - 42 - $uri = id(new PhutilURI('/differential/filter/')) 43 - ->setQueryParams($this->getRequest()->getRequestURI()->getQueryParams()); 44 - $filters = $this->getFilters(); 45 - $filter = $this->selectFilter($filters, $filter, $viewer_is_anonymous); 46 - 47 - $side_nav = new AphrontSideNavFilterView(); 48 - $side_nav->setBaseURI($uri); 49 - foreach ($filters as $filter) { 50 - list($filter_name, $display_name) = $filter; 51 - if ($filter_name) { 52 - $side_nav->addFilter($filter_name.'/'.$username, $display_name); 53 - } else { 54 - $side_nav->addLabel($display_name); 55 - } 56 - } 57 - 58 - return $side_nav; 59 - } 21 + public function buildSideNavView($for_app = false) { 22 + $viewer = $this->getRequest()->getUser(); 60 23 61 - protected function getFilters() { 62 - return array( 63 - array(null, pht('User Revisions')), 64 - array('active', pht('Active')), 65 - array('revisions', pht('Revisions')), 66 - array('reviews', pht('Reviews')), 67 - array('subscribed', pht('Subscribed')), 68 - array('drafts', pht('Draft Reviews')), 69 - array(null, pht('All Revisions')), 70 - array('all', pht('All')), 71 - ); 72 - } 24 + $nav = new AphrontSideNavFilterView(); 25 + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); 73 26 74 - protected function selectFilter( 75 - array $filters, 76 - $requested_filter, 77 - $viewer_is_anonymous) { 27 + id(new DifferentialRevisionSearchEngine()) 28 + ->setViewer($viewer) 29 + ->addNavigationItems($nav->getMenu()); 78 30 79 - $default_filter = ($viewer_is_anonymous ? 'all' : 'active'); 31 + $nav->selectFilter(null); 80 32 81 - // If the user requested a filter, make sure it actually exists. 82 - if ($requested_filter) { 83 - foreach ($filters as $filter) { 84 - if ($filter[0] === $requested_filter) { 85 - return $requested_filter; 86 - } 87 - } 88 - } 89 - 90 - // If not, return the default filter. 91 - return $default_filter; 33 + return $nav; 92 34 } 93 35 94 36 public function buildApplicationMenu() { 95 - return $this->buildSideNav(null, true)->getMenu(); 37 + return $this->buildSideNavView(true)->getMenu(); 96 38 } 97 39 98 40 }
+63 -415
src/applications/differential/controller/DifferentialRevisionListController.php
··· 1 1 <?php 2 2 3 - final class DifferentialRevisionListController extends DifferentialController { 3 + final class DifferentialRevisionListController extends DifferentialController 4 + implements PhabricatorApplicationSearchResultsControllerInterface { 4 5 5 - private $filter; 6 - private $username; 6 + private $queryKey; 7 7 8 8 public function shouldRequireLogin() { 9 9 return !$this->allowsAnonymousAccess(); 10 10 } 11 11 12 + public function shouldAllowPublic() { 13 + return true; 14 + } 15 + 12 16 public function willProcessRequest(array $data) { 13 - $this->filter = idx($data, 'filter'); 14 - $this->username = idx($data, 'username'); 17 + $this->queryKey = idx($data, 'queryKey'); 15 18 } 16 19 17 20 public function processRequest() { 18 21 $request = $this->getRequest(); 19 - $user = $request->getUser(); 20 - 21 - $params = array_filter( 22 - array( 23 - 'status' => $request->getStr('status'), 24 - 'order' => $request->getStr('order'), 25 - )); 26 - $params['participants'] = $request->getArr('participants'); 27 - 28 - $filters = $this->getFilters(); 29 - $this->filter = $this->selectFilter($filters, 30 - $this->filter, !$user->isLoggedIn()); 31 - 32 - // Redirect from search to canonical URL. 33 - $phid_arr = $request->getArr('view_users'); 34 - if ($phid_arr) { 35 - $view_users = id(new PhabricatorUser()) 36 - ->loadAllWhere('phid IN (%Ls)', $phid_arr); 37 - 38 - if (count($view_users) == 1) { 39 - // This is a single user, so generate a pretty URI. 40 - $uri = new PhutilURI( 41 - '/differential/filter/'.$this->filter.'/'. 42 - phutil_escape_uri(reset($view_users)->getUserName()).'/'); 43 - $uri->setQueryParams($params); 44 - 45 - return id(new AphrontRedirectResponse())->setURI($uri); 46 - } 47 - 48 - } 49 - 50 - $uri = new PhutilURI('/differential/filter/'.$this->filter.'/'); 51 - $uri->setQueryParams($params); 52 - 53 - $username = ''; 54 - if ($this->username) { 55 - $view_user = id(new PhabricatorUser()) 56 - ->loadOneWhere('userName = %s', $this->username); 57 - if (!$view_user) { 58 - return new Aphront404Response(); 59 - } 60 - $username = phutil_escape_uri($this->username).'/'; 61 - $uri->setPath('/differential/filter/'.$this->filter.'/'.$username); 62 - $params['view_users'] = array($view_user->getPHID()); 63 - } else { 64 - $phids = $request->getArr('view_users'); 65 - if ($phids) { 66 - $params['view_users'] = $phids; 67 - $uri->setQueryParams($params); 68 - } 69 - } 70 - 71 - // Fill in the defaults we'll actually use for calculations if any 72 - // parameters are missing. 73 - $params += array( 74 - 'view_users' => array($user->getPHID()), 75 - 'status' => 'all', 76 - 'order' => 'modified', 77 - ); 78 - 79 - $side_nav = $this->buildSideNav($this->filter, false, $username); 80 - $side_nav->selectFilter($this->filter.'/'.$username, null); 81 - 82 - $panels = array(); 83 - $handles = array(); 84 - $controls = $this->getFilterControls($this->filter); 85 - if ($this->getFilterRequiresUser($this->filter) && !$params['view_users']) { 86 - // In the anonymous case, we still want to let you see some user's 87 - // list, but we don't have a default PHID to provide (normally, we use 88 - // the viewing user's). Show a warning instead. 89 - $warning = new AphrontErrorView(); 90 - $warning->setSeverity(AphrontErrorView::SEVERITY_WARNING); 91 - $warning->setTitle(pht('User Required')); 92 - $warning->appendChild( 93 - pht('This filter requires that a user be specified above.')); 94 - $panels[] = $warning; 95 - } else { 96 - $query = $this->buildQuery($this->filter, $params); 97 - 98 - $pager = null; 99 - if ($this->getFilterAllowsPaging($this->filter)) { 100 - $pager = new AphrontCursorPagerView(); 101 - $pager->readFromRequest($request); 102 - } 103 - 104 - foreach ($controls as $control) { 105 - $this->applyControlToQuery($control, $query, $params); 106 - } 107 - 108 - if ($pager) { 109 - $revisions = $query->executeWithCursorPager($pager); 110 - } else { 111 - $revisions = $query->execute(); 112 - } 113 - 114 - $views = $this->buildViews( 115 - $this->filter, 116 - $params['view_users'], 117 - $revisions); 118 - 119 - $view_objects = array(); 120 - foreach ($views as $view) { 121 - if (empty($view['special'])) { 122 - $view_objects[] = $view['view']; 123 - } 124 - } 125 - $phids = mpull($view_objects, 'getRequiredHandlePHIDs'); 126 - $phids[] = $params['view_users']; 127 - $phids = array_mergev($phids); 128 - $handles = $this->loadViewerHandles($phids); 129 - 130 - foreach ($views as $view) { 131 - $view['view']->setHandles($handles); 132 - $panel = new AphrontPanelView(); 133 - $panel->setHeader($view['title']); 134 - $panel->appendChild($view['view']); 135 - if ($pager) { 136 - $panel->appendChild($pager); 137 - } 138 - $panel->setNoBackground(); 139 - $panels[] = $panel; 140 - } 141 - } 142 - 143 - $filter_form = id(new AphrontFormView()) 144 - ->setMethod('GET') 145 - ->setNoShading(true) 146 - ->setAction('/differential/filter/'.$this->filter.'/') 147 - ->setUser($user); 148 - foreach ($controls as $control) { 149 - $control_view = $this->renderControl($control, $handles, $uri, $params); 150 - $filter_form->appendChild($control_view); 151 - } 152 - $filter_form 153 - ->addHiddenInput('status', $params['status']) 154 - ->addHiddenInput('order', $params['order']) 155 - ->appendChild( 156 - id(new AphrontFormSubmitControl()) 157 - ->setValue(pht('Filter Revisions'))); 158 - 159 - $filter_view = new AphrontListFilterView(); 160 - $filter_view->appendChild($filter_form); 161 - 162 - $side_nav->appendChild($filter_view); 163 - 164 - foreach ($panels as $panel) { 165 - $side_nav->appendChild($panel); 166 - } 167 - 168 - $crumbs = $this->buildApplicationCrumbs(); 169 - $name = $side_nav 170 - ->getMenu() 171 - ->getItem($side_nav->getSelectedFilter()) 172 - ->getName(); 173 - $crumbs->addCrumb( 174 - id(new PhabricatorCrumbView()) 175 - ->setName($name) 176 - ->setHref($request->getRequestURI())); 177 - $side_nav->setCrumbs($crumbs); 178 - 179 - return $this->buildApplicationPage( 180 - $side_nav, 181 - array( 182 - 'title' => pht('Differential Home'), 183 - 'device' => true, 184 - 'dust' => true, 185 - )); 186 - } 187 - 188 - private function getFilterRequiresUser($filter) { 189 - static $requires = array( 190 - 'active' => true, 191 - 'revisions' => true, 192 - 'reviews' => true, 193 - 'subscribed' => true, 194 - 'drafts' => true, 195 - 'all' => false, 196 - ); 197 - if (!isset($requires[$filter])) { 198 - throw new Exception("Unknown filter '{$filter}'!"); 199 - } 200 - return $requires[$filter]; 201 - } 202 - 203 - private function getFilterAllowsPaging($filter) { 204 - static $allows = array( 205 - 'active' => false, 206 - 'revisions' => true, 207 - 'reviews' => true, 208 - 'subscribed' => true, 209 - 'drafts' => true, 210 - 'all' => true, 211 - ); 212 - if (!isset($allows[$filter])) { 213 - throw new Exception("Unknown filter '{$filter}'!"); 214 - } 215 - return $allows[$filter]; 216 - } 217 - 218 - private function getFilterControls($filter) { 219 - static $controls = array( 220 - 'active' => array('phid'), 221 - 'revisions' => array('phid', 'participants', 'status', 'order'), 222 - 'reviews' => array('phid', 'participants', 'status', 'order'), 223 - 'subscribed' => array('subscriber', 'status', 'order'), 224 - 'drafts' => array('phid', 'status', 'order'), 225 - 'all' => array('status', 'order'), 226 - ); 227 - if (!isset($controls[$filter])) { 228 - throw new Exception("Unknown filter '{$filter}'!"); 229 - } 230 - return $controls[$filter]; 231 - } 232 - 233 - private function buildQuery($filter, array $params) { 234 - $user_phids = $params['view_users']; 235 - $query = id(new DifferentialRevisionQuery()) 236 - ->setViewer($this->getRequest()->getUser()) 237 - ->needRelationships(true); 238 - 239 - switch ($filter) { 240 - case 'active': 241 - $query->withResponsibleUsers($user_phids); 242 - $query->withStatus(DifferentialRevisionQuery::STATUS_OPEN); 243 - $query->setLimit(null); 244 - break; 245 - case 'revisions': 246 - $query->withAuthors($user_phids); 247 - $query->withReviewers($params['participants']); 248 - break; 249 - case 'reviews': 250 - $query->withReviewers($user_phids); 251 - $query->withAuthors($params['participants']); 252 - break; 253 - case 'subscribed': 254 - $query->withSubscribers($user_phids); 255 - break; 256 - case 'drafts': 257 - $query->withDraftRepliesByAuthors($user_phids); 258 - break; 259 - case 'all': 260 - break; 261 - default: 262 - throw new Exception("Unknown filter '{$filter}'!"); 263 - } 264 - return $query; 265 - } 266 - 267 - private function renderControl( 268 - $control, 269 - array $handles, 270 - PhutilURI $uri, 271 - array $params) { 272 - assert_instances_of($handles, 'PhabricatorObjectHandle'); 273 - 274 - switch ($control) { 275 - case 'subscriber': 276 - case 'phid': 277 - $value = mpull( 278 - array_select_keys($handles, $params['view_users']), 279 - 'getFullName'); 280 - 281 - if ($control == 'subscriber') { 282 - $source = '/typeahead/common/allmailable/'; 283 - $label = pht('View Subscribers'); 284 - } else { 285 - $source = '/typeahead/common/accounts/'; 286 - switch ($this->filter) { 287 - case 'revisions': 288 - $label = pht('Authors'); 289 - break; 290 - case 'reviews': 291 - $label = pht('Reviewers'); 292 - break; 293 - default: 294 - $label = pht('View Users'); 295 - break; 296 - } 297 - } 298 - 299 - return id(new AphrontFormTokenizerControl()) 300 - ->setDatasource($source) 301 - ->setLabel($label) 302 - ->setName('view_users') 303 - ->setValue($value); 304 - 305 - case 'participants': 306 - switch ($this->filter) { 307 - case 'revisions': 308 - $label = pht('Reviewers'); 309 - break; 310 - case 'reviews': 311 - $label = pht('Authors'); 312 - break; 313 - } 314 - $value = mpull( 315 - array_select_keys($handles, $params['participants']), 316 - 'getFullName'); 317 - return id(new AphrontFormTokenizerControl()) 318 - ->setDatasource('/typeahead/common/accounts/') 319 - ->setLabel($label) 320 - ->setName('participants') 321 - ->setValue($value); 322 - 323 - case 'status': 324 - return id(new AphrontFormToggleButtonsControl()) 325 - ->setLabel(pht('Status')) 326 - ->setValue($params['status']) 327 - ->setBaseURI($uri, 'status') 328 - ->setButtons( 329 - array( 330 - 'all' => pht('All'), 331 - 'open' => pht('Open'), 332 - 'closed' => pht('Closed'), 333 - 'abandoned' => pht('Abandoned'), 334 - )); 335 - 336 - case 'order': 337 - return id(new AphrontFormToggleButtonsControl()) 338 - ->setLabel(pht('Order')) 339 - ->setValue($params['order']) 340 - ->setBaseURI($uri, 'order') 341 - ->setButtons( 342 - array( 343 - 'modified' => pht('Updated'), 344 - 'created' => pht('Created'), 345 - )); 22 + $controller = id(new PhabricatorApplicationSearchController($request)) 23 + ->setQueryKey($this->queryKey) 24 + ->setSearchEngine(new DifferentialRevisionSearchEngine()) 25 + ->setNavigation($this->buildSideNavView()); 346 26 347 - default: 348 - throw new Exception("Unknown control '{$control}'!"); 349 - } 27 + return $this->delegateToController($controller); 350 28 } 351 29 352 - private function applyControlToQuery($control, $query, array $params) { 353 - switch ($control) { 354 - case 'phid': 355 - case 'subscriber': 356 - case 'participants': 357 - // Already applied by query construction. 358 - break; 359 - case 'status': 360 - if ($params['status'] == 'open') { 361 - $query->withStatus(DifferentialRevisionQuery::STATUS_OPEN); 362 - } else if ($params['status'] == 'closed') { 363 - $query->withStatus(DifferentialRevisionQuery::STATUS_CLOSED); 364 - } else if ($params['status'] == 'abandoned') { 365 - $query->withStatus(DifferentialRevisionQuery::STATUS_ABANDONED); 366 - } 367 - break; 368 - case 'order': 369 - if ($params['order'] == 'created') { 370 - $query->setOrder(DifferentialRevisionQuery::ORDER_CREATED); 371 - } 372 - break; 373 - default: 374 - throw new Exception("Unknown control '{$control}'!"); 375 - } 376 - } 377 - 378 - private function buildViews($filter, array $user_phids, array $revisions) { 30 + public function renderResultsList( 31 + array $revisions, 32 + PhabricatorSavedQuery $query) { 379 33 assert_instances_of($revisions, 'DifferentialRevision'); 380 34 381 35 $user = $this->getRequest()->getUser(); 382 - 383 36 $template = id(new DifferentialRevisionListView()) 384 37 ->setUser($user) 385 38 ->setFields(DifferentialRevisionListView::getDefaultFields($user)); 386 39 387 40 $views = array(); 388 - switch ($filter) { 389 - case 'active': 390 - list($blocking, $active, $waiting) = 391 - DifferentialRevisionQuery::splitResponsible( 392 - $revisions, 393 - $user_phids); 41 + if ($query->getQueryKey() == 'active') { 42 + $split = DifferentialRevisionQuery::splitResponsible( 43 + $revisions, 44 + $query->getParameter('responsiblePHIDs')); 45 + list($blocking, $active, $waiting) = $split; 394 46 395 - $view = id(clone $template) 396 - ->setHighlightAge(true) 397 - ->setRevisions($blocking) 398 - ->loadAssets(); 399 - $views[] = array( 400 - 'title' => pht('Blocking Others'), 401 - 'view' => $view, 402 - ); 47 + $views[] = id(clone $template) 48 + ->setHeader(pht('Blocking Others')) 49 + ->setNoDataString( 50 + pht('No revisions are blocked on your action.')) 51 + ->setHighlightAge(true) 52 + ->setRevisions($blocking) 53 + ->setHandles(array()) 54 + ->loadAssets(); 55 + 56 + $views[] = id(clone $template) 57 + ->setHeader(pht('Action Required')) 58 + ->setNoDataString( 59 + pht('No revisions require your action.')) 60 + ->setHighlightAge(true) 61 + ->setRevisions($active) 62 + ->setHandles(array()) 63 + ->loadAssets(); 64 + 65 + $views[] = id(clone $template) 66 + ->setHeader(pht('Waiting on Others')) 67 + ->setNoDataString( 68 + pht('You have no revisions waiting on others.')) 69 + ->setRevisions($waiting) 70 + ->setHandles(array()) 71 + ->loadAssets(); 72 + } else { 73 + $views[] = id(clone $template) 74 + ->setRevisions($revisions) 75 + ->setHandles(array()) 76 + ->loadAssets(); 77 + } 403 78 404 - $view = id(clone $template) 405 - ->setHighlightAge(true) 406 - ->setRevisions($active) 407 - ->loadAssets(); 408 - $views[] = array( 409 - 'title' => pht('Action Required'), 410 - 'view' => $view, 411 - ); 79 + $phids = array_mergev(mpull($views, 'getRequiredHandlePHIDs')); 80 + $handles = $this->loadViewerHandles($phids); 412 81 413 - $view = id(clone $template) 414 - ->setRevisions($waiting) 415 - ->loadAssets(); 416 - $views[] = array( 417 - 'title' => pht('Waiting On Others'), 418 - 'view' => $view, 419 - ); 420 - break; 421 - case 'revisions': 422 - case 'reviews': 423 - case 'subscribed': 424 - case 'drafts': 425 - case 'all': 426 - $titles = array( 427 - 'revisions' => pht('Revisions by Author'), 428 - 'reviews' => pht('Revisions by Reviewer'), 429 - 'subscribed' => pht('Revisions by Subscriber'), 430 - 'all' => pht('Revisions'), 431 - ); 432 - $view = id(clone $template) 433 - ->setRevisions($revisions) 434 - ->loadAssets(); 435 - $views[] = array( 436 - 'title' => idx($titles, $filter), 437 - 'view' => $view, 438 - ); 439 - break; 440 - default: 441 - throw new Exception("Unknown filter '{$filter}'!"); 82 + foreach ($views as $view) { 83 + $view->setHandles($handles); 442 84 } 443 85 444 - return $views; 86 + if (count($views) == 1) { 87 + // Reduce this to a PhabricatorObjectItemListView so we can get the free 88 + // support from ApplicationSearch. 89 + return head($views)->render(); 90 + } else { 91 + return $views; 92 + } 445 93 } 446 94 447 95 }
+217
src/applications/differential/query/DifferentialRevisionSearchEngine.php
··· 1 + <?php 2 + 3 + final class DifferentialRevisionSearchEngine 4 + extends PhabricatorApplicationSearchEngine { 5 + 6 + public function getPageSize(PhabricatorSavedQuery $saved) { 7 + if ($saved->getQueryKey() == 'active') { 8 + return 0xFFFF; 9 + } 10 + return parent::getPageSize($saved); 11 + } 12 + 13 + public function buildSavedQueryFromRequest(AphrontRequest $request) { 14 + $saved = new PhabricatorSavedQuery(); 15 + 16 + $saved->setParameter( 17 + 'responsiblePHIDs', 18 + $request->getArr('responsiblePHIDs')); 19 + 20 + $saved->setParameter( 21 + 'authorPHIDs', 22 + $request->getArr('authorPHIDs')); 23 + 24 + $saved->setParameter( 25 + 'reviewerPHIDs', 26 + $request->getArr('reviewerPHIDs')); 27 + 28 + $saved->setParameter( 29 + 'subscriberPHIDs', 30 + $request->getArr('subscriberPHIDs')); 31 + 32 + $saved->setParameter( 33 + 'draft', 34 + $request->getBool('draft')); 35 + 36 + $saved->setParameter( 37 + 'order', 38 + $request->getStr('order')); 39 + 40 + $saved->setParameter( 41 + 'status', 42 + $request->getStr('status')); 43 + 44 + return $saved; 45 + } 46 + 47 + public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { 48 + $query = id(new DifferentialRevisionQuery()) 49 + ->needRelationships(true); 50 + 51 + $responsible_phids = $saved->getParameter('responsiblePHIDs', array()); 52 + if ($responsible_phids) { 53 + $query->withResponsibleUsers($responsible_phids); 54 + } 55 + 56 + $author_phids = $saved->getParameter('authorPHIDs', array()); 57 + if ($author_phids) { 58 + $query->withAuthors($author_phids); 59 + } 60 + 61 + $reviewer_phids = $saved->getParameter('reviewerPHIDs', array()); 62 + if ($reviewer_phids) { 63 + $query->withReviewers($reviewer_phids); 64 + } 65 + 66 + $subscriber_phids = $saved->getParameter('subscriberPHIDs', array()); 67 + if ($subscriber_phids) { 68 + $query->withCCs($subscriber_phids); 69 + } 70 + 71 + $draft = $saved->getParameter('draft', false); 72 + if ($draft && $this->requireViewer()->isLoggedIn()) { 73 + $query->withDraftRepliesByAuthors( 74 + array($this->requireViewer()->getPHID())); 75 + } 76 + 77 + $status = $saved->getParameter('status'); 78 + if (idx($this->getStatusOptions(), $status)) { 79 + $query->withStatus($status); 80 + } 81 + 82 + $order = $saved->getParameter('order'); 83 + if (idx($this->getOrderOptions(), $order)) { 84 + $query->setOrder($order); 85 + } 86 + 87 + return $query; 88 + } 89 + 90 + public function buildSearchForm( 91 + AphrontFormView $form, 92 + PhabricatorSavedQuery $saved) { 93 + 94 + $responsible_phids = $saved->getParameter('responsiblePHIDs', array()); 95 + $author_phids = $saved->getParameter('authorPHIDs', array()); 96 + $reviewer_phids = $saved->getParameter('reviewerPHIDs', array()); 97 + $subscriber_phids = $saved->getParameter('subscriberPHIDs', array()); 98 + $only_draft = $saved->getParameter('draft', false); 99 + 100 + $all_phids = array_mergev( 101 + array( 102 + $responsible_phids, 103 + $author_phids, 104 + $reviewer_phids, 105 + $subscriber_phids, 106 + )); 107 + 108 + $handles = id(new PhabricatorObjectHandleData($all_phids)) 109 + ->setViewer($this->requireViewer()) 110 + ->loadHandles(); 111 + 112 + $tokens = mpull($handles, 'getFullName', 'getPHID'); 113 + 114 + $form 115 + ->appendChild( 116 + id(new AphrontFormTokenizerControl()) 117 + ->setLabel(pht('Responsible Users')) 118 + ->setName('responsiblePHIDs') 119 + ->setDatasource('/typeahead/common/users/') 120 + ->setValue(array_select_keys($tokens, $responsible_phids))) 121 + ->appendChild( 122 + id(new AphrontFormTokenizerControl()) 123 + ->setLabel(pht('Authors')) 124 + ->setName('authorPHIDs') 125 + ->setDatasource('/typeahead/common/authors/') 126 + ->setValue(array_select_keys($tokens, $author_phids))) 127 + ->appendChild( 128 + id(new AphrontFormTokenizerControl()) 129 + ->setLabel(pht('Reviewers')) 130 + ->setName('reviewerPHIDs') 131 + ->setDatasource('/typeahead/common/users/') 132 + ->setValue(array_select_keys($tokens, $reviewer_phids))) 133 + ->appendChild( 134 + id(new AphrontFormTokenizerControl()) 135 + ->setLabel(pht('Subscribers')) 136 + ->setName('subscriberPHIDs') 137 + ->setDatasource('/typeahead/common/mailable/') 138 + ->setValue(array_select_keys($tokens, $subscriber_phids))) 139 + ->appendChild( 140 + id(new AphrontFormSelectControl()) 141 + ->setLabel(pht('Status')) 142 + ->setName('status') 143 + ->setOptions($this->getStatusOptions()) 144 + ->setValue($saved->getParameter('status'))); 145 + 146 + if ($this->requireViewer()->isLoggedIn()) { 147 + $form 148 + ->appendChild( 149 + id(new AphrontFormCheckboxControl()) 150 + ->addCheckbox( 151 + 'draft', 152 + 1, 153 + pht('Show only revisions with a draft comment.'), 154 + $only_draft)); 155 + } 156 + 157 + $form 158 + ->appendChild( 159 + id(new AphrontFormSelectControl()) 160 + ->setLabel(pht('Order')) 161 + ->setName('order') 162 + ->setOptions($this->getOrderOptions()) 163 + ->setValue($saved->getParameter('order'))); 164 + 165 + } 166 + 167 + protected function getURI($path) { 168 + return '/differential/'.$path; 169 + } 170 + 171 + public function getBuiltinQueryNames() { 172 + $names = array(); 173 + 174 + if ($this->requireViewer()->isLoggedIn()) { 175 + $names['active'] = pht('Active Revisions'); 176 + } 177 + 178 + $names['all'] = pht('All Revisions'); 179 + 180 + return $names; 181 + } 182 + 183 + public function buildSavedQueryFromBuiltin($query_key) { 184 + $query = $this->newSavedQuery(); 185 + $query->setQueryKey($query_key); 186 + 187 + $viewer = $this->requireViewer(); 188 + 189 + switch ($query_key) { 190 + case 'active': 191 + return $query 192 + ->setParameter('responsiblePHIDs', array($viewer->getPHID())) 193 + ->setParameter('status', DifferentialRevisionQuery::STATUS_OPEN); 194 + case 'all': 195 + return $query; 196 + } 197 + 198 + return parent::buildSavedQueryFromBuiltin($query_key); 199 + } 200 + 201 + private function getStatusOptions() { 202 + return array( 203 + DifferentialRevisionQuery::STATUS_ANY => pht('All'), 204 + DifferentialRevisionQuery::STATUS_OPEN => pht('Open'), 205 + DifferentialRevisionQuery::STATUS_CLOSED => pht('Closed'), 206 + DifferentialRevisionQuery::STATUS_ABANDONED => pht('Abandoned'), 207 + ); 208 + } 209 + 210 + private function getOrderOptions() { 211 + return array( 212 + DifferentialRevisionQuery::ORDER_CREATED => pht('Created'), 213 + DifferentialRevisionQuery::ORDER_MODIFIED => pht('Updated'), 214 + ); 215 + } 216 + 217 + }
+43 -15
src/applications/differential/view/DifferentialRevisionListView.php
··· 11 11 private $handles; 12 12 private $fields; 13 13 private $highlightAge; 14 + private $header; 15 + private $noDataString; 16 + 17 + public function setNoDataString($no_data_string) { 18 + $this->noDataString = $no_data_string; 19 + return $this; 20 + } 21 + 22 + public function setHeader($header) { 23 + $this->header = $header; 24 + return $this; 25 + } 14 26 15 27 public function setFields(array $fields) { 16 28 assert_instances_of($fields, 'DifferentialFieldSpecification'); ··· 101 113 102 114 $list = new PhabricatorObjectItemListView(); 103 115 $list->setCards(true); 104 - $list->setFlush(true); 105 116 106 117 foreach ($this->revisions as $revision) { 107 118 $item = new PhabricatorObjectItemView(); ··· 116 127 } 117 128 if (array_key_exists($revision->getID(), $this->drafts)) { 118 129 $icons['draft'] = array( 119 - 'icon' => 'file-white', 120 - 'href' => '/D'.$revision->getID().'#comment-preview', 130 + 'icon' => 'file-grey', 121 131 ); 122 132 } 123 133 ··· 129 139 if ($stale && $modified < $stale) { 130 140 $days = floor((time() - $modified) / 60 / 60 / 24); 131 141 $icons['age'] = array( 132 - 'icon' => 'warning-white', 142 + 'icon' => 'warning-grey', 133 143 'label' => pht('Old (%d days)', $days), 134 144 ); 135 145 } else if ($fresh && $modified < $fresh) { 136 146 $days = floor((time() - $modified) / 60 / 60 / 24); 137 147 $icons['age'] = array( 138 - 'icon' => 'perflab-white', 148 + 'icon' => 'perflab-grey', 139 149 'label' => pht('Stale (%d days)', $days), 140 150 ); 141 151 } else { ··· 170 180 // Reviewers 171 181 $item->addAttribute(pht('Reviewers: %s', $rev_fields['Reviewers'])); 172 182 173 - $do_not_display_age = array( 174 - ArcanistDifferentialRevisionStatus::CLOSED => true, 175 - ArcanistDifferentialRevisionStatus::ABANDONED => true, 176 - ); 177 - if (isset($icons['age']) && !isset($do_not_display_age[$status])) { 178 - $item->addFootIcon($icons['age']['icon'], $icons['age']['label']); 183 + $item->setStateIconColumns(1); 184 + if ($this->highlightAge) { 185 + $item->setStateIconColumns(2); 186 + $do_not_display_age = array( 187 + ArcanistDifferentialRevisionStatus::CLOSED => true, 188 + ArcanistDifferentialRevisionStatus::ABANDONED => true, 189 + ); 190 + if (isset($icons['age']) && !isset($do_not_display_age[$status])) { 191 + $item->addStateIcon($icons['age']['icon'], $icons['age']['label']); 192 + } else { 193 + $item->addStateIcon('none'); 194 + } 179 195 } 196 + 180 197 if (isset($icons['draft'])) { 181 - $item->addFootIcon($icons['draft']['icon'], pht('Saved Draft'), 182 - $icons['draft']['href']); 198 + $item->addStateIcon( 199 + $icons['draft']['icon'], 200 + pht('Saved Comments')); 201 + } else { 202 + $item->addStateIcon('none'); 203 + } 204 + 205 + if ($flag_icon) { 206 + $item->addStateIcon($flag_icon, pht('Flagged')); 207 + } else { 208 + $item->addStateIcon('none'); 183 209 } 184 210 185 211 // Updated on 186 - $item->addIcon($flag_icon ? $flag_icon : 'none', 187 - hsprintf('%s', $rev_fields['Updated'])); 212 + $item->addIcon('none', $rev_fields['Updated']); 188 213 189 214 // First remove the fields we already have 190 215 $count = 7; ··· 198 223 199 224 $list->addItem($item); 200 225 } 226 + 227 + $list->setHeader($this->header); 228 + $list->setNoDataString($this->noDataString); 201 229 202 230 return $list; 203 231 }
+3 -2
src/view/layout/PhabricatorObjectItemView.php
··· 407 407 408 408 $state_icons = null; 409 409 if ($this->stateIconColumns) { 410 + $state_icon_list = array(); 410 411 foreach ($this->stateIcons as $state_icon) { 411 412 $sicon = id(new PHUIIconView()) 412 413 ->setSpriteSheet(PHUIIconView::SPRITE_ICONS) ··· 420 421 )); 421 422 } 422 423 423 - $icon_list[] = $sicon; 424 + $state_icon_list[] = $sicon; 424 425 } 425 426 $cols = $this->stateIconColumns; 426 427 $state_icons = phutil_tag( ··· 429 430 'class' => 'phabricator-object-item-state-icons '. 430 431 'state-icon-'.$cols.'-cols', 431 432 ), 432 - $icon_list); 433 + $state_icon_list); 433 434 } 434 435 435 436 $content = phutil_tag(
+9
webroot/rsrc/css/layout/phabricator-object-item-list-view.css
··· 10 10 padding: 20px; 11 11 } 12 12 13 + .phabricator-object-item-list-view + .phabricator-object-item-list-view { 14 + padding-top: 0; 15 + } 16 + 13 17 .phabricator-object-item-list-view.phabricator-object-list-flush { 14 18 padding: 0; 15 19 } ··· 122 126 .phabricator-object-item-grippable.phabricator-object-item-state-2-cols 123 127 .phabricator-object-item-frame { 124 128 padding-left: 55px; 129 + } 130 + 131 + .phabricator-object-item-list-header { 132 + padding: 0 0 8px 0; 133 + color: #555555; 125 134 } 126 135 127 136 /* - Item Actions --------------------------------------------------------------