# HG changeset patch # User Marcin Kuzminski # Date 2019-03-13 11:12:01 # Node ID a75b51f85a1baf1ffab465d984ca3b2081388fb5 # Parent f42cdfb774057062705c75429c8fb292eb1d1d66 go-to search: updated logic of goto switcher - when in repo group searcher are ordered by the entries in the group - support prefixes with spaces, eg file: PATTERN - new contextual search for groups/repos with mark token - better suggestions 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 @@ -33,7 +33,7 @@ from rhodecode.lib.index import searcher from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int from rhodecode.lib.ext_json import json from rhodecode.model.db import ( - func, true, or_, in_filter_generator, Repository, RepoGroup, User, UserGroup) + func, true, or_, case, 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 @@ -105,21 +105,27 @@ class HomeView(BaseAppView): return {'suggestions': _user_groups} - def _get_repo_list(self, name_contains=None, repo_type=None, limit=20): + def _get_repo_list(self, name_contains=None, repo_type=None, repo_group_name='', 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] query = Repository.query()\ - .order_by(func.length(Repository.repo_name))\ - .order_by(Repository.repo_name)\ .filter(Repository.archived.isnot(true()))\ .filter(or_( # generate multiple IN to fix limitation problems *in_filter_generator(Repository.repo_id, allowed_ids) )) + query = query.order_by(case( + [ + (Repository.repo_name.startswith(repo_group_name), repo_group_name+'/'), + ], + )) + query = query.order_by(func.length(Repository.repo_name)) + query = query.order_by(Repository.repo_name) + if repo_type: query = query.filter(Repository.repo_type == repo_type) @@ -145,20 +151,26 @@ class HomeView(BaseAppView): } for obj in acl_iter] - def _get_repo_group_list(self, name_contains=None, limit=20): + def _get_repo_group_list(self, name_contains=None, repo_group_name='', 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] query = RepoGroup.query()\ - .order_by(func.length(RepoGroup.group_name))\ - .order_by(RepoGroup.group_name) \ .filter(or_( # generate multiple IN to fix limitation problems *in_filter_generator(RepoGroup.group_id, allowed_ids) )) + query = query.order_by(case( + [ + (RepoGroup.group_name.startswith(repo_group_name), repo_group_name+'/'), + ], + )) + query = query.order_by(func.length(RepoGroup.group_name)) + query = query.order_by(RepoGroup.group_name) + if name_contains: ilike_expression = u'%{}%'.format(safe_unicode(name_contains)) query = query.filter( @@ -183,11 +195,17 @@ class HomeView(BaseAppView): def _get_user_list(self, name_contains=None, limit=20): org_query = name_contains if not name_contains: - return [] + return [], False - name_contains = re.compile('(?:user:)(.+)').findall(name_contains) + # TODO(marcink): should all logged in users be allowed to search others? + allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER + if not allowed_user_search: + return [], False + + name_contains = re.compile('(?:user:[ ]?)(.+)').findall(name_contains) if len(name_contains) != 1: - return [] + return [], False + name_contains = name_contains[0] query = User.query()\ @@ -207,22 +225,28 @@ class HomeView(BaseAppView): { 'id': obj.user_id, 'value': org_query, - 'value_display': obj.username, + 'value_display': 'user: `{}`'.format(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] + for obj in acl_iter], True def _get_user_groups_list(self, name_contains=None, limit=20): org_query = name_contains if not name_contains: - return [] + return [], False - name_contains = re.compile('(?:user_group:)(.+)').findall(name_contains) + # TODO(marcink): should all logged in users be allowed to search others? + allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER + if not allowed_user_search: + return [], False + + name_contains = re.compile('(?:user_group:[ ]?)(.+)').findall(name_contains) if len(name_contains) != 1: - return [] + return [], False + name_contains = name_contains[0] query = UserGroup.query()\ @@ -241,27 +265,34 @@ class HomeView(BaseAppView): { 'id': obj.users_group_id, 'value': org_query, - 'value_display': obj.users_group_name, + 'value_display': 'user_group: `{}`'.format(obj.users_group_name), 'type': 'user_group', 'url': h.route_path( 'user_group_profile', user_group_name=obj.users_group_name) } - for obj in acl_iter] + for obj in acl_iter], True - def _get_hash_commit_list(self, auth_user, searcher, query): + def _get_hash_commit_list(self, auth_user, searcher, query, repo=None, repo_group=None): + repo_name = repo_group_name = None + if repo: + repo_name = repo.repo_name + if repo_group: + repo_group_name = repo_group.group_name + org_query = query if not query or len(query) < 3 or not searcher: - return [] + return [], False - commit_hashes = re.compile('(?:commit:)([0-9a-f]{2,40})').findall(query) + commit_hashes = re.compile('(?:commit:[ ]?)([0-9a-f]{2,40})').findall(query) if len(commit_hashes) != 1: - return [] + return [], False + commit_hash = commit_hashes[0] result = searcher.search( 'commit_id:{}*'.format(commit_hash), 'commit', auth_user, - raise_on_exc=False) + repo_name, repo_group_name, raise_on_exc=False) commits = [] for entry in result['results']: @@ -286,22 +317,29 @@ class HomeView(BaseAppView): } commits.append(commit_entry) - return commits + return commits, True - def _get_path_list(self, auth_user, searcher, query): + def _get_path_list(self, auth_user, searcher, query, repo=None, repo_group=None): + repo_name = repo_group_name = None + if repo: + repo_name = repo.repo_name + if repo_group: + repo_group_name = repo_group.group_name + org_query = query if not query or len(query) < 3 or not searcher: - return [] + return [], False - paths_re = re.compile('(?:file:)(.{1,})').findall(query) + paths_re = re.compile('(?:file:[ ]?)(.+)').findall(query) if len(paths_re) != 1: - return [] + return [], False + file_path = paths_re[0] search_path = searcher.escape_specials(file_path) result = searcher.search( 'file.raw:*{}*'.format(search_path), 'path', auth_user, - raise_on_exc=False) + repo_name, repo_group_name, raise_on_exc=False) files = [] for entry in result['results']: @@ -327,7 +365,7 @@ class HomeView(BaseAppView): } files.append(file_entry) - return files + return files, True @LoginRequired() @view_config( @@ -405,17 +443,15 @@ class HomeView(BaseAppView): qry = query return {'q': qry, 'type': 'content'} label = u'File search for `{}` in this repository.'.format(query) - queries.append( - { - 'id': -10, - 'value': query, - 'value_display': label, - 'type': 'search', - 'url': h.route_path('search_repo', - repo_name=repo_name, - _query=query_modifier()) + file_qry = { + 'id': -10, + 'value': query, + 'value_display': label, + 'type': 'search', + 'url': h.route_path('search_repo', + repo_name=repo_name, + _query=query_modifier()) } - ) # commits def query_modifier(): @@ -423,17 +459,22 @@ class HomeView(BaseAppView): return {'q': qry, 'type': 'commit'} label = u'Commit search for `{}` in this repository.'.format(query) - queries.append( - { - 'id': -20, - 'value': query, - 'value_display': label, - 'type': 'search', - 'url': h.route_path('search_repo', - repo_name=repo_name, - _query=query_modifier()) + commit_qry = { + 'id': -20, + 'value': query, + 'value_display': label, + 'type': 'search', + 'url': h.route_path('search_repo', + repo_name=repo_name, + _query=query_modifier()) } - ) + + if repo_context in ['commit', 'changelog']: + queries.extend([commit_qry, file_qry]) + elif repo_context in ['files', 'summary']: + queries.extend([file_qry, commit_qry]) + else: + queries.extend([commit_qry, file_qry]) elif is_es_6 and repo_group_name: # files @@ -442,17 +483,15 @@ class HomeView(BaseAppView): return {'q': qry, 'type': 'content'} label = u'File search for `{}` in this repository group'.format(query) - queries.append( - { - 'id': -30, - 'value': query, - 'value_display': label, - 'type': 'search', - 'url': h.route_path('search_repo_group', - repo_group_name=repo_group_name, - _query=query_modifier()) + file_qry = { + 'id': -30, + 'value': query, + 'value_display': label, + 'type': 'search', + 'url': h.route_path('search_repo_group', + repo_group_name=repo_group_name, + _query=query_modifier()) } - ) # commits def query_modifier(): @@ -460,18 +499,24 @@ class HomeView(BaseAppView): return {'q': qry, 'type': 'commit'} label = u'Commit search for `{}` in this repository group'.format(query) - queries.append( - { - 'id': -40, - 'value': query, - 'value_display': label, - 'type': 'search', - 'url': h.route_path('search_repo_group', - repo_group_name=repo_group_name, - _query=query_modifier()) + commit_qry = { + 'id': -40, + 'value': query, + 'value_display': label, + 'type': 'search', + 'url': h.route_path('search_repo_group', + repo_group_name=repo_group_name, + _query=query_modifier()) } - ) + if repo_context in ['commit', 'changelog']: + queries.extend([commit_qry, file_qry]) + elif repo_context in ['files', 'summary']: + queries.extend([file_qry, commit_qry]) + else: + queries.extend([commit_qry, file_qry]) + + # Global, not scoped if not queries: queries.append( { @@ -510,65 +555,107 @@ class HomeView(BaseAppView): if not query: return {'suggestions': res} + def no_match(name): + return { + 'id': -1, + 'value': "", + 'value_display': name, + 'type': 'text', + 'url': "" + } searcher = searcher_from_config(self.request.registry.settings) - for _q in self._get_default_search_queries(self.request.GET, searcher, query): - res.append(_q) + has_specialized_search = False + # set repo context + repo = None + repo_id = safe_int(self.request.GET.get('search_context[repo_id]')) + if repo_id: + repo = Repository.get(repo_id) + + # set group context + repo_group = None repo_group_id = safe_int(self.request.GET.get('search_context[repo_group_id]')) if repo_group_id: repo_group = RepoGroup.get(repo_group_id) - composed_hint = '{}/{}'.format(repo_group.group_name, query) - show_hint = not query.startswith(repo_group.group_name) - if repo_group and show_hint: - hint = u'Repository search inside: `{}`'.format(composed_hint) - res.append({ - 'id': -1, - 'value': composed_hint, - 'value_display': hint, - 'type': 'hint', - 'url': "" - }) + prefix_match = False + + # user: type search + if not prefix_match: + users, prefix_match = self._get_user_list(query) + if users: + has_specialized_search = True + for serialized_user in users: + res.append(serialized_user) + elif prefix_match: + has_specialized_search = True + res.append(no_match('No matching users found')) - repo_groups = self._get_repo_group_list(query) - for serialized_repo_group in repo_groups: - res.append(serialized_repo_group) + # user_group: type search + if not prefix_match: + user_groups, prefix_match = self._get_user_groups_list(query) + if user_groups: + has_specialized_search = True + for serialized_user_group in user_groups: + res.append(serialized_user_group) + elif prefix_match: + has_specialized_search = True + res.append(no_match('No matching user groups found')) - repos = self._get_repo_list(query) - for serialized_repo in repos: - res.append(serialized_repo) + # FTS commit: type search + if not prefix_match: + commits, prefix_match = self._get_hash_commit_list( + c.auth_user, searcher, query, repo, repo_group) + if commits: + has_specialized_search = True + unique_repos = collections.OrderedDict() + for commit in commits: + repo_name = commit['repo'] + unique_repos.setdefault(repo_name, []).append(commit) - # TODO(marcink): should all logged in users be allowed to search others? - allowed_user_search = self._rhodecode_user.username != User.DEFAULT_USER - if allowed_user_search: - users = self._get_user_list(query) - for serialized_user in users: - res.append(serialized_user) + for _repo, commits in unique_repos.items(): + for commit in commits: + res.append(commit) + elif prefix_match: + has_specialized_search = True + res.append(no_match('No matching commits found')) - user_groups = self._get_user_groups_list(query) - for serialized_user_group in user_groups: - res.append(serialized_user_group) + # FTS file: type search + if not prefix_match: + paths, prefix_match = self._get_path_list( + c.auth_user, searcher, query, repo, repo_group) + if paths: + has_specialized_search = True + unique_repos = collections.OrderedDict() + for path in paths: + repo_name = path['repo'] + unique_repos.setdefault(repo_name, []).append(path) - commits = self._get_hash_commit_list(c.auth_user, searcher, query) - if commits: - unique_repos = collections.OrderedDict() - for commit in commits: - repo_name = commit['repo'] - unique_repos.setdefault(repo_name, []).append(commit) + for repo, paths in unique_repos.items(): + for path in paths: + res.append(path) + elif prefix_match: + has_specialized_search = True + res.append(no_match('No matching files found')) - for repo, commits in unique_repos.items(): - for commit in commits: - res.append(commit) + # main suggestions + if not has_specialized_search: + repo_group_name = '' + if repo_group: + repo_group_name = repo_group.group_name - paths = self._get_path_list(c.auth_user, searcher, query) - if paths: - unique_repos = collections.OrderedDict() - for path in paths: - repo_name = path['repo'] - unique_repos.setdefault(repo_name, []).append(path) + for _q in self._get_default_search_queries(self.request.GET, searcher, query): + res.append(_q) + + repo_groups = self._get_repo_group_list(query, repo_group_name=repo_group_name) + for serialized_repo_group in repo_groups: + res.append(serialized_repo_group) - for repo, paths in unique_repos.items(): - for path in paths: - res.append(path) + repos = self._get_repo_list(query, repo_group_name=repo_group_name) + for serialized_repo in repos: + res.append(serialized_repo) + + if not repos and not repo_groups: + res.append(no_match('No matches found')) return {'suggestions': res} diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -2037,6 +2037,6 @@ def get_repo_view_type(request): 'repo_files': 'files', 'repo_summary': 'summary', 'repo_commit': 'commit' + } - } return route_to_view_type.get(route_name) diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -39,7 +39,7 @@ from sqlalchemy import ( Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column, Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary, Text, Float, PickleType) -from sqlalchemy.sql.expression import true, false +from sqlalchemy.sql.expression import true, false, case from sqlalchemy.sql.functions import coalesce, count # pragma: no cover from sqlalchemy.orm import ( relationship, joinedload, class_mapper, validates, aliased) 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 @@ -644,6 +644,40 @@ ul#context-pages { .main_filter_input_box { display: inline-block; + + .searchItems { + display:flex; + background: #666666; + padding: 0px; + + a { + border: none !important; + } + } + + .searchTag { + line-height: 28px; + padding: 0px 4px; + + .tag { + color: @nav-grey; + border-color: @nav-grey; + } + } + + .searchTagFilter { + background-color: @grey3 !important; + } + + .searchTagHelp { + background-color: @grey2 !important; + } + .searchTagHelp:hover { + background-color: @grey2 !important; + } + .searchTagInput { + background-color: @grey3 !important; + } } .main_filter_box { @@ -667,7 +701,8 @@ ul#context-pages { color: @nav-grey; background: @grey3; min-height: 18px; - + border:none; + border-radius: 0; &:active { color: @grey2 !important; diff --git a/rhodecode/public/js/src/plugins/jquery.autocomplete.js b/rhodecode/public/js/src/plugins/jquery.autocomplete.js --- a/rhodecode/public/js/src/plugins/jquery.autocomplete.js +++ b/rhodecode/public/js/src/plugins/jquery.autocomplete.js @@ -528,7 +528,19 @@ if ($.isFunction(serviceUrl)) { serviceUrl = serviceUrl.call(that.element, query); } - cacheKey = serviceUrl + '?' + $.param(params || {}); + + var callParams = {}; + //make an evaluated copy of params + $.each(params, function(index, value) { + if($.isFunction(value)){ + callParams[index] = value(); + } + else { + callParams[index] = value; + } + }); + + cacheKey = serviceUrl + '?' + $.param(callParams); response = that.cachedResponse[cacheKey]; } @@ -536,7 +548,7 @@ that.suggestions = response.suggestions; that.suggest(); } else if (!that.isBadQuery(query)) { - if (options.onSearchStart.call(that.element, options.params) === false) { + if (options.onSearchStart.call(that.element, params) === false) { return; } if (that.currentRequest) { 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 @@ -233,7 +233,6 @@
  • -
  • ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()" %if c.rhodecode_db_repo.repo_type in ['git','hg']: @@ -338,7 +337,6 @@
    @@ -622,6 +653,22 @@ }(result, escapeMarkup); }; + var escapeRegExChars = function (value) { + return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }; + + var getRepoIcon = function(repo_type) { + if (repo_type === 'hg') { + return ' '; + } + else if (repo_type === 'git') { + return ' '; + } + else if (repo_type === 'svn') { + return ' '; + } + return '' + }; var autocompleteMainFilterFormatResult = function (data, value, org_formatter) { @@ -632,27 +679,14 @@ var searchType = data['type']; var valueDisplay = data['value_display']; - var escapeRegExChars = function (value) { - return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }; var pattern = '(' + escapeRegExChars(value) + ')'; - var getRepoIcon = function(repo_type) { - if (repo_type === 'hg') { - return ' '; - } - else if (repo_type === 'git') { - return ' '; - } - else if (repo_type === 'svn') { - return ' '; - } - return '' - }; + valueDisplay = Select2.util.escapeMarkup(valueDisplay); // highlight match - valueDisplay = Select2.util.escapeMarkup(valueDisplay); - valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '$1<\/strong>'); + if (searchType != 'text') { + valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '$1<\/strong>'); + } var icon = ''; @@ -684,6 +718,7 @@ else if (searchType === 'user_group') { icon += ' '; } + // user else if (searchType === 'user') { icon += ''.format(data['icon_link']); } @@ -707,6 +742,10 @@ icon += ''; } } + // generic text + else if (searchType === 'text') { + icon = ''; + } var tmpl = '
    {0}{1}
    '; return tmpl.format(icon, valueDisplay); @@ -716,25 +755,52 @@ if (suggestion.type === "hint") { // we skip action $('#main_filter').focus(); + } + else if (suggestion.type === "text") { + // we skip action + $('#main_filter').focus(); + } else { window.location = suggestion['url']; } }; + var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) { if (queryLowerCase.split(':').length === 2) { queryLowerCase = queryLowerCase.split(':')[1] } + if (suggestion.type === "text") { + // special case we don't want to "skip" display for + return true + } return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1; }; + var cleanContext = { + repo_view_type: null, + + repo_id: null, + repo_name: "", + + repo_group_id: null, + repo_group_name: null + }; + var removeGoToFilter = function () { + $('.searchTagHidable').hide(); + $('#main_filter').autocomplete( + 'setOptions', {params:{search_context: cleanContext}}); + }; + $('#main_filter').autocomplete({ serviceUrl: pyroutes.url('goto_switcher_data'), - params: {"search_context": templateContext.search_context}, + params: { + "search_context": templateContext.search_context + }, minChars:2, maxHeight:400, deferRequestBy: 300, //miliseconds tabDisabled: true, - autoSelectFirst: true, + autoSelectFirst: false, formatResult: autocompleteMainFilterFormatResult, lookupFilter: autocompleteMainFilterResult, onSelect: function (element, suggestion) { @@ -753,6 +819,18 @@ $('#main_filter_help').toggle(); }; + $('#main_filter').on('keydown.autocomplete', function (e) { + + var BACKSPACE = 8; + var el = $(e.currentTarget); + if(e.which === BACKSPACE){ + var inputVal = el.val(); + if (inputVal === ""){ + removeGoToFilter() + } + } + }); + diff --git a/rhodecode/templates/base/root.mako b/rhodecode/templates/base/root.mako --- a/rhodecode/templates/base/root.mako +++ b/rhodecode/templates/base/root.mako @@ -7,7 +7,7 @@ go_import_header = '' if hasattr(c, 'rhodecode_db_repo'): c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1] - ## check repo context + c.template_context['repo_id'] = c.rhodecode_db_repo.repo_id c.template_context['repo_view_type'] = h.get_repo_view_type(request) if getattr(c, 'repo_group', None): @@ -29,6 +29,7 @@ c.template_context['default_user'] = { c.template_context['search_context'] = { 'repo_group_id': c.template_context.get('repo_group_id'), 'repo_group_name': c.template_context.get('repo_group_name'), + 'repo_id': c.template_context.get('repo_id'), 'repo_name': c.template_context.get('repo_name'), 'repo_view_type': c.template_context.get('repo_view_type'), } diff --git a/rhodecode/templates/search/search.mako b/rhodecode/templates/search/search.mako --- a/rhodecode/templates/search/search.mako +++ b/rhodecode/templates/search/search.mako @@ -37,9 +37,9 @@ <%def name="menu_bar_subnav()"> %if c.repo_name: - ${self.repo_menu(active='search')} + ${self.repo_menu(active='summary')} %elif c.repo_group_name: - ${self.repo_group_menu(active='search')} + ${self.repo_group_menu(active='home')} %endif