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

Summary:
Ref T3721. Releeph currently attempts to implement a flexible, field-driven search for branches, but it's building all of its own infrastructure and it ends up heading down some weird paths. In particular, it loads **every** request and then makes calls into fields to filter them. It also tries to be very very general, which isn't really necessary (for example, I think it's reasonable for us to assume that we won't let you disable the "requestor" field).

ApplicationSearch and CustomField provide more scalable approaches to this problem; move search on top of them. The query still ends up doing some filtering in-process, but it's now far more limited in scope and can be denormalized later.

Test Plan: {F54304}

Reviewers: btrahan

Reviewed By: btrahan

CC: chad, aran

Maniphest Tasks: T3721

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

+399 -726
+45 -45
src/__celerity_resource_map__.php
··· 854 854 ), 855 855 'aphront-form-view-css' => 856 856 array( 857 - 'uri' => '/res/7793ddd1/rsrc/css/aphront/form-view.css', 857 + 'uri' => '/res/1be2545a/rsrc/css/aphront/form-view.css', 858 858 'type' => 'css', 859 859 'requires' => 860 860 array( ··· 4172 4172 ), array( 4173 4173 'packages' => 4174 4174 array( 4175 - '566968f8' => 4175 + '9737ebe0' => 4176 4176 array( 4177 4177 'name' => 'core.pkg.css', 4178 4178 'symbols' => ··· 4220 4220 40 => 'phabricator-property-list-view-css', 4221 4221 41 => 'phabricator-tag-view-css', 4222 4222 ), 4223 - 'uri' => '/res/pkg/566968f8/core.pkg.css', 4223 + 'uri' => '/res/pkg/9737ebe0/core.pkg.css', 4224 4224 'type' => 'css', 4225 4225 ), 4226 4226 '4f81c788' => ··· 4411 4411 ), 4412 4412 'reverse' => 4413 4413 array( 4414 - 'aphront-dialog-view-css' => '566968f8', 4415 - 'aphront-error-view-css' => '566968f8', 4416 - 'aphront-form-view-css' => '566968f8', 4417 - 'aphront-list-filter-view-css' => '566968f8', 4418 - 'aphront-pager-view-css' => '566968f8', 4419 - 'aphront-panel-view-css' => '566968f8', 4420 - 'aphront-table-view-css' => '566968f8', 4421 - 'aphront-tokenizer-control-css' => '566968f8', 4422 - 'aphront-tooltip-css' => '566968f8', 4423 - 'aphront-typeahead-control-css' => '566968f8', 4414 + 'aphront-dialog-view-css' => '9737ebe0', 4415 + 'aphront-error-view-css' => '9737ebe0', 4416 + 'aphront-form-view-css' => '9737ebe0', 4417 + 'aphront-list-filter-view-css' => '9737ebe0', 4418 + 'aphront-pager-view-css' => '9737ebe0', 4419 + 'aphront-panel-view-css' => '9737ebe0', 4420 + 'aphront-table-view-css' => '9737ebe0', 4421 + 'aphront-tokenizer-control-css' => '9737ebe0', 4422 + 'aphront-tooltip-css' => '9737ebe0', 4423 + 'aphront-typeahead-control-css' => '9737ebe0', 4424 4424 'differential-changeset-view-css' => '09216861', 4425 4425 'differential-core-view-css' => '09216861', 4426 4426 'differential-inline-comment-editor' => 'd07a3bc2', ··· 4434 4434 'differential-table-of-contents-css' => '09216861', 4435 4435 'diffusion-commit-view-css' => 'c8ce2d88', 4436 4436 'diffusion-icons-css' => 'c8ce2d88', 4437 - 'global-drag-and-drop-css' => '566968f8', 4437 + 'global-drag-and-drop-css' => '9737ebe0', 4438 4438 'inline-comment-summary-css' => '09216861', 4439 4439 'javelin-aphlict' => '4f81c788', 4440 4440 'javelin-behavior' => '2dbbb7d1', ··· 4507 4507 'javelin-util' => '2dbbb7d1', 4508 4508 'javelin-vector' => '2dbbb7d1', 4509 4509 'javelin-workflow' => '2dbbb7d1', 4510 - 'lightbox-attachment-css' => '566968f8', 4510 + 'lightbox-attachment-css' => '9737ebe0', 4511 4511 'maniphest-task-summary-css' => '06bacb9a', 4512 4512 'maniphest-transaction-detail-css' => '06bacb9a', 4513 - 'phabricator-action-list-view-css' => '566968f8', 4514 - 'phabricator-application-launch-view-css' => '566968f8', 4513 + 'phabricator-action-list-view-css' => '9737ebe0', 4514 + 'phabricator-application-launch-view-css' => '9737ebe0', 4515 4515 'phabricator-busy' => '4f81c788', 4516 4516 'phabricator-content-source-view-css' => '09216861', 4517 - 'phabricator-core-css' => '566968f8', 4518 - 'phabricator-crumbs-view-css' => '566968f8', 4517 + 'phabricator-core-css' => '9737ebe0', 4518 + 'phabricator-crumbs-view-css' => '9737ebe0', 4519 4519 'phabricator-drag-and-drop-file-upload' => 'd07a3bc2', 4520 4520 'phabricator-dropdown-menu' => '4f81c788', 4521 4521 'phabricator-file-upload' => '4f81c788', 4522 - 'phabricator-filetree-view-css' => '566968f8', 4523 - 'phabricator-flag-css' => '566968f8', 4524 - 'phabricator-form-view-css' => '566968f8', 4525 - 'phabricator-header-view-css' => '566968f8', 4522 + 'phabricator-filetree-view-css' => '9737ebe0', 4523 + 'phabricator-flag-css' => '9737ebe0', 4524 + 'phabricator-form-view-css' => '9737ebe0', 4525 + 'phabricator-header-view-css' => '9737ebe0', 4526 4526 'phabricator-hovercard' => '4f81c788', 4527 - 'phabricator-jump-nav' => '566968f8', 4527 + 'phabricator-jump-nav' => '9737ebe0', 4528 4528 'phabricator-keyboard-shortcut' => '4f81c788', 4529 4529 'phabricator-keyboard-shortcut-manager' => '4f81c788', 4530 - 'phabricator-main-menu-view' => '566968f8', 4530 + 'phabricator-main-menu-view' => '9737ebe0', 4531 4531 'phabricator-menu-item' => '4f81c788', 4532 - 'phabricator-nav-view-css' => '566968f8', 4532 + 'phabricator-nav-view-css' => '9737ebe0', 4533 4533 'phabricator-notification' => '4f81c788', 4534 - 'phabricator-notification-css' => '566968f8', 4535 - 'phabricator-notification-menu-css' => '566968f8', 4536 - 'phabricator-object-item-list-view-css' => '566968f8', 4534 + 'phabricator-notification-css' => '9737ebe0', 4535 + 'phabricator-notification-menu-css' => '9737ebe0', 4536 + 'phabricator-object-item-list-view-css' => '9737ebe0', 4537 4537 'phabricator-object-selector-css' => '09216861', 4538 4538 'phabricator-phtize' => '4f81c788', 4539 4539 'phabricator-prefab' => '4f81c788', 4540 4540 'phabricator-project-tag-css' => '06bacb9a', 4541 - 'phabricator-property-list-view-css' => '566968f8', 4542 - 'phabricator-remarkup-css' => '566968f8', 4541 + 'phabricator-property-list-view-css' => '9737ebe0', 4542 + 'phabricator-remarkup-css' => '9737ebe0', 4543 4543 'phabricator-shaped-request' => 'd07a3bc2', 4544 - 'phabricator-side-menu-view-css' => '566968f8', 4545 - 'phabricator-standard-page-view' => '566968f8', 4546 - 'phabricator-tag-view-css' => '566968f8', 4544 + 'phabricator-side-menu-view-css' => '9737ebe0', 4545 + 'phabricator-standard-page-view' => '9737ebe0', 4546 + 'phabricator-tag-view-css' => '9737ebe0', 4547 4547 'phabricator-textareautils' => '4f81c788', 4548 4548 'phabricator-tooltip' => '4f81c788', 4549 - 'phabricator-transaction-view-css' => '566968f8', 4550 - 'phabricator-zindex-css' => '566968f8', 4551 - 'phui-button-css' => '566968f8', 4552 - 'phui-form-css' => '566968f8', 4553 - 'phui-icon-view-css' => '566968f8', 4554 - 'phui-spacing-css' => '566968f8', 4555 - 'sprite-apps-large-css' => '566968f8', 4556 - 'sprite-gradient-css' => '566968f8', 4557 - 'sprite-icons-css' => '566968f8', 4558 - 'sprite-menu-css' => '566968f8', 4559 - 'syntax-highlighting-css' => '566968f8', 4549 + 'phabricator-transaction-view-css' => '9737ebe0', 4550 + 'phabricator-zindex-css' => '9737ebe0', 4551 + 'phui-button-css' => '9737ebe0', 4552 + 'phui-form-css' => '9737ebe0', 4553 + 'phui-icon-view-css' => '9737ebe0', 4554 + 'phui-spacing-css' => '9737ebe0', 4555 + 'sprite-apps-large-css' => '9737ebe0', 4556 + 'sprite-gradient-css' => '9737ebe0', 4557 + 'sprite-icons-css' => '9737ebe0', 4558 + 'sprite-menu-css' => '9737ebe0', 4559 + 'syntax-highlighting-css' => '9737ebe0', 4560 4560 ), 4561 4561 ));
+7 -5
src/__phutil_library_map__.php
··· 32 32 'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php', 33 33 'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php', 34 34 'AphrontFormControl' => 'view/form/control/AphrontFormControl.php', 35 - 'AphrontFormCountedToggleButtonsControl' => 'view/form/control/AphrontFormCountedToggleButtonsControl.php', 36 35 'AphrontFormCropControl' => 'view/form/control/AphrontFormCropControl.php', 37 36 'AphrontFormDateControl' => 'view/form/control/AphrontFormDateControl.php', 38 37 'AphrontFormDividerControl' => 'view/form/control/AphrontFormDividerControl.php', ··· 1020 1019 'PhabricatorCountdownSearchEngine' => 'applications/countdown/query/PhabricatorCountdownSearchEngine.php', 1021 1020 'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php', 1022 1021 'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php', 1023 - 'PhabricatorCountedToggleButtonsExample' => 'applications/uiexample/examples/PhabricatorCountedToggleButtonsExample.php', 1024 1022 'PhabricatorCrumbView' => 'view/layout/PhabricatorCrumbView.php', 1025 1023 'PhabricatorCrumbsView' => 'view/layout/PhabricatorCrumbsView.php', 1026 1024 'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php', ··· 1996 1994 'ReleephRequestMailReceiver' => 'applications/releeph/mail/ReleephRequestMailReceiver.php', 1997 1995 'ReleephRequestQuery' => 'applications/releeph/query/ReleephRequestQuery.php', 1998 1996 'ReleephRequestReplyHandler' => 'applications/releeph/mail/ReleephRequestReplyHandler.php', 1997 + 'ReleephRequestSearchEngine' => 'applications/releeph/query/ReleephRequestSearchEngine.php', 1999 1998 'ReleephRequestStatus' => 'applications/releeph/constants/ReleephRequestStatus.php', 2000 1999 'ReleephRequestStatusView' => 'applications/releeph/view/request/ReleephRequestStatusView.php', 2001 2000 'ReleephRequestTransaction' => 'applications/releeph/storage/ReleephRequestTransaction.php', ··· 2062 2061 'AphrontFileResponse' => 'AphrontResponse', 2063 2062 'AphrontFormCheckboxControl' => 'AphrontFormControl', 2064 2063 'AphrontFormControl' => 'AphrontView', 2065 - 'AphrontFormCountedToggleButtonsControl' => 'AphrontFormControl', 2066 2064 'AphrontFormCropControl' => 'AphrontFormControl', 2067 2065 'AphrontFormDateControl' => 'AphrontFormControl', 2068 2066 'AphrontFormDividerControl' => 'AphrontFormControl', ··· 3088 3086 'PhabricatorCountdownSearchEngine' => 'PhabricatorApplicationSearchEngine', 3089 3087 'PhabricatorCountdownView' => 'AphrontTagView', 3090 3088 'PhabricatorCountdownViewController' => 'PhabricatorCountdownController', 3091 - 'PhabricatorCountedToggleButtonsExample' => 'PhabricatorUIExample', 3092 3089 'PhabricatorCrumbView' => 'AphrontView', 3093 3090 'PhabricatorCrumbsView' => 'AphrontView', 3094 3091 'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery', ··· 4129 4126 'ReleephBranchNamePreviewController' => 'ReleephController', 4130 4127 'ReleephBranchPreviewView' => 'AphrontFormControl', 4131 4128 'ReleephBranchQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 4132 - 'ReleephBranchViewController' => 'ReleephProjectController', 4129 + 'ReleephBranchViewController' => 4130 + array( 4131 + 0 => 'ReleephProjectController', 4132 + 1 => 'PhabricatorApplicationSearchResultsControllerInterface', 4133 + ), 4133 4134 'ReleephCommitFinderException' => 'Exception', 4134 4135 'ReleephCommitMessageFieldSpecification' => 'ReleephFieldSpecification', 4135 4136 'ReleephController' => 'PhabricatorController', ··· 4190 4191 'ReleephRequestMailReceiver' => 'PhabricatorObjectMailReceiver', 4191 4192 'ReleephRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 4192 4193 'ReleephRequestReplyHandler' => 'PhabricatorMailReplyHandler', 4194 + 'ReleephRequestSearchEngine' => 'PhabricatorApplicationSearchEngine', 4193 4195 'ReleephRequestStatusView' => 'AphrontView', 4194 4196 'ReleephRequestTransaction' => 'PhabricatorApplicationTransaction', 4195 4197 'ReleephRequestTransactionComment' => 'PhabricatorApplicationTransactionComment',
+1 -1
src/applications/releeph/application/PhabricatorApplicationReleeph.php
··· 75 75 76 76 // Branch navigation made pretty, as it's the most common: 77 77 '(?P<projectName>[^/]+)/(?P<branchName>[^/]+)/' => array( 78 - '' => 'ReleephBranchViewController', 78 + '(?:query/(?P<queryKey>[^/]+)/)?' => 'ReleephBranchViewController', 79 79 'edit/' => 'ReleephBranchEditController', 80 80 'request/' => 'ReleephRequestEditController', 81 81 '(?P<action>close|re-open)/' => 'ReleephBranchAccessController',
+64 -54
src/applications/releeph/controller/branch/ReleephBranchViewController.php
··· 1 1 <?php 2 2 3 - final class ReleephBranchViewController extends ReleephProjectController { 3 + final class ReleephBranchViewController extends ReleephProjectController 4 + implements PhabricatorApplicationSearchResultsControllerInterface { 4 5 5 - public function processRequest() { 6 - $request = $this->getRequest(); 6 + private $queryKey; 7 7 8 - $releeph_branch = $this->getReleephBranch(); 9 - $releeph_project = $this->getReleephProject(); 10 - $all_releeph_requests = $releeph_branch->loadReleephRequests( 11 - $request->getUser()); 8 + public function shouldAllowPublic() { 9 + return true; 10 + } 12 11 13 - $selector = $releeph_project->getReleephFieldSelector(); 14 - $fields = $selector->arrangeFieldsForSelectForm( 15 - $selector->getFieldSpecifications()); 12 + public function willProcessRequest(array $data) { 13 + parent::willProcessRequest($data); 14 + $this->queryKey = idx($data, 'queryKey'); 15 + } 16 16 17 - $form = id(new AphrontFormView()) 18 - ->setMethod('GET') 19 - ->setUser($request->getUser()); 20 17 21 - $filtered_releeph_requests = $all_releeph_requests; 22 - foreach ($fields as $field) { 23 - $all_releeph_requests_without_this_field = $all_releeph_requests; 24 - foreach ($fields as $other_field) { 25 - if ($other_field != $field) { 26 - $other_field->selectReleephRequestsHook( 27 - $request, 28 - $all_releeph_requests_without_this_field); 18 + public function processRequest() { 19 + $request = $this->getRequest(); 29 20 30 - } 31 - } 21 + $controller = id(new PhabricatorApplicationSearchController($request)) 22 + ->setQueryKey($this->queryKey) 23 + ->setSearchEngine($this->getSearchEngine()) 24 + ->setNavigation($this->buildSideNavView()); 32 25 33 - $field->appendSelectControlsHook( 34 - $form, 35 - $request, 36 - $all_releeph_requests, 37 - $all_releeph_requests_without_this_field); 26 + return $this->delegateToController($controller); 27 + } 38 28 39 - $field->selectReleephRequestsHook( 40 - $request, 41 - $filtered_releeph_requests); 42 - } 29 + public function renderResultsList( 30 + array $requests, 31 + PhabricatorSavedQuery $query) { 43 32 44 - $form->appendChild( 45 - id(new AphrontFormSubmitControl()) 46 - ->setValue(pht('Filter'))); 33 + assert_instances_of($requests, 'ReleephRequest'); 34 + $viewer = $this->getRequest()->getUser(); 35 + 36 + $releeph_branch = $this->getReleephBranch(); 37 + $releeph_project = $this->getReleephProject(); 38 + 39 + // TODO: Really gross. 40 + $releeph_branch->populateReleephRequestHandles( 41 + $viewer, 42 + $requests); 47 43 48 44 $list = id(new ReleephRequestHeaderListView()) 49 45 ->setOriginType('branch') 50 - ->setUser($request->getUser()) 46 + ->setUser($viewer) 51 47 ->setAphrontRequest($this->getRequest()) 52 48 ->setReleephProject($releeph_project) 53 49 ->setReleephBranch($releeph_branch) 54 - ->setReleephRequests($filtered_releeph_requests); 50 + ->setReleephRequests($requests); 51 + 52 + return $list; 53 + } 54 + 55 + public function buildSideNavView($for_app = false) { 56 + $user = $this->getRequest()->getUser(); 57 + 58 + $nav = new AphrontSideNavFilterView(); 59 + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); 60 + 61 + 62 + $this->getSearchEngine()->addNavigationItems($nav->getMenu()); 63 + 64 + $nav->selectFilter(null); 65 + 66 + return $nav; 67 + } 68 + 69 + private function getSearchEngine() { 70 + $branch = $this->getReleephBranch(); 71 + return id(new ReleephRequestSearchEngine()) 72 + ->setBranch($branch) 73 + ->setBaseURI($branch->getURI()) 74 + ->setViewer($this->getRequest()->getUser()); 75 + } 55 76 56 - $filter = id(new AphrontListFilterView()) 57 - ->appendChild($form); 77 + public function buildApplicationCrumbs() { 78 + $releeph_branch = $this->getReleephBranch(); 79 + $releeph_project = $this->getReleephProject(); 58 80 59 - $crumbs = $this->buildApplicationCrumbs() 81 + $crumbs = parent::buildApplicationCrumbs() 60 82 ->addCrumb( 61 83 id(new PhabricatorCrumbView()) 62 84 ->setName($releeph_project->getName()) ··· 66 88 ->setName($releeph_branch->getDisplayNameWithDetail()) 67 89 ->setHref($releeph_branch->getURI())); 68 90 69 - // Don't show the request button for inactive (closed) branches 70 91 if ($releeph_branch->isActive()) { 71 92 $create_uri = $releeph_branch->getURI('request/'); 72 93 $crumbs->addAction( ··· 76 97 ->setIcon('create')); 77 98 } 78 99 79 - return $this->buildStandardPageResponse( 80 - array( 81 - $crumbs, 82 - $filter, 83 - $list 84 - ), 85 - array( 86 - 'title' => 87 - $releeph_project->getName(). 88 - ' - '. 89 - $releeph_branch->getDisplayName(). 90 - ' requests' 91 - )); 100 + return $crumbs; 92 101 } 102 + 93 103 94 104 }
-177
src/applications/releeph/field/specification/ReleephFieldSpecification.php
··· 45 45 return $this->getStorageKey() !== null; 46 46 } 47 47 48 - /** 49 - * This will be called many times if you are using **Selecting**. In 50 - * particular, for N selecting fields, selectReleephRequests() is called 51 - * N-squared times, each time for R ReleephRequests. 52 - */ 53 48 final public function getValue() { 54 49 if ($this->requestValue !== null) { 55 50 return $this->requestValue; ··· 174 169 public function bulkLoad(array $releeph_requests) { 175 170 } 176 171 177 - 178 - /* -( Selecting )---------------------------------------------------------- */ 179 - 180 - /** 181 - * Append select controls to the given form. 182 - * 183 - * You are given: 184 - * 185 - * - the AphrontFormView to append to; 186 - * 187 - * - the AphrontRequest, so you can make use of the value currently selected 188 - * in the form; 189 - * 190 - * - $all_releeph_requests: an array of all the ReleephRequests without any 191 - * selection based filtering; and 192 - * 193 - * - $all_releeph_requests_without_this_field: an array of ReleephRequests 194 - * that have been selected by all the other select controls on this page. 195 - * 196 - * The example in ReleephLevelFieldSpecification shows how to use these. 197 - * $all_releeph_requests lets you find out all the values of a field in all 198 - * ReleephRequests, so you can render controls for every known value. 199 - * 200 - * $all_releeph_requests_without_this_field lets you count how many 201 - * ReleephRequests could be affected by this field's select control, after 202 - * all the other fields have made their selections. 203 - * ReleephLevelFieldSpecification uses this to render a preview count for 204 - * each select button, and disables the button completely (but still renders 205 - * it) if it couldn't possibly select anything. 206 - */ 207 - protected function appendSelectControls( 208 - AphrontFormView $form, 209 - AphrontRequest $request, 210 - array $all_releeph_requests, 211 - array $all_releeph_requests_without_this_field) { 212 - 213 - return null; 214 - } 215 - 216 - /** 217 - * Filter the $releeph_requests using the data you set with your form 218 - * controls, and which is now available in the provided AphrontRequest. 219 - */ 220 - protected function selectReleephRequests(AphrontRequest $request, 221 - array &$releeph_requests) { 222 - return null; 223 - } 224 - 225 - /** 226 - * If you have PHIDs that can be used in an AphrontFormTokenizerControl, 227 - * return true here, return the PHIDs in getSelectablePHIDs(), and return the 228 - * URL the Tokenizer should use for the form control in 229 - * getSelectTokenizerDatasource(). 230 - * 231 - * This is a cheap alternative to implementing appendSelectControls() and 232 - * selectReleephRequests() in full. 233 - */ 234 - protected function hasSelectablePHIDs() { 235 - return false; 236 - } 237 - 238 - protected function getSelectablePHIDs() { 239 - throw new ReleephFieldSpecificationIncompleteException($this); 240 - } 241 - 242 - protected function getSelectTokenizerDatasource() { 243 - throw new ReleephFieldSpecificationIncompleteException($this); 244 - } 245 - 246 - 247 172 /* -( Commit Messages )---------------------------------------------------- */ 248 173 249 174 public function shouldAppearOnCommitMessage() { ··· 326 251 327 252 final public function shouldUseMarkupCache($field) { 328 253 return true; 329 - } 330 - 331 - 332 - /* -( Implementation )----------------------------------------------------- */ 333 - 334 - /** 335 - * The "hook" functions ##appendSelectControlsHook()## and 336 - * ##selectReleephRequestsHook()## are used with ##hasSelectablePHIDs()##, to 337 - * use the tokenizing helpers if ##hasSelectablePHIDs()## returns true. 338 - */ 339 - public function appendSelectControlsHook( 340 - AphrontFormView $form, 341 - AphrontRequest $request, 342 - array $all_releeph_requests, 343 - array $all_releeph_requests_without_this_field) { 344 - 345 - if ($this->hasSelectablePHIDs()) { 346 - $this->appendTokenizingSelectControl( 347 - $form, 348 - $request, 349 - $all_releeph_requests, 350 - $all_releeph_requests_without_this_field); 351 - } else { 352 - $this->appendSelectControls( 353 - $form, 354 - $request, 355 - $all_releeph_requests, 356 - $all_releeph_requests_without_this_field); 357 - } 358 - } 359 - 360 - // See above 361 - public function selectReleephRequestsHook(AphrontRequest $request, 362 - array &$releeph_requests) { 363 - 364 - if ($this->hasSelectablePHIDs()) { 365 - $this->selectReleephRequestsFromTokens( 366 - $request, 367 - $releeph_requests); 368 - } else { 369 - $this->selectReleephRequests( 370 - $request, 371 - $releeph_requests); 372 - } 373 - } 374 - 375 - private function appendTokenizingSelectControl( 376 - AphrontFormView $form, 377 - AphrontRequest $request, 378 - array $all_releeph_requests, 379 - array $all_releeph_requests_without_this_field) { 380 - 381 - $key = urlencode(strtolower($this->getName())); 382 - $selected_phids = $request->getArr($key); 383 - $handles = id(new PhabricatorObjectHandleData($selected_phids)) 384 - ->setViewer($request->getUser()) 385 - ->loadHandles(); 386 - 387 - $tokens = array(); 388 - foreach ($selected_phids as $phid) { 389 - $tokens[$phid] = $handles[$phid]->getFullName(); 390 - } 391 - 392 - $datasource = $this->getSelectTokenizerDatasource(); 393 - $control = 394 - id(new AphrontFormTokenizerControl()) 395 - ->setDatasource($datasource) 396 - ->setName($key) 397 - ->setLabel($this->getName()) 398 - ->setValue($tokens); 399 - 400 - $form->appendChild($control); 401 - } 402 - 403 - private function selectReleephRequestsFromTokens(AphrontRequest $request, 404 - array &$releeph_requests) { 405 - 406 - $key = urlencode(strtolower($this->getName())); 407 - $selected_phids = $request->getArr($key); 408 - if (!$selected_phids) { 409 - return; 410 - } 411 - 412 - $selected_phid_lookup = array(); 413 - foreach ($selected_phids as $phid) { 414 - $selected_phid_lookup[$phid] = $phid; 415 - } 416 - 417 - $filtered = array(); 418 - foreach ($releeph_requests as $releeph_request) { 419 - $rq_phids = $this 420 - ->setReleephRequest($releeph_request) 421 - ->getSelectablePHIDs(); 422 - foreach ($rq_phids as $rq_phid) { 423 - if (idx($selected_phid_lookup, $rq_phid)) { 424 - $filtered[] = $releeph_request; 425 - break; 426 - } 427 - } 428 - } 429 - 430 - $releeph_requests = $filtered; 431 254 } 432 255 433 256 }
+6 -90
src/applications/releeph/field/specification/ReleephLevelFieldSpecification.php
··· 19 19 abstract public function getNameForLevel($level); 20 20 abstract public function getDescriptionForLevel($level); 21 21 22 - /** 23 - * Use getCanonicalLevel() to convert old, unsupported levels to new ones. 24 - */ 25 - protected function getCanonicalLevel($misc_level) { 26 - return $misc_level; 27 - } 28 - 29 22 public function getStorageKey() { 30 23 $class = get_class($this); 31 24 throw new ReleephFieldSpecificationIncompleteException( ··· 34 27 } 35 28 36 29 public function renderValueForHeaderView() { 37 - $raw_level = $this->getValue(); 38 - $level = $this->getCanonicalLevel($raw_level); 39 - return $this->getNameForLevel($level); 30 + return $this->getNameForLevel($this->getValue()); 40 31 } 41 32 42 33 public function renderEditControl() { 43 34 $control_name = $this->getRequiredStorageKey(); 44 35 $all_levels = $this->getLevels(); 45 36 46 - $level = $this->getCanonicalLevel($this->getValue()); 37 + $level = $request->getStr($control_name); 38 + if (!$level) { 39 + $level = $this->getValue(); 40 + } 41 + 47 42 if (!$level) { 48 43 $level = $this->getDefaultLevel(); 49 44 } ··· 139 134 } 140 135 } 141 136 return idx($this->nameMap, $name); 142 - } 143 - 144 - protected function appendSelectControls( 145 - AphrontFormView $form, 146 - AphrontRequest $request, 147 - array $all_releeph_requests, 148 - array $all_releeph_requests_without_this_field) { 149 - 150 - $buttons = array(null => 'All'); 151 - 152 - // Add in known level/names 153 - foreach ($this->getLevels() as $level) { 154 - $name = $this->getNameForLevel($level); 155 - $buttons[$name] = $name; 156 - } 157 - 158 - // Add in any names we've seen in the wild, as well. 159 - foreach ($all_releeph_requests as $releeph_request) { 160 - $raw_level = $this->setReleephRequest($releeph_request)->getValue(); 161 - if (!$raw_level) { 162 - // The ReleephRequest might not have a level set 163 - continue; 164 - } 165 - $level = $this->getCanonicalLevel($raw_level); 166 - $name = $this->getNameForLevel($level); 167 - $buttons[$name] = $name; 168 - } 169 - 170 - $key = $this->getRequiredStorageKey(); 171 - $current = $request->getStr($key); 172 - 173 - $counters = array(null => count($all_releeph_requests_without_this_field)); 174 - foreach ($all_releeph_requests_without_this_field as $releeph_request) { 175 - $raw_level = $this->setReleephRequest($releeph_request)->getValue(); 176 - if (!$raw_level) { 177 - // The ReleephRequest might not have a level set 178 - continue; 179 - } 180 - $level = $this->getCanonicalLevel($raw_level); 181 - $name = $this->getNameForLevel($level); 182 - 183 - if (!isset($counters[$name])) { 184 - $counters[$name] = 0; 185 - } 186 - $counters[$name]++; 187 - } 188 - 189 - $control = id(new AphrontFormCountedToggleButtonsControl()) 190 - ->setLabel($this->getName()) 191 - ->setValue($current) 192 - ->setBaseURI($request->getRequestURI(), $key) 193 - ->setButtons($buttons) 194 - ->setCounters($counters); 195 - 196 - $form 197 - ->appendChild($control) 198 - ->addHiddenInput($key, $current); 199 - } 200 - 201 - protected function selectReleephRequests(AphrontRequest $request, 202 - array &$releeph_requests) { 203 - $key = $this->getRequiredStorageKey(); 204 - $current = $request->getStr($key); 205 - 206 - if (!$current) { 207 - return; 208 - } 209 - 210 - $filtered = array(); 211 - foreach ($releeph_requests as $releeph_request) { 212 - $raw_level = $this->setReleephRequest($releeph_request)->getValue(); 213 - $level = $this->getCanonicalLevel($raw_level); 214 - $name = $this->getNameForLevel($level); 215 - if ($name == $current) { 216 - $filtered[] = $releeph_request; 217 - } 218 - } 219 - 220 - $releeph_requests = $filtered; 221 137 } 222 138 223 139 }
-14
src/applications/releeph/field/specification/ReleephRequestorFieldSpecification.php
··· 26 26 ->render(); 27 27 } 28 28 29 - public function hasSelectablePHIDs() { 30 - return true; 31 - } 32 - 33 - public function getSelectTokenizerDatasource() { 34 - return '/typeahead/common/users/'; 35 - } 36 - 37 - public function getSelectablePHIDs() { 38 - return array( 39 - $this->getReleephRequest()->getRequestUserPHID(), 40 - ); 41 - } 42 - 43 29 public function shouldAppearOnCommitMessage() { 44 30 return true; 45 31 }
-73
src/applications/releeph/field/specification/ReleephStatusFieldSpecification.php
··· 17 17 ->render(); 18 18 } 19 19 20 - private static $filters = array( 21 - 'req' => ReleephRequestStatus::STATUS_REQUESTED, 22 - 'app' => ReleephRequestStatus::STATUS_NEEDS_PICK, 23 - 'rej' => ReleephRequestStatus::STATUS_REJECTED, 24 - 'abn' => ReleephRequestStatus::STATUS_ABANDONED, 25 - 'mer' => ReleephRequestStatus::STATUS_PICKED, 26 - 'rrq' => ReleephRequestStatus::STATUS_NEEDS_REVERT, 27 - 'rev' => ReleephRequestStatus::STATUS_REVERTED, 28 - ); 29 - 30 - protected function appendSelectControls( 31 - AphrontFormView $form, 32 - AphrontRequest $request, 33 - array $all_releeph_requests, 34 - array $all_releeph_requests_without_this_field) { 35 - 36 - $filter_names = array( 37 - null => 'All', 38 - ); 39 - 40 - foreach (self::$filters as $code => $status) { 41 - $name = ReleephRequestStatus::getStatusDescriptionFor($status); 42 - $filter_names[$code] = $name; 43 - } 44 - 45 - $key = 'status'; 46 - $code = $request->getStr($key); 47 - $current_status = idx(self::$filters, $code); 48 - 49 - $codes = array_flip(self::$filters); 50 - 51 - $counters = array(null => count($all_releeph_requests_without_this_field)); 52 - foreach ($all_releeph_requests_without_this_field as $releeph_request) { 53 - $this_status = $releeph_request->getStatus(); 54 - $this_code = idx($codes, $this_status); 55 - if (!isset($counters[$this_code])) { 56 - $counters[$this_code] = 0; 57 - } 58 - $counters[$this_code]++; 59 - } 60 - 61 - $control = id(new AphrontFormCountedToggleButtonsControl()) 62 - ->setLabel($this->getName()) 63 - ->setValue($code) 64 - ->setBaseURI($request->getRequestURI(), $key) 65 - ->setButtons($filter_names) 66 - ->setCounters($counters); 67 - 68 - $form 69 - ->appendChild($control) 70 - ->addHiddenInput($key, $code); 71 - } 72 - 73 - protected function selectReleephRequests(AphrontRequest $request, 74 - array &$releeph_requests) { 75 - 76 - $key = 'status'; 77 - $code = $request->getStr($key); 78 - if (!$code) { 79 - return; 80 - } 81 - 82 - $current_status = idx(self::$filters, $code); 83 - 84 - $filtered = array(); 85 - foreach ($releeph_requests as $releeph_request) { 86 - if ($releeph_request->getStatus() == $current_status) { 87 - $filtered[] = $releeph_request; 88 - } 89 - } 90 - $releeph_requests = $filtered; 91 - } 92 - 93 20 }
+118
src/applications/releeph/query/ReleephRequestQuery.php
··· 7 7 private $commitToRevMap; 8 8 private $ids; 9 9 private $phids; 10 + private $severities; 11 + private $requestorPHIDs; 12 + private $branchIDs; 13 + 14 + const STATUS_ALL = 'status-all'; 15 + const STATUS_OPEN = 'status-open'; 16 + const STATUS_REQUESTED = 'status-requested'; 17 + const STATUS_NEEDS_PULL = 'status-needs-pull'; 18 + const STATUS_REJECTED = 'status-rejected'; 19 + const STATUS_ABANDONED = 'status-abandoned'; 20 + const STATUS_PULLED = 'status-pulled'; 21 + const STATUS_NEEDS_REVERT = 'status-needs-revert'; 22 + const STATUS_REVERTED = 'status-reverted'; 23 + 24 + private $status = self::STATUS_ALL; 10 25 11 26 public function withIDs(array $ids) { 12 27 $this->ids = $ids; ··· 18 33 return $this; 19 34 } 20 35 36 + public function withBranchIDs(array $branch_ids) { 37 + $this->branchIDs = $branch_ids; 38 + return $this; 39 + } 40 + 21 41 public function getRevisionPHID($commit_phid) { 22 42 if ($this->commitToRevMap) { 23 43 return idx($this->commitToRevMap, $commit_phid, null); ··· 26 46 return null; 27 47 } 28 48 49 + public function withStatus($status) { 50 + $this->status = $status; 51 + return $this; 52 + } 53 + 29 54 public function withRequestedCommitPHIDs(array $requested_commit_phids) { 30 55 $this->requestedCommitPHIDs = $requested_commit_phids; 31 56 return $this; 32 57 } 33 58 59 + public function withRequestorPHIDs(array $phids) { 60 + $this->requestorPHIDs = $phids; 61 + return $this; 62 + } 63 + 64 + public function withSeverities(array $severities) { 65 + $this->severities = $severities; 66 + return $this; 67 + } 68 + 34 69 public function withRevisionPHIDs(array $revision_phids) { 35 70 $type = PhabricatorEdgeConfig::TYPE_DREV_HAS_COMMIT; 36 71 ··· 65 100 return $table->loadAllFromArray($data); 66 101 } 67 102 103 + public function willFilterPage(array $requests) { 104 + 105 + // TODO: These should be serviced by the query, but are not currently 106 + // denormalized anywhere. For now, filter them here instead. 107 + 108 + $keep_status = array_fuse($this->getKeepStatusConstants()); 109 + if ($keep_status) { 110 + foreach ($requests as $key => $request) { 111 + if (empty($keep_status[$request->getStatus()])) { 112 + unset($requests[$key]); 113 + } 114 + } 115 + } 116 + 117 + if ($this->severities) { 118 + $severities = array_fuse($this->severities); 119 + foreach ($requests as $key => $request) { 120 + if (empty($severities[$request->getDetail('releeph:severity')])) { 121 + unset($requests[$key]); 122 + } 123 + } 124 + } 125 + 126 + return $requests; 127 + } 128 + 68 129 private function buildWhereClause(AphrontDatabaseConnection $conn_r) { 69 130 $where = array(); 70 131 ··· 82 143 $this->phids); 83 144 } 84 145 146 + if ($this->branchIDs) { 147 + $where[] = qsprintf( 148 + $conn_r, 149 + 'branchID IN (%Ld)', 150 + $this->branchIDs); 151 + } 152 + 85 153 if ($this->requestedCommitPHIDs) { 86 154 $where[] = qsprintf( 87 155 $conn_r, ··· 89 157 $this->requestedCommitPHIDs); 90 158 } 91 159 160 + if ($this->requestorPHIDs) { 161 + $where[] = qsprintf( 162 + $conn_r, 163 + 'requestUserPHID IN (%Ls)', 164 + $this->requestorPHIDs); 165 + } 166 + 92 167 $where[] = $this->buildPagingClause($conn_r); 93 168 94 169 return $this->formatWhereClause($where); 170 + } 171 + 172 + private function getKeepStatusConstants() { 173 + switch ($this->status) { 174 + case self::STATUS_ALL: 175 + return array(); 176 + case self::STATUS_OPEN: 177 + return array( 178 + ReleephRequestStatus::STATUS_REQUESTED, 179 + ReleephRequestStatus::STATUS_NEEDS_PICK, 180 + ReleephRequestStatus::STATUS_NEEDS_REVERT, 181 + ); 182 + case self::STATUS_REQUESTED: 183 + return array( 184 + ReleephRequestStatus::STATUS_REQUESTED, 185 + ); 186 + case self::STATUS_NEEDS_PULL: 187 + return array( 188 + ReleephRequestStatus::STATUS_NEEDS_PICK, 189 + ); 190 + case self::STATUS_REJECTED: 191 + return array( 192 + ReleephRequestStatus::STATUS_REJECTED, 193 + ); 194 + case self::STATUS_ABANDONED: 195 + return array( 196 + ReleephRequestStatus::STATUS_ABANDONED, 197 + ); 198 + case self::STATUS_PULLED: 199 + return array( 200 + ReleephRequestStatus::STATUS_PICKED, 201 + ); 202 + case self::STATUS_NEEDS_REVERT: 203 + return array( 204 + ReleephRequestStatus::NEEDS_REVERT, 205 + ); 206 + case self::STATUS_REVERTED: 207 + return array( 208 + ReleephRequestStatus::REVERTED, 209 + ); 210 + default: 211 + throw new Exception("Unknown status '{$this->status}'!"); 212 + } 95 213 } 96 214 97 215 }
+158
src/applications/releeph/query/ReleephRequestSearchEngine.php
··· 1 + <?php 2 + 3 + final class ReleephRequestSearchEngine 4 + extends PhabricatorApplicationSearchEngine { 5 + 6 + private $branch; 7 + private $baseURI; 8 + 9 + public function setBranch(ReleephBranch $branch) { 10 + $this->branch = $branch; 11 + return $this; 12 + } 13 + 14 + public function getBranch() { 15 + return $this->branch; 16 + } 17 + 18 + public function setBaseURI($base_uri) { 19 + $this->baseURI = $base_uri; 20 + return $this; 21 + } 22 + 23 + public function buildSavedQueryFromRequest(AphrontRequest $request) { 24 + $saved = new PhabricatorSavedQuery(); 25 + 26 + $saved->setParameter('status', $request->getStr('status')); 27 + $saved->setParameter('severity', $request->getStr('severity')); 28 + $saved->setParameter('requestorPHIDs', $request->getArr('requestorPHIDs')); 29 + 30 + return $saved; 31 + } 32 + 33 + public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { 34 + $query = id(new ReleephRequestQuery()) 35 + ->withBranchIDs(array($this->getBranch()->getID())); 36 + 37 + $status = $saved->getParameter('status'); 38 + $status = idx($this->getStatusValues(), $status); 39 + if ($status) { 40 + $query->withStatus($status); 41 + } 42 + 43 + $severity = $saved->getParameter('severity'); 44 + if ($severity) { 45 + $query->withSeverities(array($severity)); 46 + } 47 + 48 + $requestor_phids = $saved->getParameter('requestorPHIDs'); 49 + if ($requestor_phids) { 50 + $query->withRequestorPHIDs($requestor_phids); 51 + } 52 + 53 + return $query; 54 + } 55 + 56 + public function buildSearchForm( 57 + AphrontFormView $form, 58 + PhabricatorSavedQuery $saved_query) { 59 + 60 + $phids = $saved_query->getParameter('requestorPHIDs', array()); 61 + $handles = id(new PhabricatorObjectHandleData($phids)) 62 + ->setViewer($this->requireViewer()) 63 + ->loadHandles(); 64 + $requestor_tokens = mpull($handles, 'getFullName', 'getPHID'); 65 + 66 + $form 67 + ->appendChild( 68 + id(new AphrontFormSelectControl()) 69 + ->setName('status') 70 + ->setLabel(pht('Status')) 71 + ->setValue($saved_query->getParameter('status')) 72 + ->setOptions($this->getStatusOptions())) 73 + ->appendChild( 74 + id(new AphrontFormSelectControl()) 75 + ->setName('severity') 76 + ->setLabel(pht('Severity')) 77 + ->setValue($saved_query->getParameter('severity')) 78 + ->setOptions($this->getSeverityOptions())) 79 + ->appendChild( 80 + id(new AphrontFormTokenizerControl()) 81 + ->setDatasource('/typeahead/common/users/') 82 + ->setName('requestorPHIDs') 83 + ->setLabel(pht('Requestors')) 84 + ->setValue($requestor_tokens)); 85 + } 86 + 87 + protected function getURI($path) { 88 + return $this->baseURI.$path; 89 + } 90 + 91 + public function getBuiltinQueryNames() { 92 + $names = array( 93 + 'open' => pht('Open Requests'), 94 + 'all' => pht('All Requests'), 95 + ); 96 + 97 + if ($this->requireViewer()->isLoggedIn()) { 98 + $names['requested'] = pht('Requested'); 99 + } 100 + 101 + return $names; 102 + } 103 + 104 + public function buildSavedQueryFromBuiltin($query_key) { 105 + 106 + $query = $this->newSavedQuery(); 107 + $query->setQueryKey($query_key); 108 + 109 + switch ($query_key) { 110 + case 'open': 111 + return $query->setParameter('status', 'open'); 112 + case 'all': 113 + return $query; 114 + case 'requested': 115 + return $query->setParameter( 116 + 'requestorPHIDs', 117 + array($this->requireViewer()->getPHID())); 118 + } 119 + 120 + return parent::buildSavedQueryFromBuiltin($query_key); 121 + } 122 + 123 + private function getStatusOptions() { 124 + return array( 125 + '' => pht('(All Requests)'), 126 + 'open' => pht('Open Requests'), 127 + 'requested' => pht('Pull Requested'), 128 + 'needs-pull' => pht('Needs Pull'), 129 + 'rejected' => pht('Rejected'), 130 + 'abandoned' => pht('Abandoned'), 131 + 'pulled' => pht('Pulled'), 132 + 'needs-revert' => pht('Needs Revert'), 133 + 'reverted' => pht('Reverted'), 134 + ); 135 + } 136 + 137 + private function getStatusValues() { 138 + return array( 139 + 'open' => ReleephRequestQuery::STATUS_OPEN, 140 + 'requested' => ReleephRequestQuery::STATUS_REQUESTED, 141 + 'needs-pull' => ReleephRequestQuery::STATUS_NEEDS_PULL, 142 + 'rejected' => ReleephRequestQuery::STATUS_REJECTED, 143 + 'abandoned' => ReleephRequestQuery::STATUS_ABANDONED, 144 + 'pulled' => ReleephRequestQuery::STATUS_PULLED, 145 + 'needs-revert' => ReleephRequestQuery::STATUS_NEEDS_REVERT, 146 + 'reverted' => ReleephRequestQuery::STATUS_REVERTED, 147 + ); 148 + } 149 + 150 + private function getSeverityOptions() { 151 + return array( 152 + '' => pht('(All Severities)'), 153 + ReleephSeverityFieldSpecification::HOTFIX => pht('Hotfix'), 154 + ReleephSeverityFieldSpecification::RELEASE => pht('Release'), 155 + ); 156 + } 157 + 158 + }
-148
src/applications/uiexample/examples/PhabricatorCountedToggleButtonsExample.php
··· 1 - <?php 2 - 3 - final class PhabricatorCountedToggleButtonsExample 4 - extends PhabricatorUIExample { 5 - 6 - public function getName() { 7 - return 'Counted Toggle Buttons'; 8 - } 9 - 10 - public function getDescription() { 11 - return 'Like AphrontFormToggleButtonsControl, but with counters.'; 12 - } 13 - 14 - private static function buildTreesList() { 15 - $all_trees = array( 16 - array( 17 - 'name' => 'Oak', 18 - 'leaves' => 'deciduous', 19 - 'wood' => 'hard', 20 - 'branches' => 'climbable', 21 - ), 22 - array( 23 - 'name' => 'Pine', 24 - 'leaves' => 'coniferous', 25 - 'wood' => 'soft', 26 - 'branches' => 'spindly', 27 - ), 28 - array( 29 - 'name' => 'Spruce', 30 - 'leaves' => 'coniferous', 31 - 'wood' => 'soft', 32 - 'branches' => 'sticky', 33 - ), 34 - array( 35 - 'name' => 'Ash', 36 - 'leaves' => 'deciduous', 37 - 'wood' => 'hard', 38 - 'branches' => 'climbable', 39 - ), 40 - array( 41 - 'name' => 'Holly', 42 - 'leaves' => 'waxy', 43 - 'wood' => 'hard', 44 - 'branches' => 'prickly', 45 - ), 46 - ); 47 - 48 - for ($ii = 0; $ii < 345; $ii++) { 49 - $name = sprintf("Soylent UltraTree \xE2\x84\xA2 Mutation 0xPD%03x", $ii); 50 - $all_trees[] = array( 51 - 'name' => $name, 52 - 'leaves' => 'carcinogenic', 53 - 'wood' => 'metallic', 54 - 'branches' => 'sentient', 55 - ); 56 - } 57 - 58 - return $all_trees; 59 - } 60 - 61 - public function renderExample() { 62 - $request = $this->getRequest(); 63 - 64 - $form = id(new AphrontFormView()) 65 - ->setUser($request->getUser()); 66 - 67 - $attributes = array('leaves', 'wood', 'branches'); 68 - 69 - $all_trees = self::buildTreesList(); 70 - $trees = $all_trees; 71 - 72 - foreach ($attributes as $attribute) { 73 - $form_value = $request->getStr($attribute); 74 - 75 - $buttons = array(null => 'all'); 76 - foreach ($all_trees as $dict) { 77 - $value = $dict[$attribute]; 78 - $buttons[$value] = $value; 79 - } 80 - 81 - // The trees filtered by other attributes, before we filter by this 82 - // attribute 83 - $trees_before = $all_trees; 84 - foreach ($attributes as $other_attribute) { 85 - if ($other_attribute != $attribute) { 86 - $trees_before = $this->filterTrees($trees_before, $other_attribute); 87 - } 88 - } 89 - 90 - $counters = array(null => count($trees_before)); 91 - foreach ($trees_before as $dict) { 92 - $value = $dict[$attribute]; 93 - if (!isset($counters[$value])) { 94 - $counters[$value] = 0; 95 - } 96 - $counters[$value]++; 97 - } 98 - 99 - $trees = $this->filterTrees($trees, $attribute); 100 - 101 - $control = id(new AphrontFormCountedToggleButtonsControl()) 102 - ->setLabel(ucfirst($attribute)) 103 - ->setName($attribute) 104 - ->setValue($form_value) 105 - ->setBaseURI($request->getRequestURI(), $attribute) 106 - ->setButtons($buttons) 107 - ->setCounters($counters); 108 - 109 - $form->appendChild($control); 110 - } 111 - 112 - $rows = array(); 113 - foreach ($trees as $dict) { 114 - $row = array_select_keys($dict, $attributes); 115 - array_unshift($row, $dict['name']); 116 - $rows[] = $row; 117 - } 118 - 119 - $headers = $attributes; 120 - array_unshift($headers, 'name'); 121 - $table = id(new AphrontTableView($rows)) 122 - ->setHeaders($headers); 123 - 124 - $panel = id(new AphrontPanelView()) 125 - ->setHeader('Counters!') 126 - ->setWidth(AphrontPanelView::WIDTH_FULL) 127 - ->appendChild($form) 128 - ->appendChild($table); 129 - 130 - return $panel; 131 - } 132 - 133 - private function filterTrees($trees, $attribute) { 134 - $form_value = $this->getRequest()->getStr($attribute); 135 - if (!$form_value) { 136 - return $trees; 137 - } 138 - 139 - $new = array(); 140 - foreach ($trees as $dict) { 141 - if ($dict[$attribute] == $form_value) { 142 - $new[] = $dict; 143 - } 144 - } 145 - return $new; 146 - } 147 - 148 - }
-80
src/view/form/control/AphrontFormCountedToggleButtonsControl.php
··· 1 - <?php 2 - 3 - final class AphrontFormCountedToggleButtonsControl extends AphrontFormControl { 4 - 5 - private $baseURI; 6 - private $param; 7 - 8 - private $buttons; 9 - private $counters = array(); 10 - 11 - public function setBaseURI(PhutilURI $uri, $param) { 12 - $this->baseURI = $uri; 13 - $this->param = $param; 14 - return $this; 15 - } 16 - 17 - public function setButtons(array $buttons) { 18 - $this->buttons = $buttons; 19 - return $this; 20 - } 21 - 22 - public function setCounters(array $counters) { 23 - $this->counters = $counters; 24 - return $this; 25 - } 26 - 27 - protected function getCustomControlClass() { 28 - return 'aphront-form-control-counted-togglebuttons'; 29 - } 30 - 31 - protected function renderInput() { 32 - if (!$this->baseURI) { 33 - throw new Exception('Call setBaseURI() before render()!'); 34 - } 35 - 36 - $selected = $this->getValue(); 37 - 38 - $out = array(); 39 - foreach ($this->buttons as $value => $label) { 40 - if ($value == $selected) { 41 - $more = ' toggle-selected toggle-fixed'; 42 - } else { 43 - $more = null; 44 - } 45 - 46 - $counter = idx($this->counters, $value); 47 - 48 - if ($counter > 0) { 49 - $href = $this->baseURI->alter($this->param, $value); 50 - $counter_markup = phutil_tag( 51 - 'div', 52 - array( 53 - 'class' => 'counter', 54 - ), 55 - $counter); 56 - } else { 57 - $href = null; 58 - $counter_markup = ''; 59 - $more .= ' disabled'; 60 - } 61 - 62 - $attributes = array( 63 - 'class' => 'toggle'.$more, 64 - ); 65 - if ($href) { 66 - $attributes['href'] = $href; 67 - } 68 - 69 - $out[] = phutil_tag( 70 - 'a', 71 - $attributes, 72 - array( 73 - $counter_markup, 74 - $label)); 75 - } 76 - 77 - return $out; 78 - } 79 - 80 - }
-39
webroot/rsrc/css/aphront/form-view.css
··· 413 413 float: right; 414 414 } 415 415 416 - .aphront-form-control-counted-togglebuttons { 417 - padding-top: 7px; 418 - } 419 - 420 - .aphront-form-control-counted-togglebuttons .toggle { 421 - position: relative; 422 - } 423 - 424 - .aphront-form-control-counted-togglebuttons .toggle-fixed { 425 - cursor: pointer; 426 - } 427 - 428 - .aphront-form-control-counted-togglebuttons .toggle .counter { 429 - font-size: smaller; 430 - display: none; 431 - position: absolute; 432 - top: -9px; 433 - right: -8px; 434 - padding: 0px 3px; 435 - border-radius: 3px; 436 - } 437 - 438 - .aphront-form-control-counted-togglebuttons:hover .toggle .counter { 439 - display: block; 440 - } 441 - 442 - .aphront-form-control-counted-togglebuttons .toggle .counter { 443 - background: gray; 444 - color: #ddd; 445 - } 446 - 447 - .aphront-form-control-counted-togglebuttons .toggle-selected .counter { 448 - color: white; 449 - } 450 - 451 - .aphront-form-control-counted-togglebuttons .toggle.disabled:hover { 452 - background-color: #a7a7a7; 453 - } 454 - 455 416 .phui-form-divider hr { 456 417 height: 1px; 457 418 border: 0;