feat(tickets): ajoute les pièces jointes sur tickets (#108)
Stockage flat côté ticket + métadonnées d'attachments, lecture exposée côté backend/MCP, et UI minimale de liste/ajout dans TicketDetail. Traverse le domaine (Issue, ports), l'application (usecases + assistant de ticket), les adaptateurs infra/MCP (issues store, orchestrateur), les DTO backend/web- server/app-tauri, et le frontend (domain/ports/adapters/hooks/UI). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -76,6 +76,18 @@ function Section({
|
||||
);
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
|
||||
const units = ["B", "KB", "MB", "GB"];
|
||||
let value = bytes;
|
||||
let unit = 0;
|
||||
while (value >= 1024 && unit < units.length - 1) {
|
||||
value /= 1024;
|
||||
unit += 1;
|
||||
}
|
||||
return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
|
||||
}
|
||||
|
||||
export function TicketDetail({
|
||||
projectId,
|
||||
ticketRef,
|
||||
@ -389,6 +401,72 @@ export function TicketDetail({
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{/* ── Attachments (#108) ── */}
|
||||
<Section title="Pièces jointes">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<p className="text-xs text-muted">
|
||||
{t.attachments.length === 0
|
||||
? "Aucune pièce jointe."
|
||||
: `${t.attachments.length} pièce${
|
||||
t.attachments.length > 1 ? "s" : ""
|
||||
} jointe${t.attachments.length > 1 ? "s" : ""}.`}
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={vm.busy}
|
||||
loading={vm.busy}
|
||||
onClick={() => void vm.attachFile()}
|
||||
>
|
||||
Joindre
|
||||
</Button>
|
||||
</div>
|
||||
{t.attachments.length > 0 && (
|
||||
<ul className="flex flex-col gap-2">
|
||||
{t.attachments.map((attachment) => (
|
||||
<li
|
||||
key={attachment.id}
|
||||
className="flex flex-wrap items-center justify-between gap-2 rounded-md border border-border bg-raised/30 px-3 py-2"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium text-content">
|
||||
{attachment.filename}
|
||||
</p>
|
||||
<p className="text-xs text-muted">
|
||||
{formatBytes(attachment.sizeBytes)} ·{" "}
|
||||
{attachment.mime || "application/octet-stream"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
attachment.summarizedInCarnet
|
||||
? "bg-success/10 text-success"
|
||||
: "bg-raised text-muted",
|
||||
)}
|
||||
>
|
||||
Résumé : {attachment.summarizedInCarnet ? "oui" : "non"}
|
||||
</span>
|
||||
{!attachment.summarizedInCarnet && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={vm.busy}
|
||||
onClick={() =>
|
||||
void vm.markAttachmentSummarized(attachment.id)
|
||||
}
|
||||
aria-label={`marquer résumé ${attachment.filename}`}
|
||||
>
|
||||
Marquer résumé
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
{/* ── Links (F5) ── */}
|
||||
<Section title="Linked tickets">
|
||||
{t.links.length === 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user