1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-28 16:30:59 +01:00

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
This commit is contained in:
epriestley 2015-04-22 14:31:36 -07:00
parent d8b4f32d04
commit 3a2c2ae3c3
18 changed files with 403 additions and 75 deletions

View file

@ -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',

View file

@ -582,4 +582,8 @@ abstract class PhabricatorApplication implements PhabricatorPolicyInterface {
}
}
public function getApplicationSearchDocumentTypes() {
return array();
}
}

View file

@ -205,4 +205,10 @@ EOTEXT
);
}
public function getApplicationSearchDocumentTypes() {
return array(
DifferentialRevisionPHIDType::TYPECONST,
);
}
}

View file

@ -161,4 +161,10 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication {
);
}
public function getApplicationSearchDocumentTypes() {
return array(
PhabricatorRepositoryCommitPHIDType::TYPECONST,
);
}
}

View file

@ -62,4 +62,10 @@ final class PhabricatorFundApplication extends PhabricatorApplication {
);
}
public function getApplicationSearchDocumentTypes() {
return array(
FundInitiativePHIDType::TYPECONST,
);
}
}

View file

@ -158,4 +158,10 @@ final class PhabricatorManiphestApplication extends PhabricatorApplication {
);
}
public function getApplicationSearchDocumentTypes() {
return array(
ManiphestTaskPHIDType::TYPECONST,
);
}
}

View file

@ -57,4 +57,10 @@ final class PhabricatorPassphraseApplication extends PhabricatorApplication {
);
}
public function getApplicationSearchDocumentTypes() {
return array(
PassphraseCredentialPHIDType::TYPECONST,
);
}
}

View file

@ -192,4 +192,10 @@ final class PhabricatorPeopleApplication extends PhabricatorApplication {
return $items;
}
public function getApplicationSearchDocumentTypes() {
return array(
PhabricatorPeopleUserPHIDType::TYPECONST,
);
}
}

View file

@ -90,4 +90,10 @@ final class PhabricatorPholioApplication extends PhabricatorApplication {
);
}
public function getApplicationSearchDocumentTypes() {
return array(
PholioMockPHIDType::TYPECONST,
);
}
}

View file

@ -69,4 +69,10 @@ final class PhabricatorPhrictionApplication extends PhabricatorApplication {
return 0.140;
}
public function getApplicationSearchDocumentTypes() {
return array(
PhrictionDocumentPHIDType::TYPECONST,
);
}
}

View file

@ -79,4 +79,10 @@ final class PhabricatorPonderApplication extends PhabricatorApplication {
);
}
public function getApplicationSearchDocumentTypes() {
return array(
PonderQuestionPHIDType::TYPECONST,
);
}
}

View file

@ -133,4 +133,10 @@ final class PhabricatorProjectApplication extends PhabricatorApplication {
);
}
public function getApplicationSearchDocumentTypes() {
return array(
PhabricatorProjectProjectPHIDType::TYPECONST,
);
}
}

View file

@ -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())

View file

@ -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';

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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();
});
});