diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index e2785f6..6fcc46b 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -16,13 +16,16 @@ function notifyError(err: ApiError) { } async function request(method: string, path: string, body?: unknown): Promise { + // FormData carries its own multipart Content-Type (with boundary), so let the + // browser set it — only JSON bodies get an explicit header + stringify. + const isForm = body instanceof FormData; let resp: Response; try { resp = await fetch(path, { method, credentials: "include", // send/receive the signed session cookie - headers: body !== undefined ? { "Content-Type": "application/json" } : undefined, - body: body !== undefined ? JSON.stringify(body) : undefined, + headers: body !== undefined && !isForm ? { "Content-Type": "application/json" } : undefined, + body: body === undefined ? undefined : isForm ? body : JSON.stringify(body), }); } catch { const err: ApiError = { error: "Can't reach the server. Check your connection.", status: 0 }; @@ -59,4 +62,5 @@ export const api = { patch: (path: string, body?: unknown) => request("PATCH", path, body), put: (path: string, body?: unknown) => request("PUT", path, body), del: (path: string) => request("DELETE", path), + postForm: (path: string, form: FormData) => request("POST", path, form), }; diff --git a/frontend/src/stores/notes.ts b/frontend/src/stores/notes.ts index 4c282b3..b6a9698 100644 --- a/frontend/src/stores/notes.ts +++ b/frontend/src/stores/notes.ts @@ -205,16 +205,7 @@ export const useNotesStore = defineStore("notes", () => { async function uploadAttachment(id: string, file: File): Promise { const form = new FormData(); form.append("file", file); - const resp = await fetch(`/api/notes/${id}/attachments`, { method: "POST", credentials: "include", body: form }); - const data: unknown = await resp.json().catch(() => ({})); - if (!resp.ok) { - const message = - typeof data === "object" && data !== null && "error" in data - ? String((data as { error: unknown }).error) - : "Upload failed."; - throw { error: message, status: resp.status }; - } - reconcile(data as Note); + reconcile(await api.postForm(`/api/notes/${id}/attachments`, form)); } async function deleteAttachment(id: string, attId: string): Promise { @@ -232,18 +223,10 @@ export const useNotesStore = defineStore("notes", () => { async function importNotes(file: File): Promise<{ source: string; imported: number; skipped: number }> { const form = new FormData(); form.append("file", file); - const resp = await fetch("/api/notes/import", { method: "POST", credentials: "include", body: form }); - const data: unknown = await resp.json().catch(() => ({})); - if (!resp.ok) { - const message = - typeof data === "object" && data !== null && "error" in data - ? String((data as { error: unknown }).error) - : "Import failed."; - throw { error: message, status: resp.status }; - } + const data = await api.postForm<{ source: string; imported: number; skipped: number }>("/api/notes/import", form); // Refresh the current lens so imported notes appear (labels reloaded by caller). await load(view.value, activeLabel.value, activeFacets.value); - return data as { source: string; imported: number; skipped: number }; + return data; } async function fetchOne(id: string): Promise {