feat(frontend): UI archivos en cards (issue 0128)

- CardFilesPanel: tab Archivos con grid thumbs + boton subir/borrar
- CardForm: drag&drop en descripcion, inserta ref markdown en cursor
- CardChatPanel: drag&drop + boton paperclip, sube y envia ref como mensaje
- MessageBody: renderer markdown minimo (img inline + link chip)
- api.ts: listCardFiles, uploadCardFile (multipart), deleteCardFile
- types.ts: CardFile
This commit is contained in:
2026-05-27 10:52:01 +02:00
parent 2401eb5abc
commit ac5f016e7e
7 changed files with 605 additions and 27 deletions
+37
View File
@@ -1,6 +1,7 @@
import type {
Board,
Card,
CardFile,
CardHistoryResponse,
CardMessage,
Column,
@@ -380,6 +381,42 @@ export function listRequesters(): Promise<string[]> {
return fetchJSON("/requesters");
}
// --- Files (issue 0128) -----------------------------------------------------
export function listCardFiles(cardId: string): Promise<CardFile[]> {
return fetchJSON(`/cards/${cardId}/files`);
}
export async function uploadCardFile(
cardId: string,
file: File,
source: "upload" | "description" | "chat" = "upload"
): Promise<CardFile> {
const fd = new FormData();
fd.append("file", file);
fd.append("source", source);
const res = await fetch(`${BASE}/cards/${cardId}/files`, {
method: "POST",
credentials: "same-origin",
body: fd,
});
if (!res.ok) {
let msg = `upload failed: ${res.status}`;
try {
const body = (await res.json()) as { Message?: string; message?: string };
if (body.Message || body.message) msg = body.Message || body.message || msg;
} catch {
/* ignore */
}
throw new HTTPError(res.status, msg);
}
return (await res.json()) as CardFile;
}
export function deleteCardFile(fileId: string): Promise<void> {
return fetchJSON(`/files/${fileId}`, { method: "DELETE" });
}
export function getMetrics(f: MetricsFilter): Promise<Metrics> {
const qs = new URLSearchParams();
if (f.from) qs.set("from", f.from);