Phase 4: power UX (palette, shortcuts, mentions, mobile, PWA)
Command palette (cmd+K) with fuzzy nav, ticket search, people lookup and action entries (new ticket, logout, show shortcuts). Opens from keyboard or user dropdown. Global keyboard shortcuts via a small useShortcut/useLeaderShortcut hook: `?` help overlay, `c` new ticket, `g d|t|m|n|s` leader nav. Tickets list: j/k cursor, Enter open, x toggle select. TicketDetail: `e` edit, `r` focus comment composer. All guarded against firing inside text fields. @mention autocomplete in the comment composer (MentionTextarea) with arrow-key nav and Tab/Enter insert. Rendered comments and audit log rewrite @username tokens to links pointing at that user's assignee filter; unknown usernames left as plain text. Mobile sweep: TicketDetail sidebar stacks below content on <md, Settings profile grid collapses to one column, admin tables get horizontal scroll with a 640px min width, CTI 3-column grid stacks vertically on <md, New ticket severity/assignee grid same. PWA: manifest.webmanifest, SVG icon, minimal network-first service worker for the app shell (never caches /api/*), registered in production builds only. Theme-color meta + manifest link in index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useShortcut } from '../hooks/useShortcuts';
|
||||
import { ChevronLeft, ChevronRight, Trash2, Save } from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { toast } from 'sonner';
|
||||
@@ -69,6 +70,7 @@ function sevColor(severity: number) {
|
||||
|
||||
export default function Tickets() {
|
||||
const [params, setParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const { user: authUser } = useAuth();
|
||||
const { data: users = [] } = useUsers();
|
||||
|
||||
@@ -83,6 +85,7 @@ export default function Tickets() {
|
||||
|
||||
const [searchInput, setSearchInput] = useState(search);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [cursor, setCursor] = useState<number>(-1);
|
||||
const [saveOpen, setSaveOpen] = useState(false);
|
||||
const [newViewName, setNewViewName] = useState('');
|
||||
const [confirmBulk, setConfirmBulk] = useState<
|
||||
@@ -213,6 +216,35 @@ export default function Tickets() {
|
||||
|
||||
const agentUsers = users.filter((u) => u.role !== 'SERVICE');
|
||||
|
||||
// Keyboard navigation
|
||||
useEffect(() => {
|
||||
setCursor((c) => (c >= tickets.length ? tickets.length - 1 : c));
|
||||
}, [tickets.length]);
|
||||
|
||||
useShortcut('j', (e) => {
|
||||
if (tickets.length === 0) return;
|
||||
e.preventDefault();
|
||||
setCursor((c) => Math.min(tickets.length - 1, c < 0 ? 0 : c + 1));
|
||||
}, [tickets.length]);
|
||||
|
||||
useShortcut('k', (e) => {
|
||||
if (tickets.length === 0) return;
|
||||
e.preventDefault();
|
||||
setCursor((c) => Math.max(0, c < 0 ? 0 : c - 1));
|
||||
}, [tickets.length]);
|
||||
|
||||
useShortcut('enter', (e) => {
|
||||
if (cursor < 0 || cursor >= tickets.length) return;
|
||||
e.preventDefault();
|
||||
navigate(`/${tickets[cursor].displayId}`);
|
||||
}, [cursor, tickets]);
|
||||
|
||||
useShortcut('x', (e) => {
|
||||
if (cursor < 0 || cursor >= tickets.length) return;
|
||||
e.preventDefault();
|
||||
toggleOne(tickets[cursor].id);
|
||||
}, [cursor, tickets]);
|
||||
|
||||
return (
|
||||
<Layout wide>
|
||||
{/* Status tabs */}
|
||||
@@ -453,10 +485,14 @@ export default function Tickets() {
|
||||
</div>
|
||||
|
||||
<ul className="divide-y divide-border">
|
||||
{tickets.map((ticket) => (
|
||||
{tickets.map((ticket, idx) => (
|
||||
<li
|
||||
key={ticket.id}
|
||||
className="flex items-center gap-3 px-4 py-3 hover:bg-accent/30 transition-colors"
|
||||
className={`flex items-center gap-3 px-4 py-3 transition-colors ${
|
||||
idx === cursor
|
||||
? 'bg-accent/50 ring-1 ring-inset ring-primary'
|
||||
: 'hover:bg-accent/30'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
Reference in New Issue
Block a user