Add repo autocomplete for searching.
This commit is contained in:
parent
bf926aceee
commit
edaad6eea2
6 changed files with 165 additions and 5 deletions
|
@ -26,6 +26,9 @@ def get_user(username):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_matching_users(username_prefix):
|
||||||
|
return list(User.select().where(User.username ** (username_prefix + '%')).limit(10))
|
||||||
|
|
||||||
def verify_user(username, password):
|
def verify_user(username, password):
|
||||||
try:
|
try:
|
||||||
fetched = User.get(User.username == username)
|
fetched = User.get(User.username == username)
|
||||||
|
@ -58,6 +61,10 @@ def get_token(code):
|
||||||
return AccessToken.get(AccessToken.code == code)
|
return AccessToken.get(AccessToken.code == code)
|
||||||
|
|
||||||
|
|
||||||
|
def get_matching_repositories(repo_term):
|
||||||
|
return list(Repository.select().where(Repository.name ** ('%' + repo_term + '%') | Repository.namespace ** ('%' + repo_term + '%') | Repository.description ** ('%' + repo_term + '%')).limit(10))
|
||||||
|
|
||||||
|
|
||||||
def change_password(user, new_password):
|
def change_password(user, new_password):
|
||||||
pw_hash = bcrypt.hashpw(new_password, bcrypt.gensalt())
|
pw_hash = bcrypt.hashpw(new_password, bcrypt.gensalt())
|
||||||
user.password_hash = pw_hash
|
user.password_hash = pw_hash
|
||||||
|
|
|
@ -32,19 +32,44 @@ def get_logged_in_user():
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/users/<prefix>', methods=['GET'])
|
||||||
|
def get_matching_users(prefix):
|
||||||
|
users = model.get_matching_users(prefix)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'users': [user.username for user in users]
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/', methods=['POST'])
|
@app.route('/api/repository/', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def create_repo_api():
|
def create_repo_api():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/repository/find/<prefix>', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def match_repos_api(prefix):
|
||||||
|
def repo_view(repo):
|
||||||
|
return {
|
||||||
|
'namespace': repo.namespace,
|
||||||
|
'name': repo.name,
|
||||||
|
'description': repo.description,
|
||||||
|
}
|
||||||
|
|
||||||
|
repos = [repo_view(repo) for repo in model.get_matching_repositories(prefix) if
|
||||||
|
ReadRepositoryPermission(repo.namespace, repo.name).can()]
|
||||||
|
response = {
|
||||||
|
'repositories': repos
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/', methods=['GET'])
|
@app.route('/api/repository/', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
def list_repos_api():
|
def list_repos_api():
|
||||||
def repo_view(repo_perm):
|
def repo_view(repo_perm):
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'namespace': repo_perm.repository.namespace,
|
'namespace': repo_perm.repository.namespace,
|
||||||
'name': repo_perm.repository.name,
|
'name': repo_perm.repository.name,
|
||||||
|
|
|
@ -1,3 +1,28 @@
|
||||||
|
#repoSearch {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-mini-listing {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-mini-listing i {
|
||||||
|
color: #aaa;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-mini-listing .description {
|
||||||
|
margin-left: 20px;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 85%;
|
||||||
|
padding-top: 4px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
margin-top: 4px;
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.editable i {
|
.editable i {
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
|
@ -146,6 +171,7 @@ p.editable:hover i {
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.repo-listing {
|
.repo-listing {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
@ -193,4 +219,64 @@ p.editable:hover i {
|
||||||
|
|
||||||
.repo .images {
|
.repo .images {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.twitter-typeahead .tt-query,
|
||||||
|
.twitter-typeahead .tt-hint {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-dropdown-menu {
|
||||||
|
min-width: 160px;
|
||||||
|
margin-top: 2px;
|
||||||
|
padding: 5px 0;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border: 1px solid rgba(0,0,0,.2);
|
||||||
|
*border-right-width: 2px;
|
||||||
|
*border-bottom-width: 2px;
|
||||||
|
-webkit-border-radius: 6px;
|
||||||
|
-moz-border-radius: 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||||
|
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||||
|
box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||||
|
-webkit-background-clip: padding-box;
|
||||||
|
-moz-background-clip: padding;
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-suggestion {
|
||||||
|
display: block;
|
||||||
|
padding: 3px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-suggestion.tt-is-under-cursor {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #0081c2;
|
||||||
|
background-image: -moz-linear-gradient(top, #0088cc, #0077b3);
|
||||||
|
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));
|
||||||
|
background-image: -webkit-linear-gradient(top, #0088cc, #0077b3);
|
||||||
|
background-image: -o-linear-gradient(top, #0088cc, #0077b3);
|
||||||
|
background-image: linear-gradient(to bottom, #0088cc, #0077b3);
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0)
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-suggestion.tt-is-under-cursor a {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-suggestion p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.twitter-typeahead .tt-hint {
|
||||||
|
display: block;
|
||||||
|
height: 34px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.428571429;
|
||||||
|
border: 1px solid transparent;
|
||||||
}
|
}
|
|
@ -2,6 +2,42 @@ function HeaderCtrl($scope, UserService) {
|
||||||
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
$scope.user = currentUser;
|
$scope.user = currentUser;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
$('#repoSearch').typeahead({
|
||||||
|
name: 'repositories',
|
||||||
|
remote: {
|
||||||
|
url: '/api/repository/find/%QUERY',
|
||||||
|
filter: function(data) {
|
||||||
|
var datums = [];
|
||||||
|
for (var i = 0; i < data.repositories.length; ++i) {
|
||||||
|
var repo = data.repositories[i];
|
||||||
|
datums.push({
|
||||||
|
'value': repo.name,
|
||||||
|
'tokens': [repo.name, repo.namespace],
|
||||||
|
'repo': repo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return datums;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
template: function (datum) {
|
||||||
|
template = '<div class="repo-mini-listing">';
|
||||||
|
template += '<i class="icon-hdd icon-large"></i>'
|
||||||
|
template += '<span class="name">' + datum.repo.namespace +'/' + datum.repo.name + '</span>'
|
||||||
|
if (datum.repo.description) {
|
||||||
|
template += '<span class="description">' + datum.repo.description + '</span>'
|
||||||
|
}
|
||||||
|
|
||||||
|
template += '</div>'
|
||||||
|
return template;
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#repoSearch').on('typeahead:selected', function (e, datum) {
|
||||||
|
$('#repoSearch').typeahead('setQuery', '');
|
||||||
|
document.location = '#/repository/' + datum.repo.namespace + '/' + datum.repo.name
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function RepoListCtrl($scope, Restangular) {
|
function RepoListCtrl($scope, Restangular) {
|
||||||
|
|
7
static/js/typeahead.min.js
vendored
Normal file
7
static/js/typeahead.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -22,6 +22,7 @@
|
||||||
<script src="static/lib/angular-moment.min.js"></script>
|
<script src="static/lib/angular-moment.min.js"></script>
|
||||||
|
|
||||||
<script src="static/js/ZeroClipboard.min.js"></script>
|
<script src="static/js/ZeroClipboard.min.js"></script>
|
||||||
|
<script src="static/js/typeahead.min.js"></script>
|
||||||
|
|
||||||
<script src="static/js/app.js"></script>
|
<script src="static/js/app.js"></script>
|
||||||
<script src="static/js/controllers.js"></script>
|
<script src="static/js/controllers.js"></script>
|
||||||
|
@ -51,13 +52,11 @@
|
||||||
<ul class="nav navbar-nav navbar-right" ng-switch on="user.anonymous">
|
<ul class="nav navbar-nav navbar-right" ng-switch on="user.anonymous">
|
||||||
<form class="navbar-form navbar-left" role="search">
|
<form class="navbar-form navbar-left" role="search">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" class="form-control" placeholder="Find Repo">
|
<input id="repoSearch" type="text" class="form-control" placeholder="Find Repo">
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<li class="dropdown" ng-switch-when="false">
|
<li class="dropdown" ng-switch-when="false">
|
||||||
<!--<button type="button" class="btn btn-default navbar-btn">Sign in</button>-->
|
|
||||||
|
|
||||||
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{ user.username }} <b class="caret"></b></a>
|
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{ user.username }} <b class="caret"></b></a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a href="/signout">Sign out</a></li>
|
<li><a href="/signout">Sign out</a></li>
|
||||||
|
|
Reference in a new issue