From 852ecc21023f3252882f05da788bc8f5191e9c89 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 31 Jul 2012 17:58:21 -0700 Subject: [PATCH] Add a basic search typeahead Summary: This needs a bunch of refinement but pretty much works. Currently shows only users and applications. Plans: - Show actual search results too. - Clean up the datasource endpoint so it's less of a mess. - Make other typeaheads look more like this one. - Improve sorting. - Make object names hit the named objects as the first match. Test Plan: Will attach screenshots. Reviewers: btrahan, vrana, chad Reviewed By: vrana CC: aran Maniphest Tasks: T1569 Differential Revision: https://secure.phabricator.com/D3110 --- src/__celerity_resource_map__.php | 17 +++- .../base/PhabricatorApplication.php | 24 ++++- .../PhabricatorApplicationDifferential.php | 8 ++ ...torTypeaheadCommonDatasourceController.php | 69 +++++++++++++- .../menu/PhabricatorMainMenuSearchView.php | 24 ++++- .../css/application/base/main-menu-view.css | 93 ++++++++++++++++--- .../core/behavior-search-typeahead.js | 52 +++++++++++ 7 files changed, 261 insertions(+), 26 deletions(-) create mode 100644 webroot/rsrc/js/application/core/behavior-search-typeahead.js diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 72a9301782..10571442ee 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -1439,6 +1439,21 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/core/behavior-oncopy.js', ), + 'javelin-behavior-phabricator-search-typeahead' => + array( + 'uri' => '/res/9ceffb09/rsrc/js/application/core/behavior-search-typeahead.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-typeahead-ondemand-source', + 2 => 'javelin-typeahead', + 3 => 'javelin-dom', + 4 => 'javelin-uri', + 5 => 'javelin-stratcom', + ), + 'disk' => '/rsrc/js/application/core/behavior-search-typeahead.js', + ), 'javelin-behavior-phabricator-tooltips' => array( 'uri' => '/res/49f92a92/rsrc/js/application/core/behavior-tooltip.js', @@ -2263,7 +2278,7 @@ celerity_register_resource_map(array( ), 'phabricator-main-menu-view' => array( - 'uri' => '/res/795788ca/rsrc/css/application/base/main-menu-view.css', + 'uri' => '/res/5bae3234/rsrc/css/application/base/main-menu-view.css', 'type' => 'css', 'requires' => array( diff --git a/src/applications/base/PhabricatorApplication.php b/src/applications/base/PhabricatorApplication.php index cd917c3fa8..65f86362c0 100644 --- a/src/applications/base/PhabricatorApplication.php +++ b/src/applications/base/PhabricatorApplication.php @@ -30,15 +30,35 @@ abstract class PhabricatorApplication { public function getName() { - return substr(__CLASS__, strlen('PhabricatorApplication')); + return substr(get_class($this), strlen('PhabricatorApplication')); + } + + public function getShortDescription() { + return $this->getName().' Application'; } public function isEnabled() { return true; } + public function getPHID() { + return 'PHID-APPS-'.get_class($this); + } -/* -( Application Information )-------------------------------------------- */ + public function getTypeaheadURI() { + return $this->getBaseURI(); + } + + public function getBaseURI() { + return null; + } + + public function getIconURI() { + return PhabricatorUser::getDefaultProfileImageURI(); + } + + +/* -( URI Routing )-------------------------------------------------------- */ public function getRoutes() { diff --git a/src/applications/differential/application/PhabricatorApplicationDifferential.php b/src/applications/differential/application/PhabricatorApplicationDifferential.php index f618e10b84..1df206c4ad 100644 --- a/src/applications/differential/application/PhabricatorApplicationDifferential.php +++ b/src/applications/differential/application/PhabricatorApplicationDifferential.php @@ -24,5 +24,13 @@ final class PhabricatorApplicationDifferential extends PhabricatorApplication { ); } + public function getBaseURI() { + return '/differential/'; + } + + public function getShortDescription() { + return 'Code Review Application'; + } + } diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php index 3095737f58..20db5eed9f 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php @@ -28,7 +28,10 @@ final class PhabricatorTypeaheadCommonDatasourceController $request = $this->getRequest(); $query = $request->getStr('q'); + $need_rich_data = false; + $need_users = false; + $need_applications = false; $need_all_users = false; $need_lists = false; $need_projs = false; @@ -38,6 +41,11 @@ final class PhabricatorTypeaheadCommonDatasourceController $need_arcanist_projects = false; $need_noproject = false; switch ($this->type) { + case 'mainsearch': + $need_users = true; + $need_applications = true; + $need_rich_data = true; + break; case 'searchowner': $need_users = true; $need_upforgrabs = true; @@ -78,9 +86,20 @@ final class PhabricatorTypeaheadCommonDatasourceController case 'arcanistprojects': $need_arcanist_projects = true; break; - } + // TODO: We transfer these fields without keys as an opitimization, but this + // function is hard to read as a result. Until we can sort it out, here's + // what the position arguments mean: + // + // 0: (required) name to match against what the user types + // 1: (optional) URI + // 2: (required) PHID + // 3: (optional) priority matching string + // 4: (optional) display name [overrides position 0] + // 5: (optional) display type + // 6: (optional) image URI + $data = array(); if ($need_upforgrabs) { @@ -106,11 +125,16 @@ final class PhabricatorTypeaheadCommonDatasourceController 'userName', 'realName', 'phid'); + + if ($need_rich_data) { + $columns[] = 'profileImagePHID'; + } + if ($query) { $conn_r = id(new PhabricatorUser())->establishConnection('r'); $ids = queryfx_all( $conn_r, - 'SELECT DISTINCT userID FROM %T WHERE token LIKE %>', + 'SELECT DISTINCT userID FROM %T WHERE token LIKE %> OR 1 = 1', PhabricatorUser::NAMETOKEN_TABLE, $query); $ids = ipull($ids, 'userID'); @@ -125,6 +149,12 @@ final class PhabricatorTypeaheadCommonDatasourceController } else { $users = id(new PhabricatorUser())->loadColumns($columns); } + + if ($need_rich_data) { + $phids = mpull($users, 'getPHID'); + $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); + } + foreach ($users as $user) { if (!$need_all_users) { if ($user->getIsSystemAgent()) { @@ -134,12 +164,18 @@ final class PhabricatorTypeaheadCommonDatasourceController continue; } } - $data[] = array( + $spec = array( $user->getUsername().' ('.$user->getRealName().')', '/p/'.$user->getUsername(), $user->getPHID(), $user->getUsername(), + null, + 'User', ); + if ($need_rich_data) { + $spec[] = $handles[$user->getPHID()]->getImageURI(); + } + $data[] = $spec; } } @@ -201,6 +237,33 @@ final class PhabricatorTypeaheadCommonDatasourceController } } + if ($need_applications) { + $applications = PhabricatorApplication::getAllInstalledApplications(); + foreach ($applications as $application) { + $uri = $application->getTypeaheadURI(); + if (!$uri) { + continue; + } + $data[] = array( + $application->getName().' '.$application->getShortDescription(), + $uri, + $application->getPHID(), + $application->getName(), + $application->getName(), + $application->getShortDescription(), + $application->getIconURI(), + ); + } + } + + if (!$need_rich_data) { + foreach ($data as $key => $info) { + unset($data[$key][4]); + unset($data[$key][5]); + unset($data[$key][6]); + } + } + return id(new AphrontAjaxResponse()) ->setContent($data); } diff --git a/src/view/page/menu/PhabricatorMainMenuSearchView.php b/src/view/page/menu/PhabricatorMainMenuSearchView.php index 8988fc4bf7..6296cd7717 100644 --- a/src/view/page/menu/PhabricatorMainMenuSearchView.php +++ b/src/view/page/menu/PhabricatorMainMenuSearchView.php @@ -42,6 +42,7 @@ final class PhabricatorMainMenuSearchView extends AphrontView { public function render() { $user = $this->user; + $target_id = celerity_generate_unique_node_id(); $search_id = $this->getID(); $input = phutil_render_tag( @@ -49,16 +50,28 @@ final class PhabricatorMainMenuSearchView extends AphrontView { array( 'type' => 'text', 'name' => 'query', - 'id' => $search_id, + 'id' => $search_id, + 'autocomplete' => 'off', )); $scope = $this->scope; - Javelin::initBehavior( - 'placeholder', + $target = javelin_render_tag( + 'div', array( - 'id' => $search_id, - 'text' => PhabricatorSearchScope::getScopePlaceholder($scope), + 'id' => $target_id, + 'class' => 'phabricator-main-menu-search-target', + ), + ''); + + Javelin::initBehavior( + 'phabricator-search-typeahead', + array( + 'id' => $target_id, + 'input' => $search_id, + 'src' => '/typeahead/common/mainsearch/', + 'limit' => 10, + 'placeholder' => PhabricatorSearchScope::getScopePlaceholder($scope), )); $scope_input = phutil_render_tag( @@ -79,6 +92,7 @@ final class PhabricatorMainMenuSearchView extends AphrontView { $input. ''. $scope_input. + $target. ''); $group = new PhabricatorMainMenuGroupView(); diff --git a/webroot/rsrc/css/application/base/main-menu-view.css b/webroot/rsrc/css/application/base/main-menu-view.css index f7c076063c..c9e74fb574 100644 --- a/webroot/rsrc/css/application/base/main-menu-view.css +++ b/webroot/rsrc/css/application/base/main-menu-view.css @@ -14,7 +14,6 @@ background: #33393d; position: relative; box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.25); - overflow: hidden; height: 44px; } @@ -49,21 +48,27 @@ */ +.phabricator-main-menu-group { + height: 44px; + position: relative; +} + .device-desktop .phabricator-main-menu-group { display: inline-block; text-align: left; - height: 44px; } .device-tablet .phabricator-main-menu-group, .device-phone .phabricator-main-menu-group { - clear: both; width: 100%; - overflow: hidden; - border-bottom: 1px solid #33393d; display: block; } +.device-tablet .phabricator-main-menu-group + .phabricator-main-menu-group, +.device-phone .phabricator-main-menu-group + .phabricator-main-menu-group { + margin-top: 1px; +} + /* - Logo ---------------------------------------------------------------------- @@ -72,7 +77,7 @@ */ -.phabricator-main-menu-group-logo { +.device-desktop .phabricator-main-menu-group-logo { float: left; } @@ -144,8 +149,6 @@ margin: 9px; display: inline-block; background-repeat: no-repeat; - position: relative; - overflow: hidden; } .device-desktop .phabricator-main-menu-icon-label { @@ -156,18 +159,17 @@ .device-phone .phabricator-main-menu-icon-label { font-weight: bold; color: #ffffff; - margin-left: 40px; - height: 26px; - margin: 15px 9px 3px 60px; + position: absolute; display: block; + height: 26px; + padding: 15px 0 3px; + left: 60px; + right: 0px; + top: 0px; } .device-tablet .phabricator-main-menu-icon, .device-phone .phabricator-main-menu-icon { - font-weight: bold; - color: white; - text-decoration: none; - border: 0; margin-left: 24px; position: absolute; } @@ -189,6 +191,23 @@ height: 24px; } +.phabricator-main-menu-search-target { + position: absolute; + top: 46px; +} + +.device-desktop .phabricator-main-menu-search-target { + width: 320px; + margin-left: -150px; +} + +.device-tablet .phabricator-main-menu-search-target, +.device-phone .phabricator-main-menu-search-target { + width: 100%; + margin-left: -25px; + +} + .device-desktop .phabricator-main-menu-search-container { margin: 0 8px 0 50px; } @@ -237,6 +256,50 @@ right: 6px; } +.phabricator-main-menu-search-target div.jx-typeahead-results { + border-radius: 4px; + box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.35); + border: 1px solid #33393d; +} + +.phabricator-main-menu-search-target div.jx-typeahead-results a.jx-result { + border: 0; +} + +.phabricator-main-menu-search-target div.jx-typeahead-results a.focused, +.phabricator-main-menu-search-target div.jx-typeahead-results a:hover { + background: #3875d7; +} + +.phabricator-main-search-typeahead-result { + display: block; + padding: 4px 4px 4px 38px; + background-position: 4px 4px; + background-size: 25px 25px; + background-repeat: no-repeat; +} + +.phabricator-main-search-typeahead-result .result-name { + display: block; + font-weight: bold; + color: #444444; +} + +.focused .phabricator-main-search-typeahead-result .result-name, +a:hover .phabricator-main-search-typeahead-result .result-name { + color: #eeeeee; +} + +.phabricator-main-search-typeahead-result .result-type { + color: #888888; +} + +.focused .phabricator-main-search-typeahead-result .result-type, +a:hover .phabricator-main-search-typeahead-result .result-type { + color: #dddddd; +} + + /* - Collapsible --------------------------------------------------------------- diff --git a/webroot/rsrc/js/application/core/behavior-search-typeahead.js b/webroot/rsrc/js/application/core/behavior-search-typeahead.js new file mode 100644 index 0000000000..1772878cdc --- /dev/null +++ b/webroot/rsrc/js/application/core/behavior-search-typeahead.js @@ -0,0 +1,52 @@ +/** + * @provides javelin-behavior-phabricator-search-typeahead + * @requires javelin-behavior + * javelin-typeahead-ondemand-source + * javelin-typeahead + * javelin-dom + * javelin-uri + * javelin-stratcom + */ + +JX.behavior('phabricator-search-typeahead', function(config) { + + var datasource = new JX.TypeaheadOnDemandSource(config.src); + + function transform(object) { + var attr = { + className: 'phabricator-main-search-typeahead-result' + } + + if (object[6]) { + attr.style = {backgroundImage: 'url('+object[6]+')'}; + } + + var render = JX.$N( + 'span', + attr, + [ + JX.$N('span', {className: 'result-name'}, object[4] || object[0]), + JX.$N('span', {className: 'result-type'}, object[5]) + ]); + + return { + name : object[0], + display : render, + uri : object[1], + id : object[2] + }; + } + + datasource.setTransformer(transform); + + var typeahead = new JX.Typeahead(JX.$(config.id), JX.$(config.input)); + typeahead.setDatasource(datasource); + typeahead.setPlaceholder(config.placeholder); + + typeahead.listen('choose', function(r) { + JX.$U(r.href).go(); + JX.Stratcom.context().kill(); + }); + + typeahead.start(); +});