From 3a2c2ae3c3ae3d88427ec96787840119508c2e73 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 22 Apr 2015 14:31:36 -0700 Subject: [PATCH] Implement a scope selector for the global search Summary: See M1433. Fixes T7266. Fixes T4475. Ref T7314. Future work/notes/etc: - Write the User Guide (see TODO). - This might needs some design tweaks -- I think it's functionally almost-equivalent to the mock, but the UI isn't quite the same. - (Mobile design is a touch off-looking I think?) - When you use a custom query, the duplicate "magnifying glass" icons are a little weird. Maybe change one or the other. - Maybe worth adding an "Open Documents in Current Application" option? Planning to wait for feedback on that. - Need a Quicksand integration to change the current application at some point. - Searching in "Current Application" from, e.g., the 404 page just searches all documents. Current plan is to just document this behavior, since the icon is a pretty good callout and it seems plausible that this is intuitive enough that users won't have a hard time with it. Test Plan: New dropdown: {F379150} Device-ish: {F379151} Normal search (current application, from maniphest, selects tasks): {F379153} Application search from non-application: {F379154} Reviewers: btrahan, chad Reviewed By: chad Subscribers: johnny-bit, epriestley Maniphest Tasks: T7266, T7314, T4475 Differential Revision: https://secure.phabricator.com/D12509 --- resources/celerity/map.php | 78 +++++----- .../base/PhabricatorApplication.php | 4 + .../PhabricatorDifferentialApplication.php | 6 + .../PhabricatorDiffusionApplication.php | 6 + .../PhabricatorFundApplication.php | 6 + .../PhabricatorManiphestApplication.php | 6 + .../PhabricatorPassphraseApplication.php | 6 + .../PhabricatorPeopleApplication.php | 6 + .../PhabricatorPholioApplication.php | 6 + .../PhabricatorPhrictionApplication.php | 6 + .../PhabricatorPonderApplication.php | 6 + .../PhabricatorProjectApplication.php | 6 + .../PhabricatorSearchController.php | 86 ++++++----- .../storage/PhabricatorUserPreferences.php | 1 + .../menu/PhabricatorMainMenuSearchView.php | 135 ++++++++++++++++++ .../page/menu/PhabricatorMainMenuView.php | 10 ++ .../css/application/base/main-menu-view.css | 22 ++- .../rsrc/js/core/behavior-search-typeahead.js | 82 +++++++++++ 18 files changed, 403 insertions(+), 75 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 932cf68d17..61dc140a36 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,9 +7,9 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'a2a90172', - 'core.pkg.js' => '8e62b4aa', - 'darkconsole.pkg.js' => 'b0a3ba93', + 'core.pkg.css' => 'f7d01efc', + 'core.pkg.js' => 'a1f9db42', + 'darkconsole.pkg.js' => '8ab24e01', 'differential.pkg.css' => '3500921f', 'differential.pkg.js' => 'c0506961', 'diffusion.pkg.css' => '591664fa', @@ -34,7 +34,7 @@ return array( 'rsrc/css/aphront/typeahead.css' => '0e403212', 'rsrc/css/application/almanac/almanac.css' => 'dbb9b3af', 'rsrc/css/application/auth/auth.css' => '1e655982', - 'rsrc/css/application/base/main-menu-view.css' => 'c648b2f5', + 'rsrc/css/application/base/main-menu-view.css' => '31e66da9', 'rsrc/css/application/base/notification-menu.css' => '3c9d8aa1', 'rsrc/css/application/base/phabricator-application-launch-view.css' => '16ca323f', 'rsrc/css/application/base/standard-page-view.css' => 'd3e1abe9', @@ -350,7 +350,7 @@ return array( 'rsrc/image/texture/table_header_hover.png' => '038ec3b9', 'rsrc/image/texture/table_header_tall.png' => 'd56b434f', 'rsrc/js/application/aphlict/Aphlict.js' => '30a6303c', - 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'ee37f73a', + 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '572566ae', 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'b1a59974', 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => 'ea681761', 'rsrc/js/application/auth/behavior-persona-login.js' => '9414ff18', @@ -460,7 +460,7 @@ return array( 'rsrc/js/core/behavior-autofocus.js' => '7319e029', 'rsrc/js/core/behavior-choose-control.js' => '6153c708', 'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2', - 'rsrc/js/core/behavior-dark-console.js' => 'b8df5663', + 'rsrc/js/core/behavior-dark-console.js' => '08883e8b', 'rsrc/js/core/behavior-device.js' => 'a205cf28', 'rsrc/js/core/behavior-drag-and-drop-textarea.js' => '6d49590e', 'rsrc/js/core/behavior-error-log.js' => '6882e80a', @@ -486,7 +486,7 @@ return array( 'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e', 'rsrc/js/core/behavior-reveal-content.js' => '60821bc7', 'rsrc/js/core/behavior-scrollbar.js' => '834a1173', - 'rsrc/js/core/behavior-search-typeahead.js' => '724b1247', + 'rsrc/js/core/behavior-search-typeahead.js' => 'bc965352', 'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6', 'rsrc/js/core/behavior-toggle-class.js' => 'e566f52c', 'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884', @@ -550,7 +550,7 @@ return array( 'inline-comment-summary-css' => 'eb5f8e8c', 'javelin-aphlict' => '30a6303c', 'javelin-behavior' => '61cbc29a', - 'javelin-behavior-aphlict-dropdown' => 'ee37f73a', + 'javelin-behavior-aphlict-dropdown' => '572566ae', 'javelin-behavior-aphlict-listen' => 'b1a59974', 'javelin-behavior-aphlict-status' => 'ea681761', 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', @@ -567,7 +567,7 @@ return array( 'javelin-behavior-conpherence-pontificate' => '21ba5861', 'javelin-behavior-conpherence-widget-pane' => '93568464', 'javelin-behavior-countdown-timer' => 'e4cc26b3', - 'javelin-behavior-dark-console' => 'b8df5663', + 'javelin-behavior-dark-console' => '08883e8b', 'javelin-behavior-dashboard-async-panel' => '469c0d9e', 'javelin-behavior-dashboard-move-panels' => '82439934', 'javelin-behavior-dashboard-query-panel-select' => '453c5375', @@ -628,7 +628,7 @@ return array( 'javelin-behavior-phabricator-oncopy' => '2926fff2', 'javelin-behavior-phabricator-remarkup-assist' => 'e32d14ab', 'javelin-behavior-phabricator-reveal-content' => '60821bc7', - 'javelin-behavior-phabricator-search-typeahead' => '724b1247', + 'javelin-behavior-phabricator-search-typeahead' => 'bc965352', 'javelin-behavior-phabricator-show-older-transactions' => 'dbbf48b6', 'javelin-behavior-phabricator-tooltips' => '3ee3408b', 'javelin-behavior-phabricator-transaction-comment-form' => '9f7309fb', @@ -737,7 +737,7 @@ return array( 'phabricator-hovercard-view-css' => '44394670', 'phabricator-keyboard-shortcut' => '1ae869f2', 'phabricator-keyboard-shortcut-manager' => 'c1700f6f', - 'phabricator-main-menu-view' => 'c648b2f5', + 'phabricator-main-menu-view' => '31e66da9', 'phabricator-nav-view-css' => '7aeaf435', 'phabricator-notification' => '0c6946e7', 'phabricator-notification-css' => '9c279160', @@ -869,6 +869,14 @@ return array( 'phabricator-shaped-request', 'conpherence-thread-manager', ), + '08883e8b' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-util', + 'javelin-dom', + 'javelin-request', + 'phabricator-keyboard-shortcut', + ), '0a3f3021' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1190,6 +1198,16 @@ return array( 'javelin-vector', 'javelin-dom', ), + '572566ae' => array( + 'javelin-behavior', + 'javelin-request', + 'javelin-stratcom', + 'javelin-vector', + 'javelin-dom', + 'javelin-uri', + 'javelin-behavior-device', + 'phabricator-title', + ), 58562350 => array( 'javelin-dom', 'javelin-util', @@ -1349,16 +1367,6 @@ return array( 'javelin-vector', 'javelin-util', ), - '724b1247' => array( - 'javelin-behavior', - 'javelin-typeahead-ondemand-source', - 'javelin-typeahead', - 'javelin-dom', - 'javelin-uri', - 'javelin-util', - 'javelin-stratcom', - 'phabricator-prefab', - ), '7319e029' => array( 'javelin-behavior', 'javelin-dom', @@ -1719,14 +1727,6 @@ return array( 'javelin-dom', 'javelin-util', ), - 'b8df5663' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-util', - 'javelin-dom', - 'javelin-request', - 'phabricator-keyboard-shortcut', - ), 'bba9eedf' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1739,6 +1739,16 @@ return array( 'javelin-mask', 'phabricator-drag-and-drop-file-upload', ), + 'bc965352' => array( + 'javelin-behavior', + 'javelin-typeahead-ondemand-source', + 'javelin-typeahead', + 'javelin-dom', + 'javelin-uri', + 'javelin-util', + 'javelin-stratcom', + 'phabricator-prefab', + ), 'bd4c8dca' => array( 'javelin-install', 'javelin-util', @@ -1940,16 +1950,6 @@ return array( 'javelin-stratcom', 'javelin-vector', ), - 'ee37f73a' => array( - 'javelin-behavior', - 'javelin-request', - 'javelin-stratcom', - 'javelin-vector', - 'javelin-dom', - 'javelin-uri', - 'javelin-behavior-device', - 'phabricator-title', - ), 'efe49472' => array( 'javelin-install', 'javelin-util', diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index 6fdb93c784..d5237adc11 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -582,4 +582,8 @@ abstract class PhabricatorApplication implements PhabricatorPolicyInterface { } } + public function getApplicationSearchDocumentTypes() { + return array(); + } + } diff --git a/src/applications/differential/application/PhabricatorDifferentialApplication.php b/src/applications/differential/application/PhabricatorDifferentialApplication.php index b866dda790..1aca323457 100644 --- a/src/applications/differential/application/PhabricatorDifferentialApplication.php +++ b/src/applications/differential/application/PhabricatorDifferentialApplication.php @@ -205,4 +205,10 @@ EOTEXT ); } + public function getApplicationSearchDocumentTypes() { + return array( + DifferentialRevisionPHIDType::TYPECONST, + ); + } + } diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php index 940a930278..92b433a48a 100644 --- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php +++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php @@ -161,4 +161,10 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication { ); } + public function getApplicationSearchDocumentTypes() { + return array( + PhabricatorRepositoryCommitPHIDType::TYPECONST, + ); + } + } diff --git a/src/applications/fund/application/PhabricatorFundApplication.php b/src/applications/fund/application/PhabricatorFundApplication.php index 72207995b8..57997a1611 100644 --- a/src/applications/fund/application/PhabricatorFundApplication.php +++ b/src/applications/fund/application/PhabricatorFundApplication.php @@ -62,4 +62,10 @@ final class PhabricatorFundApplication extends PhabricatorApplication { ); } + public function getApplicationSearchDocumentTypes() { + return array( + FundInitiativePHIDType::TYPECONST, + ); + } + } diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php index cbdd4a79e6..3937d6be7f 100644 --- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php +++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php @@ -158,4 +158,10 @@ final class PhabricatorManiphestApplication extends PhabricatorApplication { ); } + public function getApplicationSearchDocumentTypes() { + return array( + ManiphestTaskPHIDType::TYPECONST, + ); + } + } diff --git a/src/applications/passphrase/application/PhabricatorPassphraseApplication.php b/src/applications/passphrase/application/PhabricatorPassphraseApplication.php index d549a451e5..df21ba8be1 100644 --- a/src/applications/passphrase/application/PhabricatorPassphraseApplication.php +++ b/src/applications/passphrase/application/PhabricatorPassphraseApplication.php @@ -57,4 +57,10 @@ final class PhabricatorPassphraseApplication extends PhabricatorApplication { ); } + public function getApplicationSearchDocumentTypes() { + return array( + PassphraseCredentialPHIDType::TYPECONST, + ); + } + } diff --git a/src/applications/people/application/PhabricatorPeopleApplication.php b/src/applications/people/application/PhabricatorPeopleApplication.php index b00078d928..c2c2203ad0 100644 --- a/src/applications/people/application/PhabricatorPeopleApplication.php +++ b/src/applications/people/application/PhabricatorPeopleApplication.php @@ -192,4 +192,10 @@ final class PhabricatorPeopleApplication extends PhabricatorApplication { return $items; } + public function getApplicationSearchDocumentTypes() { + return array( + PhabricatorPeopleUserPHIDType::TYPECONST, + ); + } + } diff --git a/src/applications/pholio/application/PhabricatorPholioApplication.php b/src/applications/pholio/application/PhabricatorPholioApplication.php index 968760fa00..e56f989630 100644 --- a/src/applications/pholio/application/PhabricatorPholioApplication.php +++ b/src/applications/pholio/application/PhabricatorPholioApplication.php @@ -90,4 +90,10 @@ final class PhabricatorPholioApplication extends PhabricatorApplication { ); } + public function getApplicationSearchDocumentTypes() { + return array( + PholioMockPHIDType::TYPECONST, + ); + } + } diff --git a/src/applications/phriction/application/PhabricatorPhrictionApplication.php b/src/applications/phriction/application/PhabricatorPhrictionApplication.php index 89fff32ffe..19967c2e70 100644 --- a/src/applications/phriction/application/PhabricatorPhrictionApplication.php +++ b/src/applications/phriction/application/PhabricatorPhrictionApplication.php @@ -69,4 +69,10 @@ final class PhabricatorPhrictionApplication extends PhabricatorApplication { return 0.140; } + public function getApplicationSearchDocumentTypes() { + return array( + PhrictionDocumentPHIDType::TYPECONST, + ); + } + } diff --git a/src/applications/ponder/application/PhabricatorPonderApplication.php b/src/applications/ponder/application/PhabricatorPonderApplication.php index b143d3ba68..e21660301c 100644 --- a/src/applications/ponder/application/PhabricatorPonderApplication.php +++ b/src/applications/ponder/application/PhabricatorPonderApplication.php @@ -79,4 +79,10 @@ final class PhabricatorPonderApplication extends PhabricatorApplication { ); } + public function getApplicationSearchDocumentTypes() { + return array( + PonderQuestionPHIDType::TYPECONST, + ); + } + } diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 7f874da3be..e7e4f623f9 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -133,4 +133,10 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { ); } + public function getApplicationSearchDocumentTypes() { + return array( + PhabricatorProjectProjectPHIDType::TYPECONST, + ); + } + } diff --git a/src/applications/search/controller/PhabricatorSearchController.php b/src/applications/search/controller/PhabricatorSearchController.php index bacb0cb3c0..1b22ad1a2f 100644 --- a/src/applications/search/controller/PhabricatorSearchController.php +++ b/src/applications/search/controller/PhabricatorSearchController.php @@ -3,6 +3,8 @@ final class PhabricatorSearchController extends PhabricatorSearchBaseController { + const SCOPE_CURRENT_APPLICATION = 'application'; + private $queryKey; public function shouldAllowPublic() { @@ -32,49 +34,65 @@ final class PhabricatorSearchController $engine = new PhabricatorSearchApplicationSearchEngine(); $engine->setViewer($viewer); - // NOTE: This is a little weird. If we're coming from primary search, we - // load the user's first search filter and overwrite the "query" part of - // it, then send them to that result page. This is sort of odd, but lets - // users choose a default query like "Open Tasks" in a reasonable way, - // with only this piece of somewhat-sketchy code. See discussion in T4365. - + // If we're coming from primary search, do some special handling to + // interpret the scope selector and query. if ($request->getBool('search:primary')) { + + // If there's no query, just take the user to advanced search. if (!strlen($request->getStr('query'))) { $advanced_uri = '/search/query/advanced/'; return id(new AphrontRedirectResponse())->setURI($advanced_uri); } - $named_queries = $engine->loadEnabledNamedQueries(); - if ($named_queries) { - $named = head($named_queries); + // First, load or construct a template for the search by examining + // the current search scope. + $scope = $request->getStr('search:scope'); + $saved = null; - $query_key = $named->getQueryKey(); - $saved = null; - if ($engine->isBuiltinQuery($query_key)) { - $saved = $engine->buildSavedQueryFromBuiltin($query_key); - } else { - $saved = id(new PhabricatorSavedQueryQuery()) - ->setViewer($viewer) - ->withQueryKeys(array($query_key)) - ->executeOne(); - } - - if ($saved) { - $saved->setParameter('query', $request->getStr('query')); - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - try { - $saved->setID(null)->save(); - } catch (AphrontDuplicateKeyQueryException $ex) { - // Ignore, this is just a repeated search. - } - unset($unguarded); - - $results_uri = $engine->getQueryResultsPageURI( - $saved->getQueryKey()).'#R'; - - return id(new AphrontRedirectResponse())->setURI($results_uri); + if ($scope == self::SCOPE_CURRENT_APPLICATION) { + $application = id(new PhabricatorApplicationQuery()) + ->setViewer($viewer) + ->withClasses(array($request->getStr('search:application'))) + ->executeOne(); + if ($application) { + $types = $application->getApplicationSearchDocumentTypes(); + if ($types) { + $saved = id(new PhabricatorSavedQuery()) + ->setEngineClassName(get_class($engine)) + ->setParameter('types', $types); + } } } + + if (!$saved && !$engine->isBuiltinQuery($scope)) { + $saved = id(new PhabricatorSavedQueryQuery()) + ->setViewer($viewer) + ->withQueryKeys(array($scope)) + ->executeOne(); + } + + if (!$saved) { + if (!$engine->isBuiltinQuery($scope)) { + $scope = 'all'; + } + $saved = $engine->buildSavedQueryFromBuiltin($scope); + } + + // Add the user's query, then save this as a new saved query and send + // the user to the results page. + $saved->setParameter('query', $request->getStr('query')); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + try { + $saved->setID(null)->save(); + } catch (AphrontDuplicateKeyQueryException $ex) { + // Ignore, this is just a repeated search. + } + unset($unguarded); + + $query_key = $saved->getQueryKey(); + $results_uri = $engine->getQueryResultsPageURI($query_key).'#R'; + return id(new AphrontRedirectResponse())->setURI($results_uri); } $controller = id(new PhabricatorApplicationSearchController()) diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index 6e05e5e44c..985f1bb4f6 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -19,6 +19,7 @@ final class PhabricatorUserPreferences extends PhabricatorUserDAO { const PREFERENCE_SEARCHBAR_JUMP = 'searchbar-jump'; const PREFERENCE_SEARCH_SHORTCUT = 'search-shortcut'; + const PREFERENCE_SEARCH_SCOPE = 'search-scope'; const PREFERENCE_DIFFUSION_BLAME = 'diffusion-blame'; const PREFERENCE_DIFFUSION_COLOR = 'diffusion-color'; diff --git a/src/view/page/menu/PhabricatorMainMenuSearchView.php b/src/view/page/menu/PhabricatorMainMenuSearchView.php index 3211025d8d..3373cdc653 100644 --- a/src/view/page/menu/PhabricatorMainMenuSearchView.php +++ b/src/view/page/menu/PhabricatorMainMenuSearchView.php @@ -3,6 +3,16 @@ final class PhabricatorMainMenuSearchView extends AphrontView { private $id; + private $application; + + public function setApplication(PhabricatorApplication $application) { + $this->application = $application; + return $this; + } + + public function getApplication() { + return $this->application; + } public function getID() { if (!$this->id) { @@ -36,6 +46,7 @@ final class PhabricatorMainMenuSearchView extends AphrontView { ''); $search_datasource = new PhabricatorSearchDatasource(); + $scope_key = PhabricatorUserPreferences::PREFERENCE_SEARCH_SCOPE; Javelin::initBehavior( 'phabricator-search-typeahead', @@ -46,6 +57,7 @@ final class PhabricatorMainMenuSearchView extends AphrontView { 'src' => $search_datasource->getDatasourceURI(), 'limit' => 10, 'placeholder' => pht('Search'), + 'scopeUpdateURI' => '/settings/adjust/?key='.$scope_key, )); $primary_input = phutil_tag( @@ -63,6 +75,8 @@ final class PhabricatorMainMenuSearchView extends AphrontView { ), pht('Search')); + $selector = $this->buildModeSelector(); + $form = phabricator_form( $user, array( @@ -78,6 +92,7 @@ final class PhabricatorMainMenuSearchView extends AphrontView { 'class' => 'phui-icon-view phui-font-fa fa-search', ), $search_text), + $selector, $primary_input, $target, ))); @@ -85,4 +100,124 @@ final class PhabricatorMainMenuSearchView extends AphrontView { return $form; } + private function buildModeSelector() { + $viewer = $this->getUser(); + + $items = array(); + $items[] = array( + 'name' => pht('Search'), + ); + + $items[] = array( + 'icon' => 'fa-globe', + 'name' => pht('Search All Documents'), + 'value' => 'all', + ); + + $application_value = null; + $application_icon = 'fa-file-o'; + $application = $this->getApplication(); + if ($application) { + $application_value = get_class($application); + if ($application->getApplicationSearchDocumentTypes()) { + $application_icon = $application->getFontIcon(); + } + } + + $items[] = array( + 'icon' => $application_icon, + 'name' => pht('Search Current Application'), + 'value' => PhabricatorSearchController::SCOPE_CURRENT_APPLICATION, + ); + + $items[] = array( + 'name' => pht('Saved Queries'), + ); + + + $engine = id(new PhabricatorSearchApplicationSearchEngine()) + ->setViewer($viewer); + $engine_queries = $engine->loadEnabledNamedQueries(); + $query_map = mpull($engine_queries, 'getQueryName', 'getQueryKey'); + foreach ($query_map as $query_key => $query_name) { + if ($query_key == 'all') { + // Skip the builtin "All" query since it's redundant with the default + // setting. + continue; + } + + $items[] = array( + 'icon' => 'fa-search', + 'name' => $query_name, + 'value' => $query_key, + ); + } + + $items[] = array( + 'name' => pht('More Options'), + ); + + $items[] = array( + 'icon' => 'fa-search-plus', + 'name' => pht('Advanced Search'), + 'href' => '/search/query/advanced/', + ); + + /* TODO: Write this. + $items[] = array( + 'icon' => 'fa-book', + 'name' => pht('User Guide: Search'), + 'href' => PhabricatorEnv::getDoclink('User Guide: Search'), + ); + */ + + $scope_key = PhabricatorUserPreferences::PREFERENCE_SEARCH_SCOPE; + $current_value = $viewer->loadPreferences()->getPreference( + $scope_key, + 'all'); + + $current_icon = 'fa-globe'; + foreach ($items as $item) { + if (idx($item, 'value') == $current_value) { + $current_icon = $item['icon']; + break; + } + } + + $selector = id(new PHUIButtonView()) + ->addClass('phabricator-main-menu-search-dropdown') + ->addSigil('global-search-dropdown') + ->setMetadata( + array( + 'items' => $items, + 'icon' => $current_icon, + 'value' => $current_value, + )) + ->setIcon( + id(new PHUIIconView()) + ->addSigil('global-search-dropdown-icon') + ->setIconFont($current_icon)) + ->setDropdown(true); + + $input = javelin_tag( + 'input', + array( + 'type' => 'hidden', + 'sigil' => 'global-search-dropdown-input', + 'name' => 'search:scope', + 'value' => $current_value, + )); + + $application_input = javelin_tag( + 'input', + array( + 'type' => 'hidden', + 'sigil' => 'global-search-dropdown-app', + 'name' => 'search:application', + 'value' => $application_value, + )); + + return array($selector, $input, $application_input); + } + } diff --git a/src/view/page/menu/PhabricatorMainMenuView.php b/src/view/page/menu/PhabricatorMainMenuView.php index cca6e80cc4..f49590eb8b 100644 --- a/src/view/page/menu/PhabricatorMainMenuView.php +++ b/src/view/page/menu/PhabricatorMainMenuView.php @@ -114,6 +114,16 @@ final class PhabricatorMainMenuView extends AphrontView { if ($show_search) { $search = new PhabricatorMainMenuSearchView(); $search->setUser($user); + + $application = null; + $controller = $this->getController(); + if ($controller) { + $application = $controller->getCurrentApplication(); + } + if ($application) { + $search->setApplication($application); + } + $result = $search; $pref_shortcut = PhabricatorUserPreferences::PREFERENCE_SEARCH_SHORTCUT; diff --git a/webroot/rsrc/css/application/base/main-menu-view.css b/webroot/rsrc/css/application/base/main-menu-view.css index e5de8f16f5..ccdd20edb0 100644 --- a/webroot/rsrc/css/application/base/main-menu-view.css +++ b/webroot/rsrc/css/application/base/main-menu-view.css @@ -160,10 +160,9 @@ height: 28px; line-height: 12px; box-shadow: 0px 1px 1px rgba(128, 128, 128, 0.25); - padding: 6px 30px 6px 6px; + padding: 6px 30px 6px 46px; float: left; width: 205px; - left: 0; } .phabricator-main-menu.main-header-dark .phabricator-main-menu-search input { @@ -207,6 +206,25 @@ border-radius: 0; } +.phabricator-main-menu-search button.phabricator-main-menu-search-dropdown { + position: absolute; + right: auto; + left: 0; + width: 40px; +} + +.phabricator-main-menu-search button.phabricator-main-menu-search-dropdown + .phui-icon-view { + color: rgba(255,255,255,.8); +} + +.phabricator-main-menu-search-dropdown .caret { + position: absolute; + right: 4px; + top: 3px; +} + + .phabricator-main-menu-search button:hover { color: #fff; } diff --git a/webroot/rsrc/js/core/behavior-search-typeahead.js b/webroot/rsrc/js/core/behavior-search-typeahead.js index 644969d107..9ef431195d 100644 --- a/webroot/rsrc/js/core/behavior-search-typeahead.js +++ b/webroot/rsrc/js/core/behavior-search-typeahead.js @@ -143,4 +143,86 @@ JX.behavior('phabricator-search-typeahead', function(config) { typeahead.setPlaceholder(''); typeahead.updatePlaceHolder(); }); + + // TODO: Quicksand needs to update the application search input as we change + // applications; we should register a listener. + // TODO: Quicksand also needs to update the application search icon on the + // button itself and in the menu. + + // Implement the scope selector menu for the global search. + JX.Stratcom.listen('click', 'global-search-dropdown', function(e) { + var data = e.getNodeData('global-search-dropdown'); + var button = e.getNode('global-search-dropdown'); + if (data.menu) { + return; + } + + e.kill(); + + function updateValue(spec) { + if (data.value == spec.value) { + return; + } + + // Swap out the icon. + var icon = JX.DOM.find(button, 'span', 'global-search-dropdown-icon'); + JX.DOM.alterClass(icon, data.icon, false); + data.icon = spec.icon; + JX.DOM.alterClass(icon, data.icon, true); + + // Update the value. + data.value = spec.value; + + // Update the form input. + var frame = button.parentNode; + var input = JX.DOM.find(frame, 'input', 'global-search-dropdown-input'); + input.value = data.value; + + new JX.Request(config.scopeUpdateURI) + .setData({value: data.value}) + .send(); + } + + var menu = new JX.PHUIXDropdownMenu(button) + .setAlign('left'); + data.menu = menu; + + menu.listen('open', function() { + var list = new JX.PHUIXActionListView(); + + for (var ii = 0; ii < data.items.length; ii++) { + var spec = data.items[ii]; + var item = new JX.PHUIXActionView() + .setName(spec.name) + .setIcon(spec.icon); + + if (spec.value) { + if (spec.value == data.value) { + item.setSelected(true); + } + + var handler = function(spec, e) { + e.prevent(); + menu.close(); + updateValue(spec); + }; + + item.setHandler(JX.bind(null, handler, spec)); + } else if (spec.href) { + item.setHref(spec.href); + item.setHandler(function() { menu.close(); }); + } else { + item.setDisabled(true); + } + + list.addItem(item); + } + + menu.setContent(list.getNode()); + }); + + menu.open(); + }); + + });