mirror of
https://we.phorge.it/source/phorge.git
synced 2025-01-14 00:31:05 +01:00
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
This commit is contained in:
parent
b43e6f2a5f
commit
852ecc2102
7 changed files with 261 additions and 26 deletions
|
@ -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(
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -24,5 +24,13 @@ final class PhabricatorApplicationDifferential extends PhabricatorApplication {
|
|||
);
|
||||
}
|
||||
|
||||
public function getBaseURI() {
|
||||
return '/differential/';
|
||||
}
|
||||
|
||||
public function getShortDescription() {
|
||||
return 'Code Review Application';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
'<button>Search</button>'.
|
||||
$scope_input.
|
||||
$target.
|
||||
'</div>');
|
||||
|
||||
$group = new PhabricatorMainMenuGroupView();
|
||||
|
|
|
@ -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 ---------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
Loading…
Reference in a new issue