Fix bulk assign user search: replace broken combobox with typeahead

The combobox modal used lt-combobox-list but lt.combobox looks for
lt-combobox-dropdown — it returned immediately, wiring nothing.

Replaced with lt.typeahead which is correct for single-select search:
- Filters users client-side as you type (minChars:1, debounced 150ms)
- Shows display_name (username) with highlight on match
- onSelect stores user ID and shows "✓ Name" confirmation below input
- Input auto-focuses when modal opens
- Enter key now selects first result even without arrow-key navigation
  (same fix applied to lt.combobox Enter handler)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-05 12:35:32 -04:00
parent 025963a78f
commit 2378e56268
2 changed files with 28 additions and 24 deletions
+1 -1
View File
@@ -2065,7 +2065,7 @@
if (!dropdown.classList.contains('is-open')) return; if (!dropdown.classList.contains('is-open')) return;
if (e.key === 'ArrowDown') { e.preventDefault(); _moveFocus(1); } if (e.key === 'ArrowDown') { e.preventDefault(); _moveFocus(1); }
if (e.key === 'ArrowUp') { e.preventDefault(); _moveFocus(-1); } if (e.key === 'ArrowUp') { e.preventDefault(); _moveFocus(-1); }
if (e.key === 'Enter') { e.preventDefault(); if (_focusedIdx >= 0 && _items[_focusedIdx]) _select(_items[_focusedIdx]); } if (e.key === 'Enter') { e.preventDefault(); const idx = _focusedIdx >= 0 ? _focusedIdx : 0; if (_items[idx]) _select(_items[idx]); }
if (e.key === 'Escape') { dropdown.classList.remove('is-open'); } if (e.key === 'Escape') { dropdown.classList.remove('is-open'); }
if (e.key === 'Tab') { dropdown.classList.remove('is-open'); } if (e.key === 'Tab') { dropdown.classList.remove('is-open'); }
}); });
+21 -17
View File
@@ -552,7 +552,7 @@ function performBulkCloseAction(ticketIds) {
}); });
} }
var _bulkAssignUserId = null; // set by combobox onSelect var _bulkAssignUserId = null;
function showBulkAssignModal() { function showBulkAssignModal() {
const ticketIds = getSelectedTicketIds(); const ticketIds = getSelectedTicketIds();
@@ -566,21 +566,20 @@ function showBulkAssignModal() {
const modalHtml = ` const modalHtml = `
<div class="lt-modal-overlay" id="bulkAssignModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="bulkAssignModalTitle"> <div class="lt-modal-overlay" id="bulkAssignModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="bulkAssignModalTitle">
<div class="lt-modal"> <div class="lt-modal lt-modal-sm">
<div class="lt-modal-header"> <div class="lt-modal-header">
<span class="lt-modal-title" id="bulkAssignModalTitle">Assign ${ticketIds.length} Ticket(s)</span> <span class="lt-modal-title" id="bulkAssignModalTitle">[ @ ] Assign ${ticketIds.length} Ticket(s)</span>
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button> <button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
</div> </div>
<div class="lt-modal-body"> <div class="lt-modal-body">
<label class="lt-label">Assign to:</label> <label class="lt-label" for="bulkAssignUserInput">Assign to</label>
<div class="lt-combobox" id="bulkAssignCombobox"> <div class="lt-typeahead" id="bulkAssignTypeahead" style="position:relative">
<div class="lt-combobox-input-wrap"> <input type="text" class="lt-input lt-w-full" id="bulkAssignUserInput"
<input type="text" class="lt-combobox-input" id="bulkAssignUserInput" placeholder="Type a name…" autocomplete="off" spellcheck="false"
placeholder="Search users" autocomplete="off" aria-label="Search users"> aria-label="Search users" aria-autocomplete="list">
<div class="lt-typeahead-dropdown" id="bulkAssignDropdown"></div>
</div> </div>
<ul class="lt-combobox-list" role="listbox" aria-hidden="true"></ul> <div id="bulkAssignSelected" style="margin-top:0.4rem;font-size:0.75rem;color:var(--terminal-cyan);min-height:1.2em"></div>
</div>
<span class="lt-field-hint lt-text-muted">Type to search — supports large user lists</span>
</div> </div>
<div class="lt-modal-footer"> <div class="lt-modal-footer">
<button data-action="perform-bulk-assign" class="lt-btn lt-btn-primary">ASSIGN</button> <button data-action="perform-bulk-assign" class="lt-btn lt-btn-primary">ASSIGN</button>
@@ -592,21 +591,26 @@ function showBulkAssignModal() {
document.body.insertAdjacentHTML('beforeend', modalHtml); document.body.insertAdjacentHTML('beforeend', modalHtml);
lt.modal.open('bulkAssignModal'); lt.modal.open('bulkAssignModal');
setTimeout(() => { const inp = document.getElementById('bulkAssignUserInput'); if (inp) inp.focus(); }, 120);
lt.api.get('/api/get_users.php') lt.api.get('/api/get_users.php')
.then(data => { .then(data => {
if (data.success && data.users) { if (!data.success || !data.users) return;
const input = document.getElementById('bulkAssignUserInput'); const input = document.getElementById('bulkAssignUserInput');
if (!input) return; if (!input) return;
const items = data.users.map(u => ({ const items = data.users.map(u => ({
value: String(u.user_id), value: String(u.user_id),
label: u.display_name || u.username label: u.display_name ? u.display_name + ' (' + u.username + ')' : u.username
})); }));
lt.combobox.init(input, items, { lt.typeahead.init(input, items, {
max: 1, minChars: 1,
onChange: function(selected) { _bulkAssignUserId = selected[0] || null; } maxResults: 8,
}); onSelect: function(item) {
_bulkAssignUserId = item.value;
const sel = document.getElementById('bulkAssignSelected');
if (sel) sel.textContent = '✓ ' + item.label;
} }
});
}) })
.catch(() => lt.toast.error('Error loading users')); .catch(() => lt.toast.error('Error loading users'));
} }