diff --git a/rhodecode/apps/home/tests/__init__.py b/rhodecode/apps/home/tests/__init__.py --- a/rhodecode/apps/home/tests/__init__.py +++ b/rhodecode/apps/home/tests/__init__.py @@ -19,22 +19,44 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ -def assert_and_get_content(result): +def assert_and_get_main_filter_content(result): repos = [] groups = [] commits = [] + users = [] + for data_item in result: + assert data_item['id'] + assert data_item['value'] + assert data_item['value_display'] + assert data_item['url'] + + if data_item['type'] == 'search': + assert data_item['value_display'].startswith('Full text search for:') + elif data_item['type'] == 'repo': + repos.append(data_item) + elif data_item['type'] == 'repo_group': + groups.append(data_item) + elif data_item['type'] == 'user': + users.append(data_item) + elif data_item['type'] == 'commit': + commits.append(data_item) + else: + raise Exception('invalid type `%s`' % data_item['type']) + + return repos, groups, users, commits + + +def assert_and_get_repo_list_content(result): + repos = [] for data in result: for data_item in data['children']: assert data_item['id'] assert data_item['text'] assert data_item['url'] + if data_item['type'] == 'repo': repos.append(data_item) - elif data_item['type'] == 'group': - groups.append(data_item) - elif data_item['type'] == 'commit': - commits.append(data_item) else: raise Exception('invalid type %s' % data_item['type']) - return repos, groups, commits \ No newline at end of file + return repos diff --git a/rhodecode/apps/home/tests/test_get_goto_switched_data.py b/rhodecode/apps/home/tests/test_get_goto_switched_data.py --- a/rhodecode/apps/home/tests/test_get_goto_switched_data.py +++ b/rhodecode/apps/home/tests/test_get_goto_switched_data.py @@ -22,7 +22,7 @@ import json import pytest -from . import assert_and_get_content +from . import assert_and_get_main_filter_content from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN from rhodecode.tests.fixture import Fixture @@ -103,19 +103,15 @@ class TestGotoSwitcherData(TestControlle RepoGroupModel().delete(el, force_delete=True) Session().commit() - def test_returns_list_of_repos_and_groups(self, xhr_header): + def test_empty_query(self, xhr_header): self.log_user() response = self.app.get( route_path('goto_switcher_data'), extra_environ=xhr_header, status=200) - result = json.loads(response.body)['results'] - - repos, groups, commits = assert_and_get_content(result) + result = json.loads(response.body)['suggestions'] - assert len(repos) == len(Repository.get_all()) - assert len(groups) == len(RepoGroup.get_all()) - assert len(commits) == 0 + assert result == [] def test_returns_list_of_repos_and_groups_filtered(self, xhr_header): self.log_user() @@ -124,14 +120,47 @@ class TestGotoSwitcherData(TestControlle route_path('goto_switcher_data'), params={'query': 'abc'}, extra_environ=xhr_header, status=200) - result = json.loads(response.body)['results'] + result = json.loads(response.body)['suggestions'] - repos, groups, commits = assert_and_get_content(result) + repos, groups, users, commits = assert_and_get_main_filter_content(result) assert len(repos) == 13 assert len(groups) == 5 + assert len(users) == 0 assert len(commits) == 0 + def test_returns_list_of_users_filtered(self, xhr_header): + self.log_user() + + response = self.app.get( + route_path('goto_switcher_data'), + params={'query': 'user:admin'}, + extra_environ=xhr_header, status=200) + result = json.loads(response.body)['suggestions'] + + repos, groups, users, commits = assert_and_get_main_filter_content(result) + + assert len(repos) == 0 + assert len(groups) == 0 + assert len(users) == 1 + assert len(commits) == 0 + + def test_returns_list_of_commits_filtered(self, xhr_header): + self.log_user() + + response = self.app.get( + route_path('goto_switcher_data'), + params={'query': 'commit:e8'}, + extra_environ=xhr_header, status=200) + result = json.loads(response.body)['suggestions'] + + repos, groups, users, commits = assert_and_get_main_filter_content(result) + + assert len(repos) == 0 + assert len(groups) == 0 + assert len(users) == 0 + assert len(commits) == 5 + def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header): self.log_user() @@ -139,13 +168,13 @@ class TestGotoSwitcherData(TestControlle route_path('goto_switcher_data'), params={'query': 'abc'}, extra_environ=xhr_header, status=200) - result = json.loads(response.body)['results'] + result = json.loads(response.body)['suggestions'] - repos, groups, commits = assert_and_get_content(result) + repos, groups, users, commits = assert_and_get_main_filter_content(result) - test_repos = [x['text'] for x in repos[:4]] + test_repos = [x['value_display'] for x in repos[:4]] assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos - test_groups = [x['text'] for x in groups[:4]] + test_groups = [x['value_display'] for x in groups[:4]] assert ['abc_repos', 'repos_abc', 'forked-abc', 'forked-abc/a'] == test_groups diff --git a/rhodecode/apps/home/tests/test_get_repo_list_data.py b/rhodecode/apps/home/tests/test_get_repo_list_data.py --- a/rhodecode/apps/home/tests/test_get_repo_list_data.py +++ b/rhodecode/apps/home/tests/test_get_repo_list_data.py @@ -20,7 +20,7 @@ import json -from . import assert_and_get_content +from . import assert_and_get_repo_list_content from rhodecode.tests import TestController from rhodecode.tests.fixture import Fixture from rhodecode.model.db import Repository @@ -50,11 +50,9 @@ class TestRepoListData(TestController): extra_environ=xhr_header, status=200) result = json.loads(response.body)['results'] - repos, groups, commits = assert_and_get_content(result) + repos = assert_and_get_repo_list_content(result) assert len(repos) == len(Repository.get_all()) - assert len(groups) == 0 - assert len(commits) == 0 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header): self.log_user() @@ -65,12 +63,10 @@ class TestRepoListData(TestController): extra_environ=xhr_header, status=200) result = json.loads(response.body)['results'] - repos, groups, commits = assert_and_get_content(result) + repos = assert_and_get_repo_list_content(result) assert len(repos) == len(Repository.query().filter( Repository.repo_name.ilike('%vcs_test_git%')).all()) - assert len(groups) == 0 - assert len(commits) == 0 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header): self.log_user() @@ -81,12 +77,10 @@ class TestRepoListData(TestController): extra_environ=xhr_header, status=200) result = json.loads(response.body)['results'] - repos, groups, commits = assert_and_get_content(result) + repos = assert_and_get_repo_list_content(result) assert len(repos) == len(Repository.query().filter( Repository.repo_name.ilike('%vcs_test_git%')).all()) - assert len(groups) == 0 - assert len(commits) == 0 def test_returns_list_of_repos_non_ascii_query(self, xhr_header): self.log_user() @@ -96,8 +90,6 @@ class TestRepoListData(TestController): extra_environ=xhr_header, status=200) result = json.loads(response.body)['results'] - repos, groups, commits = assert_and_get_content(result) + repos = assert_and_get_repo_list_content(result) assert len(repos) == 0 - assert len(groups) == 0 - assert len(commits) == 0 diff --git a/rhodecode/apps/home/views.py b/rhodecode/apps/home/views.py --- a/rhodecode/apps/home/views.py +++ b/rhodecode/apps/home/views.py @@ -20,6 +20,7 @@ import re import logging +import collections from pyramid.view import view_config @@ -31,7 +32,7 @@ from rhodecode.lib.index import searcher from rhodecode.lib.utils2 import safe_unicode, str2bool from rhodecode.lib.ext_json import json from rhodecode.model.db import ( - func, or_, in_filter_generator, Repository, RepoGroup) + func, or_, in_filter_generator, Repository, RepoGroup, User, UserGroup) from rhodecode.model.repo import RepoModel from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.scm import RepoGroupList, RepoList @@ -104,6 +105,7 @@ class HomeView(BaseAppView): return {'suggestions': _user_groups} def _get_repo_list(self, name_contains=None, repo_type=None, limit=20): + org_query = name_contains allowed_ids = self._rhodecode_user.repo_acl_ids( ['repository.read', 'repository.write', 'repository.admin'], cache=False, name_filter=name_contains) or [-1] @@ -125,20 +127,24 @@ class HomeView(BaseAppView): Repository.repo_name.ilike(ilike_expression)) query = query.limit(limit) - acl_repo_iter = query + acl_iter = query return [ { 'id': obj.repo_name, + 'value': org_query, + 'value_display': obj.repo_name, 'text': obj.repo_name, 'type': 'repo', - 'obj': {'repo_type': obj.repo_type, 'private': obj.private, - 'repo_id': obj.repo_id}, + 'repo_id': obj.repo_id, + 'repo_type': obj.repo_type, + 'private': obj.private, 'url': h.route_path('repo_summary', repo_name=obj.repo_name) } - for obj in acl_repo_iter] + for obj in acl_iter] def _get_repo_group_list(self, name_contains=None, limit=20): + org_query = name_contains allowed_ids = self._rhodecode_user.repo_group_acl_ids( ['group.read', 'group.write', 'group.admin'], cache=False, name_filter=name_contains) or [-1] @@ -157,20 +163,56 @@ class HomeView(BaseAppView): RepoGroup.group_name.ilike(ilike_expression)) query = query.limit(limit) - acl_repo_iter = query + acl_iter = query return [ { 'id': obj.group_name, - 'text': obj.group_name, - 'type': 'group', - 'obj': {}, + 'value': org_query, + 'value_display': obj.group_name, + 'type': 'repo_group', 'url': h.route_path( 'repo_group_home', repo_group_name=obj.group_name) } - for obj in acl_repo_iter] + for obj in acl_iter] + + def _get_user_list(self, name_contains=None, limit=20): + org_query = name_contains + if not name_contains: + return [] + + name_contains = re.compile('(?:user:)(.+)').findall(name_contains) + if len(name_contains) != 1: + return [] + name_contains = name_contains[0] + + query = User.query()\ + .order_by(func.length(User.username))\ + .order_by(User.username) \ + .filter(User.username != User.DEFAULT_USER) - def _get_hash_commit_list(self, auth_user, query=None): + if name_contains: + ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) + query = query.filter( + User.username.ilike(ilike_expression)) + query = query.limit(limit) + + acl_iter = query + + return [ + { + 'id': obj.user_id, + 'value': org_query, + 'value_display': obj.username, + 'type': 'user', + 'icon_link': h.gravatar_url(obj.email, 30), + 'url': h.route_path( + 'user_profile', username=obj.username) + } + for obj in acl_iter] + + def _get_hash_commit_list(self, auth_user, query): + org_query = query if not query or len(query) < 3: return [] @@ -178,20 +220,21 @@ class HomeView(BaseAppView): if len(commit_hashes) != 1: return [] - - commit_hash_prefix = commit_hashes[0] + commit_hash = commit_hashes[0] searcher = searcher_from_config(self.request.registry.settings) result = searcher.search( - 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user, + 'commit_id:%s*' % commit_hash, 'commit', auth_user, raise_on_exc=False) return [ { 'id': entry['commit_id'], - 'text': entry['commit_id'], + 'value': org_query, + 'value_display': 'repo `{}` commit: {}'.format( + entry['repository'], entry['commit_id']), 'type': 'commit', - 'obj': {'repo': entry['repository']}, + 'repo': entry['repository'], 'url': h.route_path( 'repo_commit', repo_name=entry['repository'], commit_id=entry['commit_id']) @@ -235,41 +278,47 @@ class HomeView(BaseAppView): _ = self.request.translate query = self.request.GET.get('query') - log.debug('generating goto switcher list, query %s', query) + log.debug('generating main filter data, query %s', query) + + default_search_val = 'Full text search for: `{}`'.format(query) + res = [] + if not query: + return {'suggestions': res} - res = [] + res.append({ + 'id': -1, + 'value': query, + 'value_display': default_search_val, + 'type': 'search', + 'url': h.route_path( + 'search', _query={'q': query}) + }) + repo_groups = self._get_repo_group_list(query) - if repo_groups: - res.append({ - 'text': _('Groups'), - 'children': repo_groups - }) + for serialized_repo_group in repo_groups: + res.append(serialized_repo_group) repos = self._get_repo_list(query) - if repos: - res.append({ - 'text': _('Repositories'), - 'children': repos - }) + for serialized_repo in repos: + res.append(serialized_repo) + + # TODO(marcink): permissions for that ? + users = self._get_user_list(query) + for serialized_user in users: + res.append(serialized_user) commits = self._get_hash_commit_list(c.auth_user, query) if commits: - unique_repos = {} + unique_repos = collections.OrderedDict() for commit in commits: - unique_repos.setdefault(commit['obj']['repo'], [] - ).append(commit) + repo_name = commit['repo'] + unique_repos.setdefault(repo_name, []).append(commit) - for repo in unique_repos: - res.append({ - 'text': _('Commits in %(repo)s') % {'repo': repo}, - 'children': unique_repos[repo] - }) + for repo, commits in unique_repos.items(): + for commit in commits: + res.append(commit) - data = { - 'more': False, - 'results': res - } - return data + return {'suggestions': res} def _get_groups_and_repos(self, repo_group_id=None): # repo groups groups diff --git a/rhodecode/apps/repository/views/repo_pull_requests.py b/rhodecode/apps/repository/views/repo_pull_requests.py --- a/rhodecode/apps/repository/views/repo_pull_requests.py +++ b/rhodecode/apps/repository/views/repo_pull_requests.py @@ -766,7 +766,10 @@ class RepoPullRequestsView(RepoAppView, 'id': obj['name'], 'text': obj['name'], 'type': 'repo', - 'obj': obj['dbrepo'] + 'repo_id': obj['dbrepo']['repo_id'], + 'repo_type': obj['dbrepo']['repo_type'], + 'private': obj['dbrepo']['private'], + }) data = { diff --git a/rhodecode/public/css/navigation.less b/rhodecode/public/css/navigation.less --- a/rhodecode/public/css/navigation.less +++ b/rhodecode/public/css/navigation.less @@ -281,11 +281,9 @@ } .navigation li.open { - - .submenu, - .repo_switcher { - display: block; - } + .submenu { + display: block; + } } .navigation li:last-child .submenu { @@ -642,3 +640,45 @@ ul#context-pages { } } + +.main_filter_help_box { + padding: 7px 7px; + border-top: 1px solid @grey4; + border-right: 1px solid @grey4; + border-bottom: 1px solid @grey4; + display: inline-block; + vertical-align: top; + margin-left: -5px; + background: @grey3; +} + +.main_filter_input_box { + display: inline-block; +} + +.main_filter_box { + margin: 9px 0 0 0; +} + +#main_filter_help { + background: @grey3; + border: 1px solid black; + position: absolute; + white-space: pre-wrap; + z-index: 9999; + color: @nav-grey; + margin: 1px 7px; + padding: 0 2px; +} + +.main_filter_input { + padding: 6px; + min-width: 220px; + color: @nav-grey; + background: @grey3; +} + +.main_filter_input::placeholder { + color: @nav-grey; + opacity: 1; +} diff --git a/rhodecode/public/css/type.less b/rhodecode/public/css/type.less --- a/rhodecode/public/css/type.less +++ b/rhodecode/public/css/type.less @@ -228,7 +228,7 @@ mark, clear: both; float: left; width: 100%; - margin: @pagepadding 0 @pagepadding; + margin: @pagepadding/2 0 @pagepadding; .breadcrumbs{ float: left; diff --git a/rhodecode/public/js/rhodecode/base/keyboard-bindings.js b/rhodecode/public/js/rhodecode/base/keyboard-bindings.js --- a/rhodecode/public/js/rhodecode/base/keyboard-bindings.js +++ b/rhodecode/public/js/rhodecode/base/keyboard-bindings.js @@ -20,7 +20,7 @@ function setRCMouseBindings(repoName, re // / open the quick filter Mousetrap.bind(['/'], function(e) { - $('#repo_switcher').select2('open'); + $('#main_filter').get(0).focus(); // return false to prevent default browser behavior // and stop event from bubbling diff --git a/rhodecode/templates/admin/my_account/my_account_auth_tokens.mako b/rhodecode/templates/admin/my_account/my_account_auth_tokens.mako --- a/rhodecode/templates/admin/my_account/my_account_auth_tokens.mako +++ b/rhodecode/templates/admin/my_account/my_account_auth_tokens.mako @@ -131,7 +131,7 @@ var repoFilter = function(data) { $.each(data.results[0].children, function() { // replace name to ID for submision - this.id = this.obj.repo_id; + this.id = this.repo_id; results.push(this); }); @@ -154,7 +154,7 @@ var selectVcsScope = function() { dropdownAutoWidth: true, containerCssClass: "drop-menu", dropdownCssClass: "drop-menu-dropdown", - formatResult: formatResult, + formatResult: formatRepoResult, query: $.debounce(250, function(query){ self = this; var cacheKey = query.term; diff --git a/rhodecode/templates/admin/repos/repo_edit_advanced.mako b/rhodecode/templates/admin/repos/repo_edit_advanced.mako --- a/rhodecode/templates/admin/repos/repo_edit_advanced.mako +++ b/rhodecode/templates/admin/repos/repo_edit_advanced.mako @@ -165,8 +165,8 @@ var repoTypeFilter = function(data) { $.each(data.results[0].children, function() { // filter out the SAME repo, it cannot be used as fork of itself - if (this.obj.repo_id != currentRepoId) { - this.id = this.obj.repo_id; + if (this.repo_id != currentRepoId) { + this.id = this.repo_id; results.push(this) } }); @@ -181,7 +181,7 @@ var repoTypeFilter = function(data) { dropdownAutoWidth: true, containerCssClass: "drop-menu", dropdownCssClass: "drop-menu-dropdown", - formatResult: formatResult, + formatResult: formatRepoResult, query: $.debounce(250, function(query){ self = this; var cacheKey = query.term; diff --git a/rhodecode/templates/admin/users/user_edit_auth_tokens.mako b/rhodecode/templates/admin/users/user_edit_auth_tokens.mako --- a/rhodecode/templates/admin/users/user_edit_auth_tokens.mako +++ b/rhodecode/templates/admin/users/user_edit_auth_tokens.mako @@ -129,7 +129,7 @@ var repoFilter = function(data) { $.each(data.results[0].children, function() { // replace name to ID for submision - this.id = this.obj.repo_id; + this.id = this.repo_id; results.push(this); }); @@ -152,7 +152,7 @@ var selectVcsScope = function() { dropdownAutoWidth: true, containerCssClass: "drop-menu", dropdownCssClass: "drop-menu-dropdown", - formatResult: formatResult, + formatResult: formatRepoResult, query: $.debounce(250, function(query){ self = this; var cacheKey = query.term; diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako --- a/rhodecode/templates/base/base.mako +++ b/rhodecode/templates/base/base.mako @@ -226,7 +226,7 @@