Fix duplicate users in bulk/quick assign modals; add combobox search
Root cause: DashboardView.php and dashboard.js both had a global
document.addEventListener('click') handler handling the same bulk-assign
and quick-assign actions. Every click fired both handlers, creating two
modals and two API fetches that both appended to the same select element.
Fix: Remove duplicate cases (bulk-*, navigate, view-ticket, quick-*,
set-view-mode, toggle-*, clear-selection) from DashboardView.php's inline
handler. dashboard.js already handles all of these correctly.
Also replace <select> with lt.combobox in both bulk-assign and
quick-assign modals so large user lists are searchable instead of a
long scrolling dropdown.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+54
-31
@@ -545,6 +545,8 @@ function performBulkCloseAction(ticketIds) {
|
||||
});
|
||||
}
|
||||
|
||||
var _bulkAssignUserId = null; // set by combobox onSelect
|
||||
|
||||
function showBulkAssignModal() {
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
@@ -553,7 +555,8 @@ function showBulkAssignModal() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create modal HTML
|
||||
_bulkAssignUserId = null;
|
||||
|
||||
const modalHtml = `
|
||||
<div class="lt-modal-overlay" id="bulkAssignModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="bulkAssignModalTitle">
|
||||
<div class="lt-modal">
|
||||
@@ -562,10 +565,15 @@ function showBulkAssignModal() {
|
||||
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<div class="lt-modal-body">
|
||||
<label for="bulkAssignUser">Assign to:</label>
|
||||
<select id="bulkAssignUser" class="lt-select">
|
||||
<option value="">Select User...</option>
|
||||
</select>
|
||||
<label class="lt-label">Assign to:</label>
|
||||
<div class="lt-combobox" id="bulkAssignCombobox">
|
||||
<div class="lt-combobox-input-wrap">
|
||||
<input type="text" class="lt-combobox-input" id="bulkAssignUserInput"
|
||||
placeholder="Search users…" autocomplete="off" aria-label="Search users">
|
||||
</div>
|
||||
<ul class="lt-combobox-list" role="listbox" aria-hidden="true"></ul>
|
||||
</div>
|
||||
<span class="lt-field-hint lt-text-muted">Type to search — supports large user lists</span>
|
||||
</div>
|
||||
<div class="lt-modal-footer">
|
||||
<button data-action="perform-bulk-assign" class="lt-btn lt-btn-primary">ASSIGN</button>
|
||||
@@ -578,19 +586,18 @@ function showBulkAssignModal() {
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
lt.modal.open('bulkAssignModal');
|
||||
|
||||
// Fetch users for the dropdown
|
||||
lt.api.get('/api/get_users.php')
|
||||
.then(data => {
|
||||
if (data.success && data.users) {
|
||||
const select = document.getElementById('bulkAssignUser');
|
||||
if (select) {
|
||||
data.users.forEach(user => {
|
||||
const option = document.createElement('option');
|
||||
option.value = user.user_id;
|
||||
option.textContent = user.display_name || user.username;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
const input = document.getElementById('bulkAssignUserInput');
|
||||
if (!input) return;
|
||||
const items = data.users.map(u => ({
|
||||
value: String(u.user_id),
|
||||
label: u.display_name || u.username
|
||||
}));
|
||||
lt.combobox.init(input, items, {
|
||||
onSelect: function(item) { _bulkAssignUserId = item.value; }
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => lt.toast.error('Error loading users'));
|
||||
@@ -603,11 +610,11 @@ function closeBulkAssignModal() {
|
||||
}
|
||||
|
||||
function performBulkAssign() {
|
||||
const userId = document.getElementById('bulkAssignUser').value;
|
||||
const userId = _bulkAssignUserId;
|
||||
const ticketIds = getSelectedTicketIds();
|
||||
|
||||
if (!userId) {
|
||||
lt.toast.warning('Please select a user', 2000);
|
||||
lt.toast.warning('Please select a user from the list', 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -997,10 +1004,14 @@ function performQuickStatusChange(ticketId) {
|
||||
});
|
||||
}
|
||||
|
||||
var _quickAssignUserId = undefined; // undefined = no change; null = unassign; string = user_id
|
||||
|
||||
/**
|
||||
* Quick assign from dashboard
|
||||
*/
|
||||
function quickAssign(ticketId) {
|
||||
_quickAssignUserId = undefined;
|
||||
|
||||
const modalHtml = `
|
||||
<div class="lt-modal-overlay" id="quickAssignModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="quickAssignModalTitle">
|
||||
<div class="lt-modal lt-modal-xs">
|
||||
@@ -1009,14 +1020,18 @@ function quickAssign(ticketId) {
|
||||
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||
</div>
|
||||
<div class="lt-modal-body">
|
||||
<p class="lt-mb-xs">Ticket #${lt.escHtml(ticketId)}</p>
|
||||
<label for="quickAssignSelect">Assign to:</label>
|
||||
<select id="quickAssignSelect" class="lt-select">
|
||||
<option value="">Unassigned</option>
|
||||
</select>
|
||||
<p class="lt-mb-xs lt-text-muted lt-text-xs">Ticket #${lt.escHtml(String(ticketId))}</p>
|
||||
<label class="lt-label">Assign to:</label>
|
||||
<div class="lt-combobox" id="quickAssignCombobox">
|
||||
<div class="lt-combobox-input-wrap">
|
||||
<input type="text" class="lt-combobox-input" id="quickAssignInput"
|
||||
placeholder="Search users…" autocomplete="off" aria-label="Search users">
|
||||
</div>
|
||||
<ul class="lt-combobox-list" role="listbox" aria-hidden="true"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lt-modal-footer">
|
||||
<button data-action="perform-quick-assign" data-ticket-id="${ticketId}" class="lt-btn lt-btn-primary">ASSIGN</button>
|
||||
<button data-action="perform-quick-assign" data-ticket-id="${lt.escHtml(String(ticketId))}" class="lt-btn lt-btn-primary">ASSIGN</button>
|
||||
<button data-action="close-quick-assign-modal" class="lt-btn lt-btn-ghost">CANCEL</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1026,16 +1041,20 @@ function quickAssign(ticketId) {
|
||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||
lt.modal.open('quickAssignModal');
|
||||
|
||||
// Load users
|
||||
lt.api.get('/api/get_users.php')
|
||||
.then(data => {
|
||||
if (data.success && data.users) {
|
||||
const select = document.getElementById('quickAssignSelect');
|
||||
data.users.forEach(user => {
|
||||
const option = document.createElement('option');
|
||||
option.value = user.user_id;
|
||||
option.textContent = user.display_name || user.username;
|
||||
select.appendChild(option);
|
||||
const input = document.getElementById('quickAssignInput');
|
||||
if (!input) return;
|
||||
const items = [
|
||||
{ value: '', label: 'Unassigned' },
|
||||
...data.users.map(u => ({
|
||||
value: String(u.user_id),
|
||||
label: u.display_name || u.username
|
||||
}))
|
||||
];
|
||||
lt.combobox.init(input, items, {
|
||||
onSelect: function(item) { _quickAssignUserId = item.value || null; }
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -1049,7 +1068,11 @@ function closeQuickAssignModal() {
|
||||
}
|
||||
|
||||
function performQuickAssign(ticketId) {
|
||||
const assignedTo = document.getElementById('quickAssignSelect').value || null;
|
||||
if (_quickAssignUserId === undefined) {
|
||||
lt.toast.warning('Please select a user from the list', 2000);
|
||||
return;
|
||||
}
|
||||
const assignedTo = _quickAssignUserId;
|
||||
|
||||
lt.api.post('/api/assign_ticket.php', { ticket_id: ticketId, assigned_to: assignedTo })
|
||||
.then(data => {
|
||||
|
||||
+3
-12
@@ -857,7 +857,9 @@ document.querySelectorAll('.lt-stat-card').forEach(function (card) {
|
||||
});
|
||||
});
|
||||
|
||||
// Event delegation for click actions
|
||||
// Event delegation for click actions — only handles cases NOT covered by dashboard.js
|
||||
// bulk-*, navigate, view-ticket, quick-*, set-view-mode, clear-selection, toggle-*
|
||||
// are all handled by dashboard.js to avoid double-firing (duplicate handlers = duplicate users in selects).
|
||||
document.addEventListener('click', function (e) {
|
||||
var target = e.target.closest('[data-action]');
|
||||
if (!target) return;
|
||||
@@ -870,19 +872,8 @@ document.addEventListener('click', function (e) {
|
||||
case 'open-advanced-search': openAdvancedSearch(); break;
|
||||
case 'close-advanced-search': closeAdvancedSearch(); break;
|
||||
case 'reset-advanced-search': resetAdvancedSearch(); break;
|
||||
case 'set-view-mode': setViewMode(target.getAttribute('data-mode')); break;
|
||||
case 'navigate': window.location.href = target.getAttribute('data-url'); break;
|
||||
case 'toggle-export-menu': e.stopPropagation(); toggleExportMenu(e); break;
|
||||
case 'export-tickets': e.preventDefault(); exportSelectedTickets(target.getAttribute('data-format')); break;
|
||||
case 'bulk-status': showBulkStatusModal(); break;
|
||||
case 'bulk-assign': showBulkAssignModal(); break;
|
||||
case 'bulk-priority': showBulkPriorityModal(); break;
|
||||
case 'clear-selection': clearSelection(); break;
|
||||
case 'toggle-select-all': toggleSelectAll(); break;
|
||||
case 'toggle-row-checkbox': toggleRowCheckbox(e, target); break;
|
||||
case 'view-ticket': e.stopPropagation(); window.location.href = '/ticket/' + target.getAttribute('data-ticket-id'); break;
|
||||
case 'quick-status': e.stopPropagation(); quickStatusChange(target.getAttribute('data-ticket-id'), target.getAttribute('data-status')); break;
|
||||
case 'quick-assign': e.stopPropagation(); quickAssign(target.getAttribute('data-ticket-id')); break;
|
||||
case 'save-filter': saveCurrentFilter(); break;
|
||||
case 'delete-filter': deleteSavedFilter(); break;
|
||||
case 'remove-filter': removeFilter(target.getAttribute('data-filter-type'), target.getAttribute('data-filter-value')); break;
|
||||
|
||||
Reference in New Issue
Block a user