Implement user search functionality with HTMX support for manual user selection in bulk updates

This commit is contained in:
Ross
2025-10-27 14:32:03 +00:00
parent 1c8f862303
commit 668007b899
3 changed files with 101 additions and 105 deletions
@@ -0,0 +1,12 @@
{% if results %}
<ul style="list-style:none;padding:0;margin:0">
{% for item in results %}
<li style="margin:6px 0;">
<strong>{{ item.text|escape }}</strong>
<button type="button" onclick="setManualUser({{ row }}, {{ item.id }}, '{{ item.text|escapejs }}')" style="margin-left:8px;">Select</button>
</li>
{% endfor %}
</ul>
{% else %}
<div><em>No users found</em></div>
{% endif %}
@@ -40,8 +40,11 @@
<div>
<span style="color:red">No match</span>
<div>
<input type="text" id="user-search-input-{{ r.row_index }}" placeholder="Search users..." style="width:220px" />
<button type="button" onclick="searchUsers({{ r.row_index }})">Search</button>
<form hx-get="{% url 'generic:user_search' %}" hx-target="#user-results-{{ r.row_index }}" hx-swap="innerHTML" onsubmit="return false;">
<input type="text" name="q" id="user-search-input-{{ r.row_index }}" placeholder="Search users..." style="width:220px" />
<input type="hidden" name="row" value="{{ r.row_index }}" />
<button type="submit">Search</button>
</form>
</div>
<div id="user-results-{{ r.row_index }}"></div>
<input type="hidden" name="manual_user_{{ r.row_index }}" id="manual_user_{{ r.row_index }}" />
@@ -65,48 +68,21 @@
{% block extra_scripts %}
<script>
function searchUsers(rowIndex) {
const input = document.getElementById('user-search-input-' + rowIndex);
const q = input ? input.value.trim() : '';
const resultsDiv = document.getElementById('user-results-' + rowIndex);
// Called from the HTMX-rendered results fragment to set the manual selection
function setManualUser(rowIndex, id, text) {
const hiddenInput = document.getElementById('manual_user_' + rowIndex);
resultsDiv.innerHTML = 'Searching...';
fetch("{% url 'generic:user_search' %}?q=" + encodeURIComponent(q))
.then(r => r.json())
.then(data => {
resultsDiv.innerHTML = '';
if (!data || !data.results || data.results.length === 0) {
resultsDiv.innerHTML = '<div><em>No users found</em></div>';
return;
const resultsDiv = document.getElementById('user-results-' + rowIndex);
if (hiddenInput) {
hiddenInput.value = id;
}
if (resultsDiv) {
resultsDiv.innerHTML = '<div>Selected: ' + escapeHtml(text) + '</div>';
}
const ul = document.createElement('ul');
ul.style.listStyle = 'none';
ul.style.padding = '0';
data.results.forEach(function(item){
const li = document.createElement('li');
li.style.margin = '4px 0';
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'Select';
btn.style.marginLeft = '8px';
btn.onclick = function(){
hiddenInput.value = item.id;
resultsDiv.innerHTML = '<div>Selected: ' + escapeHtml(item.text) + '</div>';
};
li.innerHTML = '<strong>' + escapeHtml(item.text) + '</strong>';
li.appendChild(btn);
ul.appendChild(li);
});
resultsDiv.appendChild(ul);
})
.catch(function(err){
resultsDiv.innerHTML = '<div style="color:red">Error searching users</div>';
console.error(err);
});
}
function escapeHtml(unsafe) {
return unsafe
if (!unsafe) return '';
return String(unsafe)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
+8
View File
@@ -4412,6 +4412,14 @@ def user_search(request):
for u in qs:
results.append({"id": u.pk, "text": f"{u.first_name} {u.last_name} ({u.email})"})
# If this is an HTMX request (or a row param was provided) render an HTML fragment
# that can be swapped into the preview row's results container. Otherwise return JSON.
row = request.GET.get("row")
if request.headers.get("HX-Request") == "true" or row is not None:
# Render a small partial that lists results with Select buttons that call
# the setManualUser(row, id, text) function in the parent page.
return render(request, "generic/partials/user_search_results.html", {"results": results, "row": int(row) if row is not None else None})
return JsonResponse({"results": results})