Allow manual ordering of opened ticket links

This commit is contained in:
michael committed 2026-08-06 08:39:35 +02:00
1 parent 7d47a05bff
commit 4390f9a7da
3 files changed
+68 -5

No files matched your search

+66 -4
View File
@@ -6,6 +6,7 @@ import {
ChartNoAxesCombined,
CircleHelp,
FileText,
GripVertical,
LayoutGrid,
LogOut,
Moon,
@@ -70,7 +71,7 @@ import { SetupPage } from "./views/SetupPage";
import { TicketDetailPage } from "./views/TicketDetailPage";
import { TimerPage } from "./views/TimerPage";
import type { AuthUser, PeriodType } from "./types";
import type { Dispatch, SetStateAction } from "react";
import type { Dispatch, DragEvent, SetStateAction } from "react";
type NavMenuEntry =
| {
@@ -395,6 +396,7 @@ export function App() {
const [selectedTimerId, setSelectedTimerId] = useState<string | null>(null);
const [tick, setTick] = useState(Date.now());
const [openTickets, setOpenTickets] = useState<OpenTicketNavItem[]>([]);
const [draggedOpenTicketKey, setDraggedOpenTicketKey] = useState<string | null>(null);
useEffect(() => {
async function loadUser() {
@@ -616,11 +618,64 @@ export function App() {
const updateOpenTicket = useCallback((ticket: OpenTicketNavItem) => {
setOpenTickets((current) => {
const filtered = current.filter((item) => item.key !== ticket.key);
return [...filtered, ticket].slice(-12);
const existingIndex = current.findIndex((item) => item.key === ticket.key);
if (existingIndex >= 0) {
return current.map((item, index) => (index === existingIndex ? ticket : item));
}
return [...current, ticket].slice(-12);
});
}, []);
function moveOpenTicket(draggedKey: string, targetKey: string) {
if (draggedKey === targetKey) {
return;
}
setOpenTickets((current) => {
const draggedIndex = current.findIndex((item) => item.key === draggedKey);
const targetIndex = current.findIndex((item) => item.key === targetKey);
if (draggedIndex < 0 || targetIndex < 0) {
return current;
}
const next = [...current];
const [dragged] = next.splice(draggedIndex, 1);
next.splice(targetIndex, 0, dragged);
return next;
});
}
function startOpenTicketDrag(event: DragEvent<HTMLDivElement>, ticket: OpenTicketNavItem) {
setDraggedOpenTicketKey(ticket.key);
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("text/plain", ticket.key);
}
function overOpenTicket(event: DragEvent<HTMLDivElement>, ticket: OpenTicketNavItem) {
const draggedKey = draggedOpenTicketKey || event.dataTransfer.getData("text/plain");
if (!draggedKey || draggedKey === ticket.key) {
return;
}
event.preventDefault();
event.dataTransfer.dropEffect = "move";
}
function dropOpenTicket(event: DragEvent<HTMLDivElement>, ticket: OpenTicketNavItem) {
event.preventDefault();
const draggedKey = draggedOpenTicketKey || event.dataTransfer.getData("text/plain");
if (draggedKey) {
moveOpenTicket(draggedKey, ticket.key);
}
setDraggedOpenTicketKey(null);
}
function closeOpenTicket(ticket: OpenTicketNavItem) {
const current = openTickets.find((item) => item.key === ticket.key) ?? ticket;
@@ -785,11 +840,18 @@ export function App() {
return (
<SidebarMenuItem key={ticket.key}>
<div
draggable
onDragStart={(event) => startOpenTicketDrag(event, ticket)}
onDragOver={(event) => overOpenTicket(event, ticket)}
onDrop={(event) => dropOpenTicket(event, ticket)}
onDragEnd={() => setDraggedOpenTicketKey(null)}
className={cn(
"mx-2 flex items-center gap-1 rounded-md px-2 py-1.5",
"mx-2 flex cursor-grab items-center gap-1 rounded-md px-2 py-1.5 active:cursor-grabbing",
isActive ? "bg-primary text-primary-foreground" : "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
draggedOpenTicketKey === ticket.key && "opacity-60",
)}
>
<GripVertical className="size-3.5 shrink-0 text-muted-foreground" />
<a
href={ticket.path}
className="flex min-w-0 flex-1 items-center gap-2 text-sm"