Allow manual ordering of opened ticket links
This commit is contained in:
+1
-1
@@ -216,7 +216,7 @@ In der Ticketdetailansicht sieht man:
|
||||
- Teamspace-Wert pro Ticket und Tag
|
||||
- Moeglichkeit, direkt eine Session zu diesem Ticket nachzutragen
|
||||
|
||||
Geoeffnete Tickets erscheinen in der Seitenleiste unter `Geoeffnete Tickets`. Die Liste bleibt nach Aktualisierung und Browserneustart erhalten. Mit einem Klick auf die Ticketnummer gelangt man direkt zur Ticketdetailansicht zurueck. Der Schliessen-Button entfernt das Ticket aus der Seitenleiste. Wenn im aktuell geoeffneten Ticket noch ungespeicherte Werte vorhanden sind, muss zuerst gespeichert werden.
|
||||
Geoeffnete Tickets erscheinen in der Seitenleiste unter `Geoeffnete Tickets`. Die Liste bleibt nach Aktualisierung und Browserneustart erhalten. Mit einem Klick auf die Ticketnummer gelangt man direkt zur Ticketdetailansicht zurueck. Die Reihenfolge bleibt beim Anklicken unveraendert und kann nur per Drag and Drop angepasst werden. Der Schliessen-Button entfernt das Ticket aus der Seitenleiste. Wenn im aktuell geoeffneten Ticket noch ungespeicherte Werte vorhanden sind, muss zuerst gespeichert werden.
|
||||
|
||||
Die Sessions sind pro Tag gruppiert. In jeder Tagesgruppe stehen:
|
||||
|
||||
|
||||
+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