Allow manual ordering of opened ticket links
This commit is contained in:
+66
-4
@@ -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"
|
||||
|
||||
@@ -214,6 +214,7 @@ export const helpSections: HelpSection[] = [
|
||||
"Ein leeres Teamspace-Feld entfernt den gespeicherten Teamspace-Wert für diesen Ticket-Tag.",
|
||||
"Der Teamspace-Wert hängt am Ticket, Benutzer und Tag. Deshalb ist er in Monats- und Tagesansicht identisch.",
|
||||
"Geöffnete Tickets werden in der Seitenleiste unter Geöffnete Tickets abgelegt und bleiben nach Refresh oder Browserneustart erhalten.",
|
||||
"Die Reihenfolge bleibt beim Anklicken stabil und kann nur per Drag and Drop geändert werden.",
|
||||
"Der Schließen-Button in der Seitenleiste entfernt ein Ticket nur, wenn im aktuell geöffneten Ticket keine ungespeicherten Werte mehr vorhanden sind.",
|
||||
"Sessions können bewertet, bearbeitet oder gelöscht werden. Gelöschte letzte Sessions können dazu führen, dass ein leeres Ticket entfernt wird."
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user