M9 S2: fold raw multipart fetches into api.postForm
Both notes-store uploads (uploadAttachment, importNotes) hand-rolled the same fetch + resp.json() + !ok error parsing that api.client already does. Add `api.postForm<T>(path, form)`: request() now detects a FormData body and lets the browser set the multipart Content-Type (skipping the JSON header + stringify), reusing the shared error handling — so the two uploads gain network-error handling and the 5xx infra toast they lacked. A too-large import returns 413 (< 500), so it still throws for inline display rather than toasting. DRY: net -13 lines; no raw fetch() remains in the stores. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -16,13 +16,16 @@ function notifyError(err: ApiError) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||||
|
// 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;
|
let resp: Response;
|
||||||
try {
|
try {
|
||||||
resp = await fetch(path, {
|
resp = await fetch(path, {
|
||||||
method,
|
method,
|
||||||
credentials: "include", // send/receive the signed session cookie
|
credentials: "include", // send/receive the signed session cookie
|
||||||
headers: body !== undefined ? { "Content-Type": "application/json" } : undefined,
|
headers: body !== undefined && !isForm ? { "Content-Type": "application/json" } : undefined,
|
||||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
body: body === undefined ? undefined : isForm ? body : JSON.stringify(body),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
const err: ApiError = { error: "Can't reach the server. Check your connection.", status: 0 };
|
const err: ApiError = { error: "Can't reach the server. Check your connection.", status: 0 };
|
||||||
@@ -59,4 +62,5 @@ export const api = {
|
|||||||
patch: <T>(path: string, body?: unknown) => request<T>("PATCH", path, body),
|
patch: <T>(path: string, body?: unknown) => request<T>("PATCH", path, body),
|
||||||
put: <T>(path: string, body?: unknown) => request<T>("PUT", path, body),
|
put: <T>(path: string, body?: unknown) => request<T>("PUT", path, body),
|
||||||
del: <T>(path: string) => request<T>("DELETE", path),
|
del: <T>(path: string) => request<T>("DELETE", path),
|
||||||
|
postForm: <T>(path: string, form: FormData) => request<T>("POST", path, form),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -205,16 +205,7 @@ export const useNotesStore = defineStore("notes", () => {
|
|||||||
async function uploadAttachment(id: string, file: File): Promise<void> {
|
async function uploadAttachment(id: string, file: File): Promise<void> {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append("file", file);
|
form.append("file", file);
|
||||||
const resp = await fetch(`/api/notes/${id}/attachments`, { method: "POST", credentials: "include", body: form });
|
reconcile(await api.postForm<Note>(`/api/notes/${id}/attachments`, 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteAttachment(id: string, attId: string): Promise<void> {
|
async function deleteAttachment(id: string, attId: string): Promise<void> {
|
||||||
@@ -232,18 +223,10 @@ export const useNotesStore = defineStore("notes", () => {
|
|||||||
async function importNotes(file: File): Promise<{ source: string; imported: number; skipped: number }> {
|
async function importNotes(file: File): Promise<{ source: string; imported: number; skipped: number }> {
|
||||||
const form = new FormData();
|
const form = new FormData();
|
||||||
form.append("file", file);
|
form.append("file", file);
|
||||||
const resp = await fetch("/api/notes/import", { method: "POST", credentials: "include", body: form });
|
const data = await api.postForm<{ source: string; imported: number; skipped: number }>("/api/notes/import", 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 };
|
|
||||||
}
|
|
||||||
// Refresh the current lens so imported notes appear (labels reloaded by caller).
|
// Refresh the current lens so imported notes appear (labels reloaded by caller).
|
||||||
await load(view.value, activeLabel.value, activeFacets.value);
|
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<Note | null> {
|
async function fetchOne(id: string): Promise<Note | null> {
|
||||||
|
|||||||
Reference in New Issue
Block a user