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() {
|
function showBulkAssignModal() {
|
||||||
const ticketIds = getSelectedTicketIds();
|
const ticketIds = getSelectedTicketIds();
|
||||||
|
|
||||||
@@ -553,7 +555,8 @@ function showBulkAssignModal() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create modal HTML
|
_bulkAssignUserId = null;
|
||||||
|
|
||||||
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">
|
||||||
@@ -562,10 +565,15 @@ function showBulkAssignModal() {
|
|||||||
<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 for="bulkAssignUser">Assign to:</label>
|
<label class="lt-label">Assign to:</label>
|
||||||
<select id="bulkAssignUser" class="lt-select">
|
<div class="lt-combobox" id="bulkAssignCombobox">
|
||||||
<option value="">Select User...</option>
|
<div class="lt-combobox-input-wrap">
|
||||||
</select>
|
<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>
|
||||||
<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>
|
||||||
@@ -578,19 +586,18 @@ function showBulkAssignModal() {
|
|||||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||||
lt.modal.open('bulkAssignModal');
|
lt.modal.open('bulkAssignModal');
|
||||||
|
|
||||||
// Fetch users for the dropdown
|
|
||||||
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) {
|
||||||
const select = document.getElementById('bulkAssignUser');
|
const input = document.getElementById('bulkAssignUserInput');
|
||||||
if (select) {
|
if (!input) return;
|
||||||
data.users.forEach(user => {
|
const items = data.users.map(u => ({
|
||||||
const option = document.createElement('option');
|
value: String(u.user_id),
|
||||||
option.value = user.user_id;
|
label: u.display_name || u.username
|
||||||
option.textContent = user.display_name || user.username;
|
}));
|
||||||
select.appendChild(option);
|
lt.combobox.init(input, items, {
|
||||||
});
|
onSelect: function(item) { _bulkAssignUserId = item.value; }
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => lt.toast.error('Error loading users'));
|
.catch(() => lt.toast.error('Error loading users'));
|
||||||
@@ -603,11 +610,11 @@ function closeBulkAssignModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function performBulkAssign() {
|
function performBulkAssign() {
|
||||||
const userId = document.getElementById('bulkAssignUser').value;
|
const userId = _bulkAssignUserId;
|
||||||
const ticketIds = getSelectedTicketIds();
|
const ticketIds = getSelectedTicketIds();
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
lt.toast.warning('Please select a user', 2000);
|
lt.toast.warning('Please select a user from the list', 2000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -997,10 +1004,14 @@ function performQuickStatusChange(ticketId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _quickAssignUserId = undefined; // undefined = no change; null = unassign; string = user_id
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quick assign from dashboard
|
* Quick assign from dashboard
|
||||||
*/
|
*/
|
||||||
function quickAssign(ticketId) {
|
function quickAssign(ticketId) {
|
||||||
|
_quickAssignUserId = undefined;
|
||||||
|
|
||||||
const modalHtml = `
|
const modalHtml = `
|
||||||
<div class="lt-modal-overlay" id="quickAssignModal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="quickAssignModalTitle">
|
<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">
|
<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>
|
<button class="lt-modal-close" data-modal-close aria-label="Close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="lt-modal-body">
|
<div class="lt-modal-body">
|
||||||
<p class="lt-mb-xs">Ticket #${lt.escHtml(ticketId)}</p>
|
<p class="lt-mb-xs lt-text-muted lt-text-xs">Ticket #${lt.escHtml(String(ticketId))}</p>
|
||||||
<label for="quickAssignSelect">Assign to:</label>
|
<label class="lt-label">Assign to:</label>
|
||||||
<select id="quickAssignSelect" class="lt-select">
|
<div class="lt-combobox" id="quickAssignCombobox">
|
||||||
<option value="">Unassigned</option>
|
<div class="lt-combobox-input-wrap">
|
||||||
</select>
|
<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>
|
||||||
<div class="lt-modal-footer">
|
<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>
|
<button data-action="close-quick-assign-modal" class="lt-btn lt-btn-ghost">CANCEL</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1026,16 +1041,20 @@ function quickAssign(ticketId) {
|
|||||||
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
document.body.insertAdjacentHTML('beforeend', modalHtml);
|
||||||
lt.modal.open('quickAssignModal');
|
lt.modal.open('quickAssignModal');
|
||||||
|
|
||||||
// Load users
|
|
||||||
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) {
|
||||||
const select = document.getElementById('quickAssignSelect');
|
const input = document.getElementById('quickAssignInput');
|
||||||
data.users.forEach(user => {
|
if (!input) return;
|
||||||
const option = document.createElement('option');
|
const items = [
|
||||||
option.value = user.user_id;
|
{ value: '', label: 'Unassigned' },
|
||||||
option.textContent = user.display_name || user.username;
|
...data.users.map(u => ({
|
||||||
select.appendChild(option);
|
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) {
|
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 })
|
lt.api.post('/api/assign_ticket.php', { ticket_id: ticketId, assigned_to: assignedTo })
|
||||||
.then(data => {
|
.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) {
|
document.addEventListener('click', function (e) {
|
||||||
var target = e.target.closest('[data-action]');
|
var target = e.target.closest('[data-action]');
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
@@ -870,19 +872,8 @@ document.addEventListener('click', function (e) {
|
|||||||
case 'open-advanced-search': openAdvancedSearch(); break;
|
case 'open-advanced-search': openAdvancedSearch(); break;
|
||||||
case 'close-advanced-search': closeAdvancedSearch(); break;
|
case 'close-advanced-search': closeAdvancedSearch(); break;
|
||||||
case 'reset-advanced-search': resetAdvancedSearch(); 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 'toggle-export-menu': e.stopPropagation(); toggleExportMenu(e); break;
|
||||||
case 'export-tickets': e.preventDefault(); exportSelectedTickets(target.getAttribute('data-format')); 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 'save-filter': saveCurrentFilter(); break;
|
||||||
case 'delete-filter': deleteSavedFilter(); break;
|
case 'delete-filter': deleteSavedFilter(); break;
|
||||||
case 'remove-filter': removeFilter(target.getAttribute('data-filter-type'), target.getAttribute('data-filter-value')); break;
|
case 'remove-filter': removeFilter(target.getAttribute('data-filter-type'), target.getAttribute('data-filter-value')); break;
|
||||||
|
|||||||
Reference in New Issue
Block a user