Implement search-as-you-type for assignees
This commit is contained in:
parent
a80b309734
commit
3f955def1a
|
@ -5,6 +5,7 @@ from srht.database import db
|
|||
from srht.flask import loginrequired
|
||||
from srht.validation import Validation
|
||||
from todosrht.access import get_tracker, get_ticket
|
||||
from todosrht.search import find_usernames
|
||||
from todosrht.tickets import add_comment, mark_seen, assign, unassign
|
||||
from todosrht.types import Event, EventType
|
||||
from todosrht.types import Label, TicketLabel
|
||||
|
@ -331,3 +332,11 @@ def ticket_unassign(owner, name, ticket_id):
|
|||
db.session.commit()
|
||||
|
||||
return redirect(ticket_url(ticket))
|
||||
|
||||
@ticket.route("/usernames")
|
||||
def usernames():
|
||||
query = request.args.get('q')
|
||||
|
||||
return {
|
||||
"results": find_usernames(query)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import re
|
||||
from sqlalchemy import or_
|
||||
from todosrht.app import db
|
||||
from todosrht.types import Label, TicketLabel
|
||||
from todosrht.types import Ticket, TicketStatus, TicketComment
|
||||
from todosrht.types import User
|
||||
|
@ -106,3 +107,21 @@ def apply_search(query, search, tracker, current_user):
|
|||
Ticket.comments.any(TicketComment.text.ilike("%" + value + "%"))))
|
||||
|
||||
return query
|
||||
|
||||
def find_usernames(query, limit=20):
|
||||
"""Given a partial username string, returns matching usernames."""
|
||||
if not query or query == '~':
|
||||
return []
|
||||
|
||||
if query.startswith("~"):
|
||||
where = User.username.startswith(query[1:], autoescape=True)
|
||||
else:
|
||||
where = User.username.contains(query, autoescape=True)
|
||||
|
||||
rows = (db.session
|
||||
.query(User.username)
|
||||
.filter(where)
|
||||
.order_by(User.username)
|
||||
.limit(limit))
|
||||
|
||||
return [f"~{r[0]}" for r in rows]
|
||||
|
|
|
@ -146,10 +146,14 @@
|
|||
{{ csrf_token() }}
|
||||
<div class="form-group">
|
||||
<input
|
||||
id="assignee-input"
|
||||
type="text"
|
||||
name="username"
|
||||
autocomplete="off"
|
||||
list="assignee-list"
|
||||
class="form-control {{valid.cls("username")}}"
|
||||
value="{{username}}" />
|
||||
<datalist id="assignee-list"></datalist>
|
||||
{{valid.summary("username")}}
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
|
@ -337,3 +341,88 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script type="text/javascript">
|
||||
function UserAutoComplete(input, list) {
|
||||
this.input = input;
|
||||
this.list = list;
|
||||
this.lastQuery = input.value;
|
||||
|
||||
// Settings
|
||||
this.delay = 250;
|
||||
this.minQueryLength = 3;
|
||||
|
||||
this.onLoad = function(event) {
|
||||
const response = event.target;
|
||||
if (response.status == 200) {
|
||||
const data = JSON.parse(response.responseText);
|
||||
|
||||
this.list.innerHTML = '';
|
||||
data.results.forEach(function (username) {
|
||||
const option = document.createElement('option');
|
||||
option.value = username;
|
||||
this.list.appendChild(option);
|
||||
}.bind(this));
|
||||
}
|
||||
}.bind(this)
|
||||
|
||||
this.sendRequest = function(query) {
|
||||
const search = encodeURIComponent(query);
|
||||
const request = new XMLHttpRequest();
|
||||
request.onload = this.onLoad;
|
||||
request.open("GET", "/usernames/?q=" + search);
|
||||
request.send();
|
||||
}
|
||||
|
||||
this.search = function() {
|
||||
const query = this.input.value
|
||||
if (query == "" || query == "~") {
|
||||
this.list.innerHTML = "";
|
||||
this.lastQuery = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const notRepeated = query !== this.lastQuery;
|
||||
const notTooShort = query.length >= this.minQueryLength;
|
||||
if (notRepeated && notTooShort) {
|
||||
this.sendRequest(query);
|
||||
this.lastQuery = query;
|
||||
}
|
||||
}.bind(this)
|
||||
|
||||
this.debounce = function(fn, delay) {
|
||||
let timeout = null;
|
||||
return function() {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
timeout = setTimeout(fn, delay);
|
||||
}
|
||||
}
|
||||
|
||||
this.register = function() {
|
||||
this.input.addEventListener("input",
|
||||
this.debounce(this.search, this.delay));
|
||||
|
||||
// Prevent search being triggered when an user is selected from the datalist
|
||||
// 'select' works in Firefox, 'change' works in Chrome
|
||||
this.input.addEventListener("select", function(e) {
|
||||
this.lastQuery = e.target.value;
|
||||
}.bind(this));
|
||||
|
||||
this.input.addEventListener("change", function(e) {
|
||||
this.lastQuery = e.target.value;
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
(function() {
|
||||
const input = document.getElementById("assignee-input");
|
||||
const list = document.getElementById("assignee-list");
|
||||
|
||||
autocomplete = new UserAutoComplete(input, list);
|
||||
autocomplete.register();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
Loading…
Reference in New Issue