1.2.0-rc1
This commit is contained in:
1 parent
45ff46b7eb
commit
fa9c122250
30 files changed
+6354
-989
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import { CalendarDays, CheckCircle2, Circle, GripVertical, Pencil, Plus, Save, Trash2, X } from "lucide-react";
|
||||
import { FormEvent, useState } from "react";
|
||||
import { type FormEvent, type ReactNode, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -40,9 +40,11 @@ type TodoPanelProps = {
|
||||
onNavigate?: (to: string) => void;
|
||||
reorderEnabled?: boolean;
|
||||
onReorder?: (draggedTodoId: string, targetTodoId: string) => void;
|
||||
toolbar?: ReactNode;
|
||||
compact?: boolean;
|
||||
};
|
||||
|
||||
export function TodoPanel({ ticketId, todos, onChanged, showCreate = true, showTicketMeta = false, onNavigate, reorderEnabled = false, onReorder }: TodoPanelProps) {
|
||||
export function TodoPanel({ ticketId, todos, onChanged, showCreate = true, showTicketMeta = false, onNavigate, reorderEnabled = false, onReorder, toolbar, compact = false }: TodoPanelProps) {
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [priority, setPriority] = useState<TodoPriority>("medium");
|
||||
@@ -156,97 +158,100 @@ export function TodoPanel({ ticketId, todos, onChanged, showCreate = true, showT
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{showCreate && ticketId ? (
|
||||
<div className="flex justify-end">
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="button" size="sm">
|
||||
<Plus className="size-4" />
|
||||
ToDo anlegen
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>ToDo anlegen</DialogTitle>
|
||||
<DialogDescription>Neue Aufgabe zu diesem Ticket oder Projekt erfassen.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form className="space-y-4" onSubmit={submitTodo}>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor={`todo-title-${ticketId}`}>
|
||||
Aufgabe
|
||||
</label>
|
||||
<Input id={`todo-title-${ticketId}`} value={title} onChange={(event) => setTitle(event.currentTarget.value)} autoFocus required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor={`todo-priority-${ticketId}`}>
|
||||
Priorität
|
||||
</label>
|
||||
<select
|
||||
id={`todo-priority-${ticketId}`}
|
||||
className="h-9 w-full rounded-lg border border-input bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
value={priority}
|
||||
onChange={(event) => setPriority(event.currentTarget.value as TodoPriority)}
|
||||
>
|
||||
{Object.entries(priorityLabels).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className={compact ? "space-y-2" : "space-y-3"}>
|
||||
{toolbar || (showCreate && ticketId) ? (
|
||||
<div className={cn("flex flex-wrap items-center gap-2", toolbar ? "justify-start" : "justify-end")}>
|
||||
<div className="min-w-0">{toolbar}</div>
|
||||
{showCreate && ticketId ? (
|
||||
<Dialog open={createOpen} onOpenChange={setCreateOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="button" size="sm">
|
||||
<Plus className="size-4" />
|
||||
ToDo anlegen
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>ToDo anlegen</DialogTitle>
|
||||
<DialogDescription>Neue Aufgabe zu diesem Ticket oder Projekt erfassen.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form className="space-y-4" onSubmit={submitTodo}>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor={`todo-starts-on-${ticketId}`}>
|
||||
Umsetzung von
|
||||
<label className="text-sm font-medium" htmlFor={`todo-title-${ticketId}`}>
|
||||
Aufgabe
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input id={`todo-starts-on-${ticketId}`} type="date" value={startsOn} onChange={(event) => setStartsOn(event.currentTarget.value)} />
|
||||
{startsOn ? (
|
||||
<Button type="button" size="icon" variant="ghost" className="shrink-0" onClick={() => setStartsOn("")} aria-label="Von-Datum entfernen">
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
<Input id={`todo-title-${ticketId}`} value={title} onChange={(event) => setTitle(event.currentTarget.value)} autoFocus required />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor={`todo-priority-${ticketId}`}>
|
||||
Priorität
|
||||
</label>
|
||||
<select
|
||||
id={`todo-priority-${ticketId}`}
|
||||
className="h-9 w-full rounded-lg border border-input bg-background px-2 text-sm focus-visible:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||
value={priority}
|
||||
onChange={(event) => setPriority(event.currentTarget.value as TodoPriority)}
|
||||
>
|
||||
{Object.entries(priorityLabels).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor={`todo-starts-on-${ticketId}`}>
|
||||
Umsetzung von
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input id={`todo-starts-on-${ticketId}`} type="date" value={startsOn} onChange={(event) => setStartsOn(event.currentTarget.value)} />
|
||||
{startsOn ? (
|
||||
<Button type="button" size="icon" variant="ghost" className="shrink-0" onClick={() => setStartsOn("")} aria-label="Von-Datum entfernen">
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor={`todo-ends-on-${ticketId}`}>
|
||||
Umsetzung bis
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input id={`todo-ends-on-${ticketId}`} type="date" value={endsOn} onChange={(event) => setEndsOn(event.currentTarget.value)} />
|
||||
{endsOn ? (
|
||||
<Button type="button" size="icon" variant="ghost" className="shrink-0" onClick={() => setEndsOn("")} aria-label="Bis-Datum entfernen">
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor={`todo-ends-on-${ticketId}`}>
|
||||
Umsetzung bis
|
||||
<label className="text-sm font-medium" htmlFor={`todo-description-${ticketId}`}>
|
||||
Details
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input id={`todo-ends-on-${ticketId}`} type="date" value={endsOn} onChange={(event) => setEndsOn(event.currentTarget.value)} />
|
||||
{endsOn ? (
|
||||
<Button type="button" size="icon" variant="ghost" className="shrink-0" onClick={() => setEndsOn("")} aria-label="Bis-Datum entfernen">
|
||||
<X className="size-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<Textarea
|
||||
id={`todo-description-${ticketId}`}
|
||||
className="min-h-24"
|
||||
value={description}
|
||||
onChange={(event) => setDescription(event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium" htmlFor={`todo-description-${ticketId}`}>
|
||||
Details
|
||||
</label>
|
||||
<Textarea
|
||||
id={`todo-description-${ticketId}`}
|
||||
className="min-h-24"
|
||||
value={description}
|
||||
onChange={(event) => setDescription(event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={saving}>
|
||||
<Plus className="size-4" />
|
||||
{saving ? "Speichert..." : "Anlegen"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={saving}>
|
||||
<Plus className="size-4" />
|
||||
{saving ? "Speichert..." : "Anlegen"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className={compact ? "space-y-1.5" : "space-y-2"}>
|
||||
{todos.map((todo) => {
|
||||
const isEditing = editingId === todo.id;
|
||||
const periodLabel = formatTodoPeriod(todo);
|
||||
@@ -256,7 +261,8 @@ export function TodoPanel({ ticketId, todos, onChanged, showCreate = true, showT
|
||||
<div
|
||||
key={todo.id}
|
||||
className={cn(
|
||||
"rounded-md border bg-card/50 p-3 transition-colors",
|
||||
"rounded-md border bg-card/50 transition-colors",
|
||||
compact ? "p-2" : "p-3",
|
||||
overdue && "border-red-500/50 bg-red-500/10",
|
||||
todo.status === "done" && "opacity-70",
|
||||
reorderEnabled && draggingId === todo.id && "border-primary/50 bg-primary/5"
|
||||
@@ -393,7 +399,7 @@ export function TodoPanel({ ticketId, todos, onChanged, showCreate = true, showT
|
||||
})}
|
||||
</div>
|
||||
|
||||
{todos.length === 0 ? <p className="rounded-md border bg-muted/20 p-4 text-center text-sm text-muted-foreground">Keine ToDos vorhanden.</p> : null}
|
||||
{todos.length === 0 ? <p className={cn("rounded-md border bg-muted/20 text-center text-sm text-muted-foreground", compact ? "p-3" : "p-4")}>Keine ToDos vorhanden.</p> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user