Add UI enhancements and new features

Keyboard Navigation:
- Add J/K keys for Gmail-style ticket list navigation
- Add N key for new ticket, C for comment focus
- Add G then D for go to dashboard (vim-style)
- Add 1-4 number keys for quick status changes on ticket page
- Add Enter to open selected ticket
- Update keyboard help modal with all new shortcuts

Ticket Age Indicator:
- Show "Last activity: X days ago" on ticket view
- Visual warning (yellow pulse) for tickets idle >5 days
- Critical warning (red pulse) for tickets idle >10 days

Ticket Clone Feature:
- Add "Clone" button on ticket view
- Creates copy with [CLONE] prefix in title
- Preserves description, priority, category, type, visibility
- Automatically creates "relates_to" dependency to original

Active Filter Badges:
- Show visual badges above ticket table for active filters
- Click X on badge to remove individual filter
- "Clear All" button to reset all filters
- Color-coded by filter type (status, priority, search)

Visual Enhancements:
- Add keyboard-selected row highlighting for J/K navigation
- Smooth animations for filter badges

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-30 19:21:36 -05:00
parent 9b40a714ed
commit 1c1eb19876
7 changed files with 565 additions and 6 deletions

View File

@@ -203,10 +203,76 @@ document.addEventListener('DOMContentLoaded', function() {
case 'open-settings-modal':
if (typeof openSettingsModal === 'function') openSettingsModal();
break;
// Filter badge actions
case 'remove-filter':
removeFilter(target.dataset.filterType, target.dataset.filterValue);
break;
case 'clear-all-filters':
clearAllFilters();
break;
}
});
});
/**
* Remove a single filter and reload page
*/
function removeFilter(filterType, filterValue) {
const params = new URLSearchParams(window.location.search);
if (filterType === 'status') {
const currentStatuses = (params.get('status') || '').split(',').filter(s => s.trim());
const newStatuses = currentStatuses.filter(s => s !== filterValue);
if (newStatuses.length > 0) {
params.set('status', newStatuses.join(','));
} else {
params.delete('status');
}
} else if (filterType === 'priority') {
const currentPriorities = (params.get('priority') || '').split(',').filter(p => p.trim());
const newPriorities = currentPriorities.filter(p => p !== filterValue);
if (newPriorities.length > 0) {
params.set('priority', newPriorities.join(','));
} else {
params.delete('priority');
}
} else if (filterType === 'search') {
params.delete('search');
} else {
params.delete(filterType);
}
// Reset to page 1 when changing filters
params.delete('page');
window.location.search = params.toString();
}
/**
* Clear all filters and reload page
*/
function clearAllFilters() {
const params = new URLSearchParams(window.location.search);
// Remove all filter parameters
params.delete('status');
params.delete('priority');
params.delete('category');
params.delete('type');
params.delete('assigned_to');
params.delete('search');
params.delete('date_from');
params.delete('date_to');
params.delete('page');
// Keep sort parameters
const sortParams = new URLSearchParams();
if (params.has('sort')) sortParams.set('sort', params.get('sort'));
if (params.has('dir')) sortParams.set('dir', params.get('dir'));
window.location.search = sortParams.toString();
}
function initTableSorting() {
const tableHeaders = document.querySelectorAll('th');
tableHeaders.forEach((header, index) => {

View File

@@ -96,9 +96,102 @@ document.addEventListener('DOMContentLoaded', function() {
e.preventDefault();
showKeyboardHelp();
}
// J: Move to next row in table (Gmail-style)
if (e.key === 'j') {
e.preventDefault();
navigateTableRow('next');
}
// K: Move to previous row in table (Gmail-style)
if (e.key === 'k') {
e.preventDefault();
navigateTableRow('prev');
}
// Enter: Open selected ticket
if (e.key === 'Enter') {
const selectedRow = document.querySelector('tbody tr.keyboard-selected');
if (selectedRow) {
e.preventDefault();
const ticketLink = selectedRow.querySelector('a[href*="/ticket/"]');
if (ticketLink) {
window.location.href = ticketLink.href;
}
}
}
// N: Create new ticket (on dashboard)
if (e.key === 'n') {
e.preventDefault();
const newTicketBtn = document.querySelector('a[href*="/create"]');
if (newTicketBtn) {
window.location.href = newTicketBtn.href;
}
}
// C: Focus comment textarea (on ticket page)
if (e.key === 'c') {
const commentBox = document.getElementById('newComment');
if (commentBox) {
e.preventDefault();
commentBox.focus();
commentBox.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
// G then D: Go to Dashboard (vim-style)
if (e.key === 'g') {
window._pendingG = true;
setTimeout(() => { window._pendingG = false; }, 1000);
}
if (e.key === 'd' && window._pendingG) {
e.preventDefault();
window._pendingG = false;
window.location.href = '/';
}
// 1-4: Quick status change on ticket page
if (['1', '2', '3', '4'].includes(e.key)) {
const statusSelect = document.getElementById('statusSelect');
if (statusSelect && !document.querySelector('.modal-overlay')) {
const statusMap = { '1': 'Open', '2': 'Pending', '3': 'In Progress', '4': 'Closed' };
const targetStatus = statusMap[e.key];
const option = Array.from(statusSelect.options).find(opt => opt.value === targetStatus);
if (option && !option.disabled) {
e.preventDefault();
statusSelect.value = targetStatus;
statusSelect.dispatchEvent(new Event('change'));
}
}
}
});
});
// Track currently selected row for J/K navigation
let currentSelectedRowIndex = -1;
function navigateTableRow(direction) {
const rows = document.querySelectorAll('tbody tr');
if (rows.length === 0) return;
// Remove current selection
rows.forEach(row => row.classList.remove('keyboard-selected'));
if (direction === 'next') {
currentSelectedRowIndex = Math.min(currentSelectedRowIndex + 1, rows.length - 1);
} else {
currentSelectedRowIndex = Math.max(currentSelectedRowIndex - 1, 0);
}
// Add selection to new row
const selectedRow = rows[currentSelectedRowIndex];
if (selectedRow) {
selectedRow.classList.add('keyboard-selected');
selectedRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
function showKeyboardHelp() {
// Check if help is already showing
if (document.getElementById('keyboardHelpModal')) {
@@ -109,17 +202,37 @@ function showKeyboardHelp() {
modal.id = 'keyboardHelpModal';
modal.className = 'modal-overlay';
modal.innerHTML = `
<div class="modal-content ascii-frame-outer" style="max-width: 400px;">
<div class="modal-content ascii-frame-outer" style="max-width: 500px;">
<div class="ascii-frame">
<div class="ascii-content">
<h3 style="margin: 0 0 1rem 0; color: var(--terminal-green);">KEYBOARD SHORTCUTS</h3>
<div class="modal-body" style="padding: 0;">
<h4 style="color: var(--terminal-amber); margin: 0.5rem 0; font-size: 0.9rem;">Navigation</h4>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 1rem;">
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>J</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Next ticket in list</td></tr>
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>K</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Previous ticket in list</td></tr>
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>Enter</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Open selected ticket</td></tr>
<tr><td style="padding: 0.4rem;"><kbd>G</kbd> then <kbd>D</kbd></td><td style="padding: 0.4rem;">Go to Dashboard</td></tr>
</table>
<h4 style="color: var(--terminal-amber); margin: 0.5rem 0; font-size: 0.9rem;">Actions</h4>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 1rem;">
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>N</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">New ticket</td></tr>
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>C</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Focus comment box</td></tr>
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>Ctrl/Cmd + E</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Toggle Edit Mode</td></tr>
<tr><td style="padding: 0.4rem;"><kbd>Ctrl/Cmd + S</kbd></td><td style="padding: 0.4rem;">Save Changes</td></tr>
</table>
<h4 style="color: var(--terminal-amber); margin: 0.5rem 0; font-size: 0.9rem;">Quick Status (Ticket Page)</h4>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 1rem;">
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>1</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Set Open</td></tr>
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>2</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Set Pending</td></tr>
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>3</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Set In Progress</td></tr>
<tr><td style="padding: 0.4rem;"><kbd>4</kbd></td><td style="padding: 0.4rem;">Set Closed</td></tr>
</table>
<h4 style="color: var(--terminal-amber); margin: 0.5rem 0; font-size: 0.9rem;">Other</h4>
<table style="width: 100%; border-collapse: collapse;">
<tr><td style="padding: 0.5rem; border-bottom: 1px solid var(--border-color);"><kbd>Ctrl/Cmd + E</kbd></td><td style="padding: 0.5rem; border-bottom: 1px solid var(--border-color);">Toggle Edit Mode</td></tr>
<tr><td style="padding: 0.5rem; border-bottom: 1px solid var(--border-color);"><kbd>Ctrl/Cmd + S</kbd></td><td style="padding: 0.5rem; border-bottom: 1px solid var(--border-color);">Save Changes</td></tr>
<tr><td style="padding: 0.5rem; border-bottom: 1px solid var(--border-color);"><kbd>Ctrl/Cmd + K</kbd></td><td style="padding: 0.5rem; border-bottom: 1px solid var(--border-color);">Focus Search</td></tr>
<tr><td style="padding: 0.5rem; border-bottom: 1px solid var(--border-color);"><kbd>ESC</kbd></td><td style="padding: 0.5rem; border-bottom: 1px solid var(--border-color);">Close Modal / Cancel</td></tr>
<tr><td style="padding: 0.5rem;"><kbd>?</kbd></td><td style="padding: 0.5rem;">Show This Help</td></tr>
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>Ctrl/Cmd + K</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Focus Search</td></tr>
<tr><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);"><kbd>ESC</kbd></td><td style="padding: 0.4rem; border-bottom: 1px solid var(--border-color);">Close Modal / Cancel</td></tr>
<tr><td style="padding: 0.4rem;"><kbd>?</kbd></td><td style="padding: 0.4rem;">Show This Help</td></tr>
</table>
</div>
<div class="modal-footer" style="margin-top: 1rem;">