1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-10 08:52:39 +01:00

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
This commit is contained in:
epriestley 2013-08-14 15:38:52 -07:00
parent 42a81554ac
commit 23e68ee8cb
13 changed files with 401 additions and 728 deletions

View file

@ -854,7 +854,7 @@ celerity_register_resource_map(array(
),
'aphront-form-view-css' =>
array(
'uri' => '/res/7793ddd1/rsrc/css/aphront/form-view.css',
'uri' => '/res/1be2545a/rsrc/css/aphront/form-view.css',
'type' => 'css',
'requires' =>
array(
@ -4172,7 +4172,7 @@ celerity_register_resource_map(array(
), array(
'packages' =>
array(
'566968f8' =>
'9737ebe0' =>
array(
'name' => 'core.pkg.css',
'symbols' =>
@ -4220,7 +4220,7 @@ celerity_register_resource_map(array(
40 => 'phabricator-property-list-view-css',
41 => 'phabricator-tag-view-css',
),
'uri' => '/res/pkg/566968f8/core.pkg.css',
'uri' => '/res/pkg/9737ebe0/core.pkg.css',
'type' => 'css',
),
'4f81c788' =>
@ -4411,16 +4411,16 @@ celerity_register_resource_map(array(
),
'reverse' =>
array(
'aphront-dialog-view-css' => '566968f8',
'aphront-error-view-css' => '566968f8',
'aphront-form-view-css' => '566968f8',
'aphront-list-filter-view-css' => '566968f8',
'aphront-pager-view-css' => '566968f8',
'aphront-panel-view-css' => '566968f8',
'aphront-table-view-css' => '566968f8',
'aphront-tokenizer-control-css' => '566968f8',
'aphront-tooltip-css' => '566968f8',
'aphront-typeahead-control-css' => '566968f8',
'aphront-dialog-view-css' => '9737ebe0',
'aphront-error-view-css' => '9737ebe0',
'aphront-form-view-css' => '9737ebe0',
'aphront-list-filter-view-css' => '9737ebe0',
'aphront-pager-view-css' => '9737ebe0',
'aphront-panel-view-css' => '9737ebe0',
'aphront-table-view-css' => '9737ebe0',
'aphront-tokenizer-control-css' => '9737ebe0',
'aphront-tooltip-css' => '9737ebe0',
'aphront-typeahead-control-css' => '9737ebe0',
'differential-changeset-view-css' => '09216861',
'differential-core-view-css' => '09216861',
'differential-inline-comment-editor' => 'd07a3bc2',
@ -4434,7 +4434,7 @@ celerity_register_resource_map(array(
'differential-table-of-contents-css' => '09216861',
'diffusion-commit-view-css' => 'c8ce2d88',
'diffusion-icons-css' => 'c8ce2d88',
'global-drag-and-drop-css' => '566968f8',
'global-drag-and-drop-css' => '9737ebe0',
'inline-comment-summary-css' => '09216861',
'javelin-aphlict' => '4f81c788',
'javelin-behavior' => '2dbbb7d1',
@ -4507,55 +4507,55 @@ celerity_register_resource_map(array(
'javelin-util' => '2dbbb7d1',
'javelin-vector' => '2dbbb7d1',
'javelin-workflow' => '2dbbb7d1',
'lightbox-attachment-css' => '566968f8',
'lightbox-attachment-css' => '9737ebe0',
'maniphest-task-summary-css' => '06bacb9a',
'maniphest-transaction-detail-css' => '06bacb9a',
'phabricator-action-list-view-css' => '566968f8',
'phabricator-application-launch-view-css' => '566968f8',
'phabricator-action-list-view-css' => '9737ebe0',
'phabricator-application-launch-view-css' => '9737ebe0',
'phabricator-busy' => '4f81c788',
'phabricator-content-source-view-css' => '09216861',
'phabricator-core-css' => '566968f8',
'phabricator-crumbs-view-css' => '566968f8',
'phabricator-core-css' => '9737ebe0',
'phabricator-crumbs-view-css' => '9737ebe0',
'phabricator-drag-and-drop-file-upload' => 'd07a3bc2',
'phabricator-dropdown-menu' => '4f81c788',
'phabricator-file-upload' => '4f81c788',
'phabricator-filetree-view-css' => '566968f8',
'phabricator-flag-css' => '566968f8',
'phabricator-form-view-css' => '566968f8',
'phabricator-header-view-css' => '566968f8',
'phabricator-filetree-view-css' => '9737ebe0',
'phabricator-flag-css' => '9737ebe0',
'phabricator-form-view-css' => '9737ebe0',
'phabricator-header-view-css' => '9737ebe0',
'phabricator-hovercard' => '4f81c788',
'phabricator-jump-nav' => '566968f8',
'phabricator-jump-nav' => '9737ebe0',
'phabricator-keyboard-shortcut' => '4f81c788',
'phabricator-keyboard-shortcut-manager' => '4f81c788',
'phabricator-main-menu-view' => '566968f8',
'phabricator-main-menu-view' => '9737ebe0',
'phabricator-menu-item' => '4f81c788',
'phabricator-nav-view-css' => '566968f8',
'phabricator-nav-view-css' => '9737ebe0',
'phabricator-notification' => '4f81c788',
'phabricator-notification-css' => '566968f8',
'phabricator-notification-menu-css' => '566968f8',
'phabricator-object-item-list-view-css' => '566968f8',
'phabricator-notification-css' => '9737ebe0',
'phabricator-notification-menu-css' => '9737ebe0',
'phabricator-object-item-list-view-css' => '9737ebe0',
'phabricator-object-selector-css' => '09216861',
'phabricator-phtize' => '4f81c788',
'phabricator-prefab' => '4f81c788',
'phabricator-project-tag-css' => '06bacb9a',
'phabricator-property-list-view-css' => '566968f8',
'phabricator-remarkup-css' => '566968f8',
'phabricator-property-list-view-css' => '9737ebe0',
'phabricator-remarkup-css' => '9737ebe0',
'phabricator-shaped-request' => 'd07a3bc2',
'phabricator-side-menu-view-css' => '566968f8',
'phabricator-standard-page-view' => '566968f8',
'phabricator-tag-view-css' => '566968f8',
'phabricator-side-menu-view-css' => '9737ebe0',
'phabricator-standard-page-view' => '9737ebe0',
'phabricator-tag-view-css' => '9737ebe0',
'phabricator-textareautils' => '4f81c788',
'phabricator-tooltip' => '4f81c788',
'phabricator-transaction-view-css' => '566968f8',
'phabricator-zindex-css' => '566968f8',
'phui-button-css' => '566968f8',
'phui-form-css' => '566968f8',
'phui-icon-view-css' => '566968f8',
'phui-spacing-css' => '566968f8',
'sprite-apps-large-css' => '566968f8',
'sprite-gradient-css' => '566968f8',
'sprite-icons-css' => '566968f8',
'sprite-menu-css' => '566968f8',
'syntax-highlighting-css' => '566968f8',
'phabricator-transaction-view-css' => '9737ebe0',
'phabricator-zindex-css' => '9737ebe0',
'phui-button-css' => '9737ebe0',
'phui-form-css' => '9737ebe0',
'phui-icon-view-css' => '9737ebe0',
'phui-spacing-css' => '9737ebe0',
'sprite-apps-large-css' => '9737ebe0',
'sprite-gradient-css' => '9737ebe0',
'sprite-icons-css' => '9737ebe0',
'sprite-menu-css' => '9737ebe0',
'syntax-highlighting-css' => '9737ebe0',
),
));

View file

@ -32,7 +32,6 @@ phutil_register_library_map(array(
'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php',
'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php',
'AphrontFormControl' => 'view/form/control/AphrontFormControl.php',
'AphrontFormCountedToggleButtonsControl' => 'view/form/control/AphrontFormCountedToggleButtonsControl.php',
'AphrontFormCropControl' => 'view/form/control/AphrontFormCropControl.php',
'AphrontFormDateControl' => 'view/form/control/AphrontFormDateControl.php',
'AphrontFormDividerControl' => 'view/form/control/AphrontFormDividerControl.php',
@ -1020,7 +1019,6 @@ phutil_register_library_map(array(
'PhabricatorCountdownSearchEngine' => 'applications/countdown/query/PhabricatorCountdownSearchEngine.php',
'PhabricatorCountdownView' => 'applications/countdown/view/PhabricatorCountdownView.php',
'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php',
'PhabricatorCountedToggleButtonsExample' => 'applications/uiexample/examples/PhabricatorCountedToggleButtonsExample.php',
'PhabricatorCrumbView' => 'view/layout/PhabricatorCrumbView.php',
'PhabricatorCrumbsView' => 'view/layout/PhabricatorCrumbsView.php',
'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
@ -1996,6 +1994,7 @@ phutil_register_library_map(array(
'ReleephRequestMailReceiver' => 'applications/releeph/mail/ReleephRequestMailReceiver.php',
'ReleephRequestQuery' => 'applications/releeph/query/ReleephRequestQuery.php',
'ReleephRequestReplyHandler' => 'applications/releeph/mail/ReleephRequestReplyHandler.php',
'ReleephRequestSearchEngine' => 'applications/releeph/query/ReleephRequestSearchEngine.php',
'ReleephRequestStatus' => 'applications/releeph/constants/ReleephRequestStatus.php',
'ReleephRequestStatusView' => 'applications/releeph/view/request/ReleephRequestStatusView.php',
'ReleephRequestTransaction' => 'applications/releeph/storage/ReleephRequestTransaction.php',
@ -2062,7 +2061,6 @@ phutil_register_library_map(array(
'AphrontFileResponse' => 'AphrontResponse',
'AphrontFormCheckboxControl' => 'AphrontFormControl',
'AphrontFormControl' => 'AphrontView',
'AphrontFormCountedToggleButtonsControl' => 'AphrontFormControl',
'AphrontFormCropControl' => 'AphrontFormControl',
'AphrontFormDateControl' => 'AphrontFormControl',
'AphrontFormDividerControl' => 'AphrontFormControl',
@ -3088,7 +3086,6 @@ phutil_register_library_map(array(
'PhabricatorCountdownSearchEngine' => 'PhabricatorApplicationSearchEngine',
'PhabricatorCountdownView' => 'AphrontTagView',
'PhabricatorCountdownViewController' => 'PhabricatorCountdownController',
'PhabricatorCountedToggleButtonsExample' => 'PhabricatorUIExample',
'PhabricatorCrumbView' => 'AphrontView',
'PhabricatorCrumbsView' => 'AphrontView',
'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
@ -4129,7 +4126,11 @@ phutil_register_library_map(array(
'ReleephBranchNamePreviewController' => 'ReleephController',
'ReleephBranchPreviewView' => 'AphrontFormControl',
'ReleephBranchQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ReleephBranchViewController' => 'ReleephProjectController',
'ReleephBranchViewController' =>
array(
0 => 'ReleephProjectController',
1 => 'PhabricatorApplicationSearchResultsControllerInterface',
),
'ReleephCommitFinderException' => 'Exception',
'ReleephCommitMessageFieldSpecification' => 'ReleephFieldSpecification',
'ReleephController' => 'PhabricatorController',
@ -4190,6 +4191,7 @@ phutil_register_library_map(array(
'ReleephRequestMailReceiver' => 'PhabricatorObjectMailReceiver',
'ReleephRequestQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
'ReleephRequestReplyHandler' => 'PhabricatorMailReplyHandler',
'ReleephRequestSearchEngine' => 'PhabricatorApplicationSearchEngine',
'ReleephRequestStatusView' => 'AphrontView',
'ReleephRequestTransaction' => 'PhabricatorApplicationTransaction',
'ReleephRequestTransactionComment' => 'PhabricatorApplicationTransactionComment',

View file

@ -75,7 +75,7 @@ final class PhabricatorApplicationReleeph extends PhabricatorApplication {
// Branch navigation made pretty, as it's the most common:
'(?P<projectName>[^/]+)/(?P<branchName>[^/]+)/' => array(
'' => 'ReleephBranchViewController',
'(?:query/(?P<queryKey>[^/]+)/)?' => 'ReleephBranchViewController',
'edit/' => 'ReleephBranchEditController',
'request/' => 'ReleephRequestEditController',
'(?P<action>close|re-open)/' => 'ReleephBranchAccessController',

View file

@ -1,62 +1,84 @@
<?php
final class ReleephBranchViewController extends ReleephProjectController {
final class ReleephBranchViewController extends ReleephProjectController
implements PhabricatorApplicationSearchResultsControllerInterface {
private $queryKey;
public function shouldAllowPublic() {
return true;
}
public function willProcessRequest(array $data) {
parent::willProcessRequest($data);
$this->queryKey = idx($data, 'queryKey');
}
public function processRequest() {
$request = $this->getRequest();
$controller = id(new PhabricatorApplicationSearchController($request))
->setQueryKey($this->queryKey)
->setSearchEngine($this->getSearchEngine())
->setNavigation($this->buildSideNavView());
return $this->delegateToController($controller);
}
public function renderResultsList(
array $requests,
PhabricatorSavedQuery $query) {
assert_instances_of($requests, 'ReleephRequest');
$viewer = $this->getRequest()->getUser();
$releeph_branch = $this->getReleephBranch();
$releeph_project = $this->getReleephProject();
$all_releeph_requests = $releeph_branch->loadReleephRequests(
$request->getUser());
$selector = $releeph_project->getReleephFieldSelector();
$fields = $selector->arrangeFieldsForSelectForm(
$selector->getFieldSpecifications());
$form = id(new AphrontFormView())
->setMethod('GET')
->setUser($request->getUser());
$filtered_releeph_requests = $all_releeph_requests;
foreach ($fields as $field) {
$all_releeph_requests_without_this_field = $all_releeph_requests;
foreach ($fields as $other_field) {
if ($other_field != $field) {
$other_field->selectReleephRequestsHook(
$request,
$all_releeph_requests_without_this_field);
}
}
$field->appendSelectControlsHook(
$form,
$request,
$all_releeph_requests,
$all_releeph_requests_without_this_field);
$field->selectReleephRequestsHook(
$request,
$filtered_releeph_requests);
}
$form->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Filter')));
// TODO: Really gross.
$releeph_branch->populateReleephRequestHandles(
$viewer,
$requests);
$list = id(new ReleephRequestHeaderListView())
->setOriginType('branch')
->setUser($request->getUser())
->setUser($viewer)
->setAphrontRequest($this->getRequest())
->setReleephProject($releeph_project)
->setReleephBranch($releeph_branch)
->setReleephRequests($filtered_releeph_requests);
->setReleephRequests($requests);
$filter = id(new AphrontListFilterView())
->appendChild($form);
return $list;
}
$crumbs = $this->buildApplicationCrumbs()
public function buildSideNavView($for_app = false) {
$user = $this->getRequest()->getUser();
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI()));
$this->getSearchEngine()->addNavigationItems($nav->getMenu());
$nav->selectFilter(null);
return $nav;
}
private function getSearchEngine() {
$branch = $this->getReleephBranch();
return id(new ReleephRequestSearchEngine())
->setBranch($branch)
->setBaseURI($branch->getURI())
->setViewer($this->getRequest()->getUser());
}
public function buildApplicationCrumbs() {
$releeph_branch = $this->getReleephBranch();
$releeph_project = $this->getReleephProject();
$crumbs = parent::buildApplicationCrumbs()
->addCrumb(
id(new PhabricatorCrumbView())
->setName($releeph_project->getName())
@ -66,7 +88,6 @@ final class ReleephBranchViewController extends ReleephProjectController {
->setName($releeph_branch->getDisplayNameWithDetail())
->setHref($releeph_branch->getURI()));
// Don't show the request button for inactive (closed) branches
if ($releeph_branch->isActive()) {
$create_uri = $releeph_branch->getURI('request/');
$crumbs->addAction(
@ -76,19 +97,8 @@ final class ReleephBranchViewController extends ReleephProjectController {
->setIcon('create'));
}
return $this->buildStandardPageResponse(
array(
$crumbs,
$filter,
$list
),
array(
'title' =>
$releeph_project->getName().
' - '.
$releeph_branch->getDisplayName().
' requests'
));
return $crumbs;
}
}

View file

@ -45,11 +45,6 @@ abstract class ReleephFieldSpecification
return $this->getStorageKey() !== null;
}
/**
* This will be called many times if you are using **Selecting**. In
* particular, for N selecting fields, selectReleephRequests() is called
* N-squared times, each time for R ReleephRequests.
*/
final public function getValue() {
if ($this->requestValue !== null) {
return $this->requestValue;
@ -174,76 +169,6 @@ abstract class ReleephFieldSpecification
public function bulkLoad(array $releeph_requests) {
}
/* -( Selecting )---------------------------------------------------------- */
/**
* Append select controls to the given form.
*
* You are given:
*
* - the AphrontFormView to append to;
*
* - the AphrontRequest, so you can make use of the value currently selected
* in the form;
*
* - $all_releeph_requests: an array of all the ReleephRequests without any
* selection based filtering; and
*
* - $all_releeph_requests_without_this_field: an array of ReleephRequests
* that have been selected by all the other select controls on this page.
*
* The example in ReleephLevelFieldSpecification shows how to use these.
* $all_releeph_requests lets you find out all the values of a field in all
* ReleephRequests, so you can render controls for every known value.
*
* $all_releeph_requests_without_this_field lets you count how many
* ReleephRequests could be affected by this field's select control, after
* all the other fields have made their selections.
* ReleephLevelFieldSpecification uses this to render a preview count for
* each select button, and disables the button completely (but still renders
* it) if it couldn't possibly select anything.
*/
protected function appendSelectControls(
AphrontFormView $form,
AphrontRequest $request,
array $all_releeph_requests,
array $all_releeph_requests_without_this_field) {
return null;
}
/**
* Filter the $releeph_requests using the data you set with your form
* controls, and which is now available in the provided AphrontRequest.
*/
protected function selectReleephRequests(AphrontRequest $request,
array &$releeph_requests) {
return null;
}
/**
* If you have PHIDs that can be used in an AphrontFormTokenizerControl,
* return true here, return the PHIDs in getSelectablePHIDs(), and return the
* URL the Tokenizer should use for the form control in
* getSelectTokenizerDatasource().
*
* This is a cheap alternative to implementing appendSelectControls() and
* selectReleephRequests() in full.
*/
protected function hasSelectablePHIDs() {
return false;
}
protected function getSelectablePHIDs() {
throw new ReleephFieldSpecificationIncompleteException($this);
}
protected function getSelectTokenizerDatasource() {
throw new ReleephFieldSpecificationIncompleteException($this);
}
/* -( Commit Messages )---------------------------------------------------- */
public function shouldAppearOnCommitMessage() {
@ -328,106 +253,4 @@ abstract class ReleephFieldSpecification
return true;
}
/* -( Implementation )----------------------------------------------------- */
/**
* The "hook" functions ##appendSelectControlsHook()## and
* ##selectReleephRequestsHook()## are used with ##hasSelectablePHIDs()##, to
* use the tokenizing helpers if ##hasSelectablePHIDs()## returns true.
*/
public function appendSelectControlsHook(
AphrontFormView $form,
AphrontRequest $request,
array $all_releeph_requests,
array $all_releeph_requests_without_this_field) {
if ($this->hasSelectablePHIDs()) {
$this->appendTokenizingSelectControl(
$form,
$request,
$all_releeph_requests,
$all_releeph_requests_without_this_field);
} else {
$this->appendSelectControls(
$form,
$request,
$all_releeph_requests,
$all_releeph_requests_without_this_field);
}
}
// See above
public function selectReleephRequestsHook(AphrontRequest $request,
array &$releeph_requests) {
if ($this->hasSelectablePHIDs()) {
$this->selectReleephRequestsFromTokens(
$request,
$releeph_requests);
} else {
$this->selectReleephRequests(
$request,
$releeph_requests);
}
}
private function appendTokenizingSelectControl(
AphrontFormView $form,
AphrontRequest $request,
array $all_releeph_requests,
array $all_releeph_requests_without_this_field) {
$key = urlencode(strtolower($this->getName()));
$selected_phids = $request->getArr($key);
$handles = id(new PhabricatorObjectHandleData($selected_phids))
->setViewer($request->getUser())
->loadHandles();
$tokens = array();
foreach ($selected_phids as $phid) {
$tokens[$phid] = $handles[$phid]->getFullName();
}
$datasource = $this->getSelectTokenizerDatasource();
$control =
id(new AphrontFormTokenizerControl())
->setDatasource($datasource)
->setName($key)
->setLabel($this->getName())
->setValue($tokens);
$form->appendChild($control);
}
private function selectReleephRequestsFromTokens(AphrontRequest $request,
array &$releeph_requests) {
$key = urlencode(strtolower($this->getName()));
$selected_phids = $request->getArr($key);
if (!$selected_phids) {
return;
}
$selected_phid_lookup = array();
foreach ($selected_phids as $phid) {
$selected_phid_lookup[$phid] = $phid;
}
$filtered = array();
foreach ($releeph_requests as $releeph_request) {
$rq_phids = $this
->setReleephRequest($releeph_request)
->getSelectablePHIDs();
foreach ($rq_phids as $rq_phid) {
if (idx($selected_phid_lookup, $rq_phid)) {
$filtered[] = $releeph_request;
break;
}
}
}
$releeph_requests = $filtered;
}
}

View file

@ -19,13 +19,6 @@ abstract class ReleephLevelFieldSpecification
abstract public function getNameForLevel($level);
abstract public function getDescriptionForLevel($level);
/**
* Use getCanonicalLevel() to convert old, unsupported levels to new ones.
*/
protected function getCanonicalLevel($misc_level) {
return $misc_level;
}
public function getStorageKey() {
$class = get_class($this);
throw new ReleephFieldSpecificationIncompleteException(
@ -34,16 +27,18 @@ abstract class ReleephLevelFieldSpecification
}
public function renderValueForHeaderView() {
$raw_level = $this->getValue();
$level = $this->getCanonicalLevel($raw_level);
return $this->getNameForLevel($level);
return $this->getNameForLevel($this->getValue());
}
public function renderEditControl() {
$control_name = $this->getRequiredStorageKey();
$all_levels = $this->getLevels();
$level = $this->getCanonicalLevel($this->getValue());
$level = $request->getStr($control_name);
if (!$level) {
$level = $this->getValue();
}
if (!$level) {
$level = $this->getDefaultLevel();
}
@ -141,83 +136,4 @@ abstract class ReleephLevelFieldSpecification
return idx($this->nameMap, $name);
}
protected function appendSelectControls(
AphrontFormView $form,
AphrontRequest $request,
array $all_releeph_requests,
array $all_releeph_requests_without_this_field) {
$buttons = array(null => 'All');
// Add in known level/names
foreach ($this->getLevels() as $level) {
$name = $this->getNameForLevel($level);
$buttons[$name] = $name;
}
// Add in any names we've seen in the wild, as well.
foreach ($all_releeph_requests as $releeph_request) {
$raw_level = $this->setReleephRequest($releeph_request)->getValue();
if (!$raw_level) {
// The ReleephRequest might not have a level set
continue;
}
$level = $this->getCanonicalLevel($raw_level);
$name = $this->getNameForLevel($level);
$buttons[$name] = $name;
}
$key = $this->getRequiredStorageKey();
$current = $request->getStr($key);
$counters = array(null => count($all_releeph_requests_without_this_field));
foreach ($all_releeph_requests_without_this_field as $releeph_request) {
$raw_level = $this->setReleephRequest($releeph_request)->getValue();
if (!$raw_level) {
// The ReleephRequest might not have a level set
continue;
}
$level = $this->getCanonicalLevel($raw_level);
$name = $this->getNameForLevel($level);
if (!isset($counters[$name])) {
$counters[$name] = 0;
}
$counters[$name]++;
}
$control = id(new AphrontFormCountedToggleButtonsControl())
->setLabel($this->getName())
->setValue($current)
->setBaseURI($request->getRequestURI(), $key)
->setButtons($buttons)
->setCounters($counters);
$form
->appendChild($control)
->addHiddenInput($key, $current);
}
protected function selectReleephRequests(AphrontRequest $request,
array &$releeph_requests) {
$key = $this->getRequiredStorageKey();
$current = $request->getStr($key);
if (!$current) {
return;
}
$filtered = array();
foreach ($releeph_requests as $releeph_request) {
$raw_level = $this->setReleephRequest($releeph_request)->getValue();
$level = $this->getCanonicalLevel($raw_level);
$name = $this->getNameForLevel($level);
if ($name == $current) {
$filtered[] = $releeph_request;
}
}
$releeph_requests = $filtered;
}
}

View file

@ -26,20 +26,6 @@ final class ReleephRequestorFieldSpecification
->render();
}
public function hasSelectablePHIDs() {
return true;
}
public function getSelectTokenizerDatasource() {
return '/typeahead/common/users/';
}
public function getSelectablePHIDs() {
return array(
$this->getReleephRequest()->getRequestUserPHID(),
);
}
public function shouldAppearOnCommitMessage() {
return true;
}

View file

@ -17,77 +17,4 @@ final class ReleephStatusFieldSpecification
->render();
}
private static $filters = array(
'req' => ReleephRequestStatus::STATUS_REQUESTED,
'app' => ReleephRequestStatus::STATUS_NEEDS_PICK,
'rej' => ReleephRequestStatus::STATUS_REJECTED,
'abn' => ReleephRequestStatus::STATUS_ABANDONED,
'mer' => ReleephRequestStatus::STATUS_PICKED,
'rrq' => ReleephRequestStatus::STATUS_NEEDS_REVERT,
'rev' => ReleephRequestStatus::STATUS_REVERTED,
);
protected function appendSelectControls(
AphrontFormView $form,
AphrontRequest $request,
array $all_releeph_requests,
array $all_releeph_requests_without_this_field) {
$filter_names = array(
null => 'All',
);
foreach (self::$filters as $code => $status) {
$name = ReleephRequestStatus::getStatusDescriptionFor($status);
$filter_names[$code] = $name;
}
$key = 'status';
$code = $request->getStr($key);
$current_status = idx(self::$filters, $code);
$codes = array_flip(self::$filters);
$counters = array(null => count($all_releeph_requests_without_this_field));
foreach ($all_releeph_requests_without_this_field as $releeph_request) {
$this_status = $releeph_request->getStatus();
$this_code = idx($codes, $this_status);
if (!isset($counters[$this_code])) {
$counters[$this_code] = 0;
}
$counters[$this_code]++;
}
$control = id(new AphrontFormCountedToggleButtonsControl())
->setLabel($this->getName())
->setValue($code)
->setBaseURI($request->getRequestURI(), $key)
->setButtons($filter_names)
->setCounters($counters);
$form
->appendChild($control)
->addHiddenInput($key, $code);
}
protected function selectReleephRequests(AphrontRequest $request,
array &$releeph_requests) {
$key = 'status';
$code = $request->getStr($key);
if (!$code) {
return;
}
$current_status = idx(self::$filters, $code);
$filtered = array();
foreach ($releeph_requests as $releeph_request) {
if ($releeph_request->getStatus() == $current_status) {
$filtered[] = $releeph_request;
}
}
$releeph_requests = $filtered;
}
}

View file

@ -7,6 +7,21 @@ final class ReleephRequestQuery
private $commitToRevMap;
private $ids;
private $phids;
private $severities;
private $requestorPHIDs;
private $branchIDs;
const STATUS_ALL = 'status-all';
const STATUS_OPEN = 'status-open';
const STATUS_REQUESTED = 'status-requested';
const STATUS_NEEDS_PULL = 'status-needs-pull';
const STATUS_REJECTED = 'status-rejected';
const STATUS_ABANDONED = 'status-abandoned';
const STATUS_PULLED = 'status-pulled';
const STATUS_NEEDS_REVERT = 'status-needs-revert';
const STATUS_REVERTED = 'status-reverted';
private $status = self::STATUS_ALL;
public function withIDs(array $ids) {
$this->ids = $ids;
@ -18,6 +33,11 @@ final class ReleephRequestQuery
return $this;
}
public function withBranchIDs(array $branch_ids) {
$this->branchIDs = $branch_ids;
return $this;
}
public function getRevisionPHID($commit_phid) {
if ($this->commitToRevMap) {
return idx($this->commitToRevMap, $commit_phid, null);
@ -26,11 +46,26 @@ final class ReleephRequestQuery
return null;
}
public function withStatus($status) {
$this->status = $status;
return $this;
}
public function withRequestedCommitPHIDs(array $requested_commit_phids) {
$this->requestedCommitPHIDs = $requested_commit_phids;
return $this;
}
public function withRequestorPHIDs(array $phids) {
$this->requestorPHIDs = $phids;
return $this;
}
public function withSeverities(array $severities) {
$this->severities = $severities;
return $this;
}
public function withRevisionPHIDs(array $revision_phids) {
$type = PhabricatorEdgeConfig::TYPE_DREV_HAS_COMMIT;
@ -65,6 +100,32 @@ final class ReleephRequestQuery
return $table->loadAllFromArray($data);
}
public function willFilterPage(array $requests) {
// TODO: These should be serviced by the query, but are not currently
// denormalized anywhere. For now, filter them here instead.
$keep_status = array_fuse($this->getKeepStatusConstants());
if ($keep_status) {
foreach ($requests as $key => $request) {
if (empty($keep_status[$request->getStatus()])) {
unset($requests[$key]);
}
}
}
if ($this->severities) {
$severities = array_fuse($this->severities);
foreach ($requests as $key => $request) {
if (empty($severities[$request->getDetail('releeph:severity')])) {
unset($requests[$key]);
}
}
}
return $requests;
}
private function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
@ -82,6 +143,13 @@ final class ReleephRequestQuery
$this->phids);
}
if ($this->branchIDs) {
$where[] = qsprintf(
$conn_r,
'branchID IN (%Ld)',
$this->branchIDs);
}
if ($this->requestedCommitPHIDs) {
$where[] = qsprintf(
$conn_r,
@ -89,9 +157,59 @@ final class ReleephRequestQuery
$this->requestedCommitPHIDs);
}
if ($this->requestorPHIDs) {
$where[] = qsprintf(
$conn_r,
'requestUserPHID IN (%Ls)',
$this->requestorPHIDs);
}
$where[] = $this->buildPagingClause($conn_r);
return $this->formatWhereClause($where);
}
private function getKeepStatusConstants() {
switch ($this->status) {
case self::STATUS_ALL:
return array();
case self::STATUS_OPEN:
return array(
ReleephRequestStatus::STATUS_REQUESTED,
ReleephRequestStatus::STATUS_NEEDS_PICK,
ReleephRequestStatus::STATUS_NEEDS_REVERT,
);
case self::STATUS_REQUESTED:
return array(
ReleephRequestStatus::STATUS_REQUESTED,
);
case self::STATUS_NEEDS_PULL:
return array(
ReleephRequestStatus::STATUS_NEEDS_PICK,
);
case self::STATUS_REJECTED:
return array(
ReleephRequestStatus::STATUS_REJECTED,
);
case self::STATUS_ABANDONED:
return array(
ReleephRequestStatus::STATUS_ABANDONED,
);
case self::STATUS_PULLED:
return array(
ReleephRequestStatus::STATUS_PICKED,
);
case self::STATUS_NEEDS_REVERT:
return array(
ReleephRequestStatus::NEEDS_REVERT,
);
case self::STATUS_REVERTED:
return array(
ReleephRequestStatus::REVERTED,
);
default:
throw new Exception("Unknown status '{$this->status}'!");
}
}
}

View file

@ -0,0 +1,158 @@
<?php
final class ReleephRequestSearchEngine
extends PhabricatorApplicationSearchEngine {
private $branch;
private $baseURI;
public function setBranch(ReleephBranch $branch) {
$this->branch = $branch;
return $this;
}
public function getBranch() {
return $this->branch;
}
public function setBaseURI($base_uri) {
$this->baseURI = $base_uri;
return $this;
}
public function buildSavedQueryFromRequest(AphrontRequest $request) {
$saved = new PhabricatorSavedQuery();
$saved->setParameter('status', $request->getStr('status'));
$saved->setParameter('severity', $request->getStr('severity'));
$saved->setParameter('requestorPHIDs', $request->getArr('requestorPHIDs'));
return $saved;
}
public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) {
$query = id(new ReleephRequestQuery())
->withBranchIDs(array($this->getBranch()->getID()));
$status = $saved->getParameter('status');
$status = idx($this->getStatusValues(), $status);
if ($status) {
$query->withStatus($status);
}
$severity = $saved->getParameter('severity');
if ($severity) {
$query->withSeverities(array($severity));
}
$requestor_phids = $saved->getParameter('requestorPHIDs');
if ($requestor_phids) {
$query->withRequestorPHIDs($requestor_phids);
}
return $query;
}
public function buildSearchForm(
AphrontFormView $form,
PhabricatorSavedQuery $saved_query) {
$phids = $saved_query->getParameter('requestorPHIDs', array());
$handles = id(new PhabricatorObjectHandleData($phids))
->setViewer($this->requireViewer())
->loadHandles();
$requestor_tokens = mpull($handles, 'getFullName', 'getPHID');
$form
->appendChild(
id(new AphrontFormSelectControl())
->setName('status')
->setLabel(pht('Status'))
->setValue($saved_query->getParameter('status'))
->setOptions($this->getStatusOptions()))
->appendChild(
id(new AphrontFormSelectControl())
->setName('severity')
->setLabel(pht('Severity'))
->setValue($saved_query->getParameter('severity'))
->setOptions($this->getSeverityOptions()))
->appendChild(
id(new AphrontFormTokenizerControl())
->setDatasource('/typeahead/common/users/')
->setName('requestorPHIDs')
->setLabel(pht('Requestors'))
->setValue($requestor_tokens));
}
protected function getURI($path) {
return $this->baseURI.$path;
}
public function getBuiltinQueryNames() {
$names = array(
'open' => pht('Open Requests'),
'all' => pht('All Requests'),
);
if ($this->requireViewer()->isLoggedIn()) {
$names['requested'] = pht('Requested');
}
return $names;
}
public function buildSavedQueryFromBuiltin($query_key) {
$query = $this->newSavedQuery();
$query->setQueryKey($query_key);
switch ($query_key) {
case 'open':
return $query->setParameter('status', 'open');
case 'all':
return $query;
case 'requested':
return $query->setParameter(
'requestorPHIDs',
array($this->requireViewer()->getPHID()));
}
return parent::buildSavedQueryFromBuiltin($query_key);
}
private function getStatusOptions() {
return array(
'' => pht('(All Requests)'),
'open' => pht('Open Requests'),
'requested' => pht('Pull Requested'),
'needs-pull' => pht('Needs Pull'),
'rejected' => pht('Rejected'),
'abandoned' => pht('Abandoned'),
'pulled' => pht('Pulled'),
'needs-revert' => pht('Needs Revert'),
'reverted' => pht('Reverted'),
);
}
private function getStatusValues() {
return array(
'open' => ReleephRequestQuery::STATUS_OPEN,
'requested' => ReleephRequestQuery::STATUS_REQUESTED,
'needs-pull' => ReleephRequestQuery::STATUS_NEEDS_PULL,
'rejected' => ReleephRequestQuery::STATUS_REJECTED,
'abandoned' => ReleephRequestQuery::STATUS_ABANDONED,
'pulled' => ReleephRequestQuery::STATUS_PULLED,
'needs-revert' => ReleephRequestQuery::STATUS_NEEDS_REVERT,
'reverted' => ReleephRequestQuery::STATUS_REVERTED,
);
}
private function getSeverityOptions() {
return array(
'' => pht('(All Severities)'),
ReleephSeverityFieldSpecification::HOTFIX => pht('Hotfix'),
ReleephSeverityFieldSpecification::RELEASE => pht('Release'),
);
}
}

View file

@ -1,148 +0,0 @@
<?php
final class PhabricatorCountedToggleButtonsExample
extends PhabricatorUIExample {
public function getName() {
return 'Counted Toggle Buttons';
}
public function getDescription() {
return 'Like AphrontFormToggleButtonsControl, but with counters.';
}
private static function buildTreesList() {
$all_trees = array(
array(
'name' => 'Oak',
'leaves' => 'deciduous',
'wood' => 'hard',
'branches' => 'climbable',
),
array(
'name' => 'Pine',
'leaves' => 'coniferous',
'wood' => 'soft',
'branches' => 'spindly',
),
array(
'name' => 'Spruce',
'leaves' => 'coniferous',
'wood' => 'soft',
'branches' => 'sticky',
),
array(
'name' => 'Ash',
'leaves' => 'deciduous',
'wood' => 'hard',
'branches' => 'climbable',
),
array(
'name' => 'Holly',
'leaves' => 'waxy',
'wood' => 'hard',
'branches' => 'prickly',
),
);
for ($ii = 0; $ii < 345; $ii++) {
$name = sprintf("Soylent UltraTree \xE2\x84\xA2 Mutation 0xPD%03x", $ii);
$all_trees[] = array(
'name' => $name,
'leaves' => 'carcinogenic',
'wood' => 'metallic',
'branches' => 'sentient',
);
}
return $all_trees;
}
public function renderExample() {
$request = $this->getRequest();
$form = id(new AphrontFormView())
->setUser($request->getUser());
$attributes = array('leaves', 'wood', 'branches');
$all_trees = self::buildTreesList();
$trees = $all_trees;
foreach ($attributes as $attribute) {
$form_value = $request->getStr($attribute);
$buttons = array(null => 'all');
foreach ($all_trees as $dict) {
$value = $dict[$attribute];
$buttons[$value] = $value;
}
// The trees filtered by other attributes, before we filter by this
// attribute
$trees_before = $all_trees;
foreach ($attributes as $other_attribute) {
if ($other_attribute != $attribute) {
$trees_before = $this->filterTrees($trees_before, $other_attribute);
}
}
$counters = array(null => count($trees_before));
foreach ($trees_before as $dict) {
$value = $dict[$attribute];
if (!isset($counters[$value])) {
$counters[$value] = 0;
}
$counters[$value]++;
}
$trees = $this->filterTrees($trees, $attribute);
$control = id(new AphrontFormCountedToggleButtonsControl())
->setLabel(ucfirst($attribute))
->setName($attribute)
->setValue($form_value)
->setBaseURI($request->getRequestURI(), $attribute)
->setButtons($buttons)
->setCounters($counters);
$form->appendChild($control);
}
$rows = array();
foreach ($trees as $dict) {
$row = array_select_keys($dict, $attributes);
array_unshift($row, $dict['name']);
$rows[] = $row;
}
$headers = $attributes;
array_unshift($headers, 'name');
$table = id(new AphrontTableView($rows))
->setHeaders($headers);
$panel = id(new AphrontPanelView())
->setHeader('Counters!')
->setWidth(AphrontPanelView::WIDTH_FULL)
->appendChild($form)
->appendChild($table);
return $panel;
}
private function filterTrees($trees, $attribute) {
$form_value = $this->getRequest()->getStr($attribute);
if (!$form_value) {
return $trees;
}
$new = array();
foreach ($trees as $dict) {
if ($dict[$attribute] == $form_value) {
$new[] = $dict;
}
}
return $new;
}
}

View file

@ -1,80 +0,0 @@
<?php
final class AphrontFormCountedToggleButtonsControl extends AphrontFormControl {
private $baseURI;
private $param;
private $buttons;
private $counters = array();
public function setBaseURI(PhutilURI $uri, $param) {
$this->baseURI = $uri;
$this->param = $param;
return $this;
}
public function setButtons(array $buttons) {
$this->buttons = $buttons;
return $this;
}
public function setCounters(array $counters) {
$this->counters = $counters;
return $this;
}
protected function getCustomControlClass() {
return 'aphront-form-control-counted-togglebuttons';
}
protected function renderInput() {
if (!$this->baseURI) {
throw new Exception('Call setBaseURI() before render()!');
}
$selected = $this->getValue();
$out = array();
foreach ($this->buttons as $value => $label) {
if ($value == $selected) {
$more = ' toggle-selected toggle-fixed';
} else {
$more = null;
}
$counter = idx($this->counters, $value);
if ($counter > 0) {
$href = $this->baseURI->alter($this->param, $value);
$counter_markup = phutil_tag(
'div',
array(
'class' => 'counter',
),
$counter);
} else {
$href = null;
$counter_markup = '';
$more .= ' disabled';
}
$attributes = array(
'class' => 'toggle'.$more,
);
if ($href) {
$attributes['href'] = $href;
}
$out[] = phutil_tag(
'a',
$attributes,
array(
$counter_markup,
$label));
}
return $out;
}
}

View file

@ -413,45 +413,6 @@ table.aphront-form-control-checkbox-layout th {
float: right;
}
.aphront-form-control-counted-togglebuttons {
padding-top: 7px;
}
.aphront-form-control-counted-togglebuttons .toggle {
position: relative;
}
.aphront-form-control-counted-togglebuttons .toggle-fixed {
cursor: pointer;
}
.aphront-form-control-counted-togglebuttons .toggle .counter {
font-size: smaller;
display: none;
position: absolute;
top: -9px;
right: -8px;
padding: 0px 3px;
border-radius: 3px;
}
.aphront-form-control-counted-togglebuttons:hover .toggle .counter {
display: block;
}
.aphront-form-control-counted-togglebuttons .toggle .counter {
background: gray;
color: #ddd;
}
.aphront-form-control-counted-togglebuttons .toggle-selected .counter {
color: white;
}
.aphront-form-control-counted-togglebuttons .toggle.disabled:hover {
background-color: #a7a7a7;
}
.phui-form-divider hr {
height: 1px;
border: 0;