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
|
||||
|
||||
|
||||
def get_matching_users(username_prefix):
|
||||
return list(User.select().where(User.username ** (username_prefix + '%')).limit(10))
|
||||
|
||||
def verify_user(username, password):
|
||||
try:
|
||||
fetched = User.get(User.username == username)
|
||||
|
@ -58,6 +61,10 @@ def get_token(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):
|
||||
pw_hash = bcrypt.hashpw(new_password, bcrypt.gensalt())
|
||||
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'])
|
||||
@login_required
|
||||
def create_repo_api():
|
||||
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'])
|
||||
@login_required
|
||||
def list_repos_api():
|
||||
def repo_view(repo_perm):
|
||||
|
||||
|
||||
return {
|
||||
'namespace': repo_perm.repository.namespace,
|
||||
'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 {
|
||||
opacity: 0.2;
|
||||
font-size: 85%;
|
||||
|
@ -146,6 +171,7 @@ p.editable:hover i {
|
|||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
|
||||
.repo-listing {
|
||||
display: block;
|
||||
margin-bottom: 20px;
|
||||
|
@ -193,4 +219,64 @@ p.editable:hover i {
|
|||
|
||||
.repo .images {
|
||||
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.user = currentUser;
|
||||
}, 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) {
|
||||
|
|
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/js/ZeroClipboard.min.js"></script>
|
||||
<script src="static/js/typeahead.min.js"></script>
|
||||
|
||||
<script src="static/js/app.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">
|
||||
<form class="navbar-form navbar-left" role="search">
|
||||
<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>
|
||||
</form>
|
||||
|
||||
<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>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="/signout">Sign out</a></li>
|
||||
|
|
Reference in a new issue