diff --git a/data/model/repository.py b/data/model/repository.py index c2f65bdfe..1b113fe42 100644 --- a/data/model/repository.py +++ b/data/model/repository.py @@ -4,6 +4,7 @@ import random from enum import Enum from datetime import timedelta, datetime from peewee import JOIN_LEFT_OUTER, fn, SQL, IntegrityError +from playhouse.shortcuts import case from cachetools import ttl_cache from data.model import (config, DataModelException, tag, db_transaction, storage, permission, @@ -485,14 +486,21 @@ def _get_sorted_matching_repositories(lookup_value, repo_kind='image', include_p # Always search at least on name (init clause) clause = Repository.name.match(lookup_value) + computed_score = RepositorySearchScore.score.alias('score') + # If the description field is in the search fields, then we need to compute a synthetic score + # to discount the weight of the description more than the name. if SEARCH_FIELDS.description.name in search_fields: clause = Repository.description.match(lookup_value) | clause - last_week = datetime.now() - timedelta(weeks=1) + cases = [ + (Repository.name.match(lookup_value), 100 * RepositorySearchScore.score), + ] + + computed_score = case(None, cases, RepositorySearchScore.score).alias('score') query = (Repository - .select(Repository, Namespace) + .select(Repository, Namespace, computed_score) .join(Namespace, on=(Namespace.id == Repository.namespace_user)) .where(clause) .group_by(Repository.id, Namespace.id)) @@ -507,7 +515,7 @@ def _get_sorted_matching_repositories(lookup_value, repo_kind='image', include_p .switch(Repository) .join(RepositorySearchScore) .group_by(Repository, Namespace, RepositorySearchScore) - .order_by(RepositorySearchScore.score.desc())) + .order_by(SQL('score').desc())) return query diff --git a/static/js/directives/ui/search-box/search-box.component.html b/static/js/directives/ui/search-box/search-box.component.html index d62b9c2f5..162529c52 100644 --- a/static/js/directives/ui/search-box/search-box.component.html +++ b/static/js/directives/ui/search-box/search-box.component.html @@ -35,16 +35,24 @@ first-line-only="true" placeholder-needed="false"> + + + {{ result.namespace.name }}/{{ result.name }} +
+
+
+
- diff --git a/static/js/directives/ui/search-box/search-box.component.ts b/static/js/directives/ui/search-box/search-box.component.ts index cdb30ffff..597019d76 100644 --- a/static/js/directives/ui/search-box/search-box.component.ts +++ b/static/js/directives/ui/search-box/search-box.component.ts @@ -10,6 +10,7 @@ import { Input, Component, Inject } from 'ng-metadata/core'; }) export class SearchBoxComponent { @Input(' { - $event['callback'](true); // Clear the value. + $event['callback'](this.clearOnSearch == 'true'); // Clear the value. this.$location.url('/search'); this.$location.search('q', $event['value']); }, 10); diff --git a/static/js/directives/ui/typeahead/typeahead.directive.ts b/static/js/directives/ui/typeahead/typeahead.directive.ts index d733d88b2..d49b6de9c 100644 --- a/static/js/directives/ui/typeahead/typeahead.directive.ts +++ b/static/js/directives/ui/typeahead/typeahead.directive.ts @@ -13,16 +13,19 @@ export class TypeaheadDirective implements AfterContentInit { @Input('taDisplayKey') displayKey: string = ''; @Input('taSuggestionTmpl') suggestionTemplate: string = ''; @Input('taClearOnSelect') clearOnSelect: boolean = false; + @Input('taDebounce') debounce: number = 250; @Output('taSelected') selected = new EventEmitter(); @Output('taEntered') entered = new EventEmitter(); private itemSelected: boolean = false; + private existingTimer: Promise = null; constructor(@Inject('$element') private $element: ng.IAugmentedJQuery, @Inject('$compile') private $compile: ng.ICompileService, @Inject('$scope') private $scope: ng.IScope, - @Inject('$templateRequest') private $templateRequest: ng.ITemplateRequestService) { + @Inject('$templateRequest') private $templateRequest: ng.ITemplateRequestService, + @Inject('$timeout') private $timeout: ng.ITimeoutService) { } public ngAfterContentInit(): void { @@ -52,12 +55,23 @@ export class TypeaheadDirective implements AfterContentInit { templates: templates, display: this.displayKey, source: (query, results, asyncResults) => { - this.typeahead.emit({'query': query, 'callback': asyncResults}); - this.itemSelected = false; + this.debounceQuery(query, asyncResults); }, }); } + private debounceQuery(query: string, asyncResults: Function): void { + if (this.existingTimer) { + this.$timeout.cancel(this.existingTimer); + this.existingTimer = null; + } + + this.existingTimer = this.$timeout(() => { + this.typeahead.emit({'query': query, 'callback': asyncResults}); + this.itemSelected = false; + }, this.debounce); + } + @HostListener('keyup', ['$event']) public onKeyup(event: JQueryKeyEventObject): void { if (!this.itemSelected && event.keyCode == 13) { diff --git a/static/partials/search.html b/static/partials/search.html index baca42247..df484a81f 100644 --- a/static/partials/search.html +++ b/static/partials/search.html @@ -1,6 +1,6 @@