M2 attachments: image upload + owner-scoped media serving
- Migration 0007: note_attachments (path/mime/size). Upload POST /api/notes/<id>/attachments (multipart, png/jpeg/gif/webp, 12MB cap via MAX_CONTENT_LENGTH) stored under Config.media_root() (first use of DATA_DIR); owner/ACL-scoped GET serves the file (nosniff); DELETE removes row + file. Note responses include attachments[]. - Frontend: notes store uploadAttachment (FormData)/deleteAttachment; editor image button + paste-to-upload + thumbnail grid with remove; card shows the first image as a cover. 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:
@@ -18,6 +18,12 @@ export interface ChecklistItem {
|
||||
position: number;
|
||||
}
|
||||
|
||||
export interface Attachment {
|
||||
id: string;
|
||||
url: string;
|
||||
mime: string;
|
||||
}
|
||||
|
||||
export interface Note {
|
||||
id: string;
|
||||
title: string | null;
|
||||
@@ -29,6 +35,7 @@ export interface Note {
|
||||
trashed: boolean;
|
||||
labels: NoteLabel[];
|
||||
items: ChecklistItem[];
|
||||
attachments: Attachment[];
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
@@ -113,6 +120,25 @@ export const useNotesStore = defineStore("notes", () => {
|
||||
reconcile(await api.del<Note>(`/api/notes/${id}/items/${itemId}`));
|
||||
}
|
||||
|
||||
async function uploadAttachment(id: string, file: File): Promise<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
async function deleteAttachment(id: string, attId: string): Promise<void> {
|
||||
reconcile(await api.del<Note>(`/api/notes/${id}/attachments/${attId}`));
|
||||
}
|
||||
|
||||
async function trash(id: string): Promise<void> {
|
||||
reconcile(await api.post<Note>(`/api/notes/${id}/trash`));
|
||||
}
|
||||
@@ -143,6 +169,8 @@ export const useNotesStore = defineStore("notes", () => {
|
||||
addItem,
|
||||
updateItem,
|
||||
deleteItem,
|
||||
uploadAttachment,
|
||||
deleteAttachment,
|
||||
trash,
|
||||
restore,
|
||||
deleteForever,
|
||||
|
||||
Reference in New Issue
Block a user