feat: file/screenshot attachments on support tickets (v0.49.1)
Support form now accepts up to 5 attachments (images, PDF, text, JSON, zip; 10MB each) via the existing S3/MinIO presigned-upload flow. New support_ticket_attachments table; attachment links get folded into the Gitea issue body and shown as thumbnails in both the user's ticket history and the admin support view. New presign endpoint (/api/v1/support/attachments/presign, rate-limited 20/hr) scoped to the uploading user rather than a ticket id, since the ticket doesn't exist yet at upload time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { toast } from "sonner";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { ExternalLink, FileText } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
@@ -16,6 +17,8 @@ import {
|
||||
|
||||
type TicketStatus = "open" | "triaged" | "closed";
|
||||
|
||||
type Attachment = { id: string; contentType: string; url: string };
|
||||
|
||||
type Ticket = {
|
||||
id: string;
|
||||
userEmail: string;
|
||||
@@ -27,8 +30,13 @@ type Ticket = {
|
||||
giteaIssueUrl: string | null;
|
||||
giteaError: string | null;
|
||||
createdAt: string;
|
||||
attachments: Attachment[];
|
||||
};
|
||||
|
||||
function isImage(contentType: string) {
|
||||
return contentType.startsWith("image/");
|
||||
}
|
||||
|
||||
function formatDateTime(iso: string) {
|
||||
return new Date(iso).toLocaleString(undefined, {
|
||||
month: "short",
|
||||
@@ -113,6 +121,27 @@ export function AdminSupportManager({ initialTickets }: { initialTickets: Ticket
|
||||
</Select>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground whitespace-pre-wrap">{ticket.description}</p>
|
||||
{ticket.attachments.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 pt-1">
|
||||
{ticket.attachments.map((a) => (
|
||||
<Link
|
||||
key={a.id}
|
||||
href={a.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="relative h-12 w-12 rounded-md border overflow-hidden block"
|
||||
>
|
||||
{isImage(a.contentType) ? (
|
||||
<Image src={a.url} unoptimized alt="Attachment" fill className="object-cover" />
|
||||
) : (
|
||||
<div className="h-full w-full flex items-center justify-center bg-muted/40">
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-xs flex-wrap pt-1">
|
||||
<Badge variant="outline" className="text-xs">{ticket.type}</Badge>
|
||||
<Badge variant={statusVariant[ticket.status]} className="text-xs">{ticket.status}</Badge>
|
||||
|
||||
Reference in New Issue
Block a user