mirror of
https://we.phorge.it/source/phorge.git
synced 2025-01-14 16:51:08 +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',
|
'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' =>
|
'javelin-behavior-phabricator-tooltips' =>
|
||||||
array(
|
array(
|
||||||
'uri' => '/res/49f92a92/rsrc/js/application/core/behavior-tooltip.js',
|
'uri' => '/res/49f92a92/rsrc/js/application/core/behavior-tooltip.js',
|
||||||
|
@ -2263,7 +2278,7 @@ celerity_register_resource_map(array(
|
||||||
),
|
),
|
||||||
'phabricator-main-menu-view' =>
|
'phabricator-main-menu-view' =>
|
||||||
array(
|
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',
|
'type' => 'css',
|
||||||
'requires' =>
|
'requires' =>
|
||||||
array(
|
array(
|
||||||
|
|
|
@ -30,15 +30,35 @@ abstract class PhabricatorApplication {
|
||||||
|
|
||||||
|
|
||||||
public function getName() {
|
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() {
|
public function isEnabled() {
|
||||||
return true;
|
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() {
|
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();
|
$request = $this->getRequest();
|
||||||
$query = $request->getStr('q');
|
$query = $request->getStr('q');
|
||||||
|
|
||||||
|
$need_rich_data = false;
|
||||||
|
|
||||||
$need_users = false;
|
$need_users = false;
|
||||||
|
$need_applications = false;
|
||||||
$need_all_users = false;
|
$need_all_users = false;
|
||||||
$need_lists = false;
|
$need_lists = false;
|
||||||
$need_projs = false;
|
$need_projs = false;
|
||||||
|
@ -38,6 +41,11 @@ final class PhabricatorTypeaheadCommonDatasourceController
|
||||||
$need_arcanist_projects = false;
|
$need_arcanist_projects = false;
|
||||||
$need_noproject = false;
|
$need_noproject = false;
|
||||||
switch ($this->type) {
|
switch ($this->type) {
|
||||||
|
case 'mainsearch':
|
||||||
|
$need_users = true;
|
||||||
|
$need_applications = true;
|
||||||
|
$need_rich_data = true;
|
||||||
|
break;
|
||||||
case 'searchowner':
|
case 'searchowner':
|
||||||
$need_users = true;
|
$need_users = true;
|
||||||
$need_upforgrabs = true;
|
$need_upforgrabs = true;
|
||||||
|
@ -78,9 +86,20 @@ final class PhabricatorTypeaheadCommonDatasourceController
|
||||||
case 'arcanistprojects':
|
case 'arcanistprojects':
|
||||||
$need_arcanist_projects = true;
|
$need_arcanist_projects = true;
|
||||||
break;
|
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();
|
$data = array();
|
||||||
|
|
||||||
if ($need_upforgrabs) {
|
if ($need_upforgrabs) {
|
||||||
|
@ -106,11 +125,16 @@ final class PhabricatorTypeaheadCommonDatasourceController
|
||||||
'userName',
|
'userName',
|
||||||
'realName',
|
'realName',
|
||||||
'phid');
|
'phid');
|
||||||
|
|
||||||
|
if ($need_rich_data) {
|
||||||
|
$columns[] = 'profileImagePHID';
|
||||||
|
}
|
||||||
|
|
||||||
if ($query) {
|
if ($query) {
|
||||||
$conn_r = id(new PhabricatorUser())->establishConnection('r');
|
$conn_r = id(new PhabricatorUser())->establishConnection('r');
|
||||||
$ids = queryfx_all(
|
$ids = queryfx_all(
|
||||||
$conn_r,
|
$conn_r,
|
||||||
'SELECT DISTINCT userID FROM %T WHERE token LIKE %>',
|
'SELECT DISTINCT userID FROM %T WHERE token LIKE %> OR 1 = 1',
|
||||||
PhabricatorUser::NAMETOKEN_TABLE,
|
PhabricatorUser::NAMETOKEN_TABLE,
|
||||||
$query);
|
$query);
|
||||||
$ids = ipull($ids, 'userID');
|
$ids = ipull($ids, 'userID');
|
||||||
|
@ -125,6 +149,12 @@ final class PhabricatorTypeaheadCommonDatasourceController
|
||||||
} else {
|
} else {
|
||||||
$users = id(new PhabricatorUser())->loadColumns($columns);
|
$users = id(new PhabricatorUser())->loadColumns($columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($need_rich_data) {
|
||||||
|
$phids = mpull($users, 'getPHID');
|
||||||
|
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
if (!$need_all_users) {
|
if (!$need_all_users) {
|
||||||
if ($user->getIsSystemAgent()) {
|
if ($user->getIsSystemAgent()) {
|
||||||
|
@ -134,12 +164,18 @@ final class PhabricatorTypeaheadCommonDatasourceController
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$data[] = array(
|
$spec = array(
|
||||||
$user->getUsername().' ('.$user->getRealName().')',
|
$user->getUsername().' ('.$user->getRealName().')',
|
||||||
'/p/'.$user->getUsername(),
|
'/p/'.$user->getUsername(),
|
||||||
$user->getPHID(),
|
$user->getPHID(),
|
||||||
$user->getUsername(),
|
$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())
|
return id(new AphrontAjaxResponse())
|
||||||
->setContent($data);
|
->setContent($data);
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,7 @@ final class PhabricatorMainMenuSearchView extends AphrontView {
|
||||||
public function render() {
|
public function render() {
|
||||||
$user = $this->user;
|
$user = $this->user;
|
||||||
|
|
||||||
|
$target_id = celerity_generate_unique_node_id();
|
||||||
$search_id = $this->getID();
|
$search_id = $this->getID();
|
||||||
|
|
||||||
$input = phutil_render_tag(
|
$input = phutil_render_tag(
|
||||||
|
@ -49,16 +50,28 @@ final class PhabricatorMainMenuSearchView extends AphrontView {
|
||||||
array(
|
array(
|
||||||
'type' => 'text',
|
'type' => 'text',
|
||||||
'name' => 'query',
|
'name' => 'query',
|
||||||
'id' => $search_id,
|
'id' => $search_id,
|
||||||
|
'autocomplete' => 'off',
|
||||||
));
|
));
|
||||||
|
|
||||||
$scope = $this->scope;
|
$scope = $this->scope;
|
||||||
|
|
||||||
Javelin::initBehavior(
|
$target = javelin_render_tag(
|
||||||
'placeholder',
|
'div',
|
||||||
array(
|
array(
|
||||||
'id' => $search_id,
|
'id' => $target_id,
|
||||||
'text' => PhabricatorSearchScope::getScopePlaceholder($scope),
|
'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(
|
$scope_input = phutil_render_tag(
|
||||||
|
@ -79,6 +92,7 @@ final class PhabricatorMainMenuSearchView extends AphrontView {
|
||||||
$input.
|
$input.
|
||||||
'<button>Search</button>'.
|
'<button>Search</button>'.
|
||||||
$scope_input.
|
$scope_input.
|
||||||
|
$target.
|
||||||
'</div>');
|
'</div>');
|
||||||
|
|
||||||
$group = new PhabricatorMainMenuGroupView();
|
$group = new PhabricatorMainMenuGroupView();
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
background: #33393d;
|
background: #33393d;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.25);
|
box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.25);
|
||||||
overflow: hidden;
|
|
||||||
height: 44px;
|
height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,21 +48,27 @@
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.phabricator-main-menu-group {
|
||||||
|
height: 44px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.device-desktop .phabricator-main-menu-group {
|
.device-desktop .phabricator-main-menu-group {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
height: 44px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-tablet .phabricator-main-menu-group,
|
.device-tablet .phabricator-main-menu-group,
|
||||||
.device-phone .phabricator-main-menu-group {
|
.device-phone .phabricator-main-menu-group {
|
||||||
clear: both;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
|
||||||
border-bottom: 1px solid #33393d;
|
|
||||||
display: block;
|
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 ----------------------------------------------------------------------
|
/* - Logo ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -72,7 +77,7 @@
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.phabricator-main-menu-group-logo {
|
.device-desktop .phabricator-main-menu-group-logo {
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,8 +149,6 @@
|
||||||
margin: 9px;
|
margin: 9px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-desktop .phabricator-main-menu-icon-label {
|
.device-desktop .phabricator-main-menu-icon-label {
|
||||||
|
@ -156,18 +159,17 @@
|
||||||
.device-phone .phabricator-main-menu-icon-label {
|
.device-phone .phabricator-main-menu-icon-label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
margin-left: 40px;
|
position: absolute;
|
||||||
height: 26px;
|
|
||||||
margin: 15px 9px 3px 60px;
|
|
||||||
display: block;
|
display: block;
|
||||||
|
height: 26px;
|
||||||
|
padding: 15px 0 3px;
|
||||||
|
left: 60px;
|
||||||
|
right: 0px;
|
||||||
|
top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-tablet .phabricator-main-menu-icon,
|
.device-tablet .phabricator-main-menu-icon,
|
||||||
.device-phone .phabricator-main-menu-icon {
|
.device-phone .phabricator-main-menu-icon {
|
||||||
font-weight: bold;
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
border: 0;
|
|
||||||
margin-left: 24px;
|
margin-left: 24px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
@ -189,6 +191,23 @@
|
||||||
height: 24px;
|
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 {
|
.device-desktop .phabricator-main-menu-search-container {
|
||||||
margin: 0 8px 0 50px;
|
margin: 0 8px 0 50px;
|
||||||
}
|
}
|
||||||
|
@ -237,6 +256,50 @@
|
||||||
right: 6px;
|
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 ---------------------------------------------------------------
|
/* - 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