Images adding in notes and cleanup of orphaned images from notes

This commit is contained in:
2026-03-04 18:20:27 -06:00
parent 9cbe7dcb12
commit 4c096f18d7
3 changed files with 100 additions and 6 deletions
+11 -4
View File
@@ -10,7 +10,7 @@ import {
Trash2 Trash2
} from "lucide-solid"; } from "lucide-solid";
import { cn, convertImageToWebpFile } from "@/lib/utils"; import { cn, convertImageToWebpFile } from "@/lib/utils";
import { activeTaskId, uploadTaskAttachment } from "@/store"; import { activeTaskId, uploadTaskAttachment, activeNoteId, uploadNoteAttachment } from "@/store";
import { toast } from "solid-sonner"; import { toast } from "solid-sonner";
export interface CommandItem { export interface CommandItem {
@@ -123,9 +123,12 @@ export const getSuggestionItems = ({ query }: { query: string }): CommandItem[]
icon: ImageIcon, icon: ImageIcon,
command: ({ editor, range }: { editor: any, range: any }) => { command: ({ editor, range }: { editor: any, range: any }) => {
editor.chain().focus().deleteRange(range).run(); editor.chain().focus().deleteRange(range).run();
const taskId = activeTaskId(); const taskId = activeTaskId();
if (!taskId) { const noteId = activeNoteId();
toast.error("Cannot upload image outside of a task.");
if (!taskId && !noteId) {
toast.error("Cannot upload image outside of a task or note.");
return; return;
} }
@@ -138,7 +141,11 @@ export const getSuggestionItems = ({ query }: { query: string }): CommandItem[]
toast.promise( toast.promise(
(async () => { (async () => {
const webpFile = await convertImageToWebpFile(originalFile); const webpFile = await convertImageToWebpFile(originalFile);
return uploadTaskAttachment(taskId, webpFile); if (taskId) {
return uploadTaskAttachment(taskId, webpFile);
} else {
return uploadNoteAttachment(noteId!, webpFile);
}
})(), })(),
{ {
loading: "Processing & uploading image...", loading: "Processing & uploading image...",
+50
View File
@@ -15,6 +15,7 @@ export const [now, setNow] = createSignal(Date.now());
// Context: 'mine' = My Bucket (default), { userId: string } = Oversight View for that user, { bucketId: string, name: string } = Bucket View // Context: 'mine' = My Bucket (default), { userId: string } = Oversight View for that user, { bucketId: string, name: string } = Bucket View
type TaskContext = 'mine' | { userId: string, name: string } | { bucketId: string, name: string }; type TaskContext = 'mine' | { userId: string, name: string } | { bucketId: string, name: string };
export const [activeTaskId, setActiveTaskId] = createSignal<string | null>(null); export const [activeTaskId, setActiveTaskId] = createSignal<string | null>(null);
export const [activeNoteId, setActiveNoteId] = createSignal<string | null>(null);
export const [currentTaskContext, setCurrentTaskContext] = createSignal<TaskContext>('mine'); export const [currentTaskContext, setCurrentTaskContext] = createSignal<TaskContext>('mine');
export type UrgencyLevel = number; export type UrgencyLevel = number;
@@ -502,6 +503,55 @@ export const uploadTaskAttachment = async (taskId: string, file: File): Promise<
} }
}; };
export const uploadNoteAttachment = async (noteId: string, file: File): Promise<string> => {
try {
const formData = new FormData();
formData.append('attachments', file);
const record = await pb.collection(NOTES_COLLECTION).update(noteId, formData);
if (record.attachments && record.attachments.length > 0) {
const latestFilename = record.attachments[record.attachments.length - 1];
return pb.files.getURL(record, latestFilename);
}
throw new Error("File uploaded but no attachment returned");
} catch (err: any) {
console.error("Error uploading note attachment:", err);
toast.error("Failed to upload image to note.");
throw err;
}
};
export const cleanupNoteAttachments = async (noteId: string) => {
if (!pb.authStore.isValid) return;
try {
const note = store.notes.find(n => n.id === noteId);
if (!note) return;
const record = await pb.collection(NOTES_COLLECTION).getOne(noteId);
if (!record.attachments || record.attachments.length === 0) return;
const content = note.content || "";
const filesToDelete: string[] = [];
for (const filename of record.attachments) {
if (!content.includes(filename)) {
filesToDelete.push(filename);
}
}
if (filesToDelete.length > 0) {
await pb.collection(NOTES_COLLECTION).update(noteId, {
"attachments-": filesToDelete
});
console.log(`[CLEANUP] Deleted ${filesToDelete.length} orphaned attachment(s) for note ${noteId}`);
}
} catch (err) {
console.error("[CLEANUP] Failed to clean up orphaned note attachments:", err);
}
};
const mapRecordToShareRule = (r: any): ShareRule => { const mapRecordToShareRule = (r: any): ShareRule => {
return { return {
id: r.id, id: r.id,
+39 -2
View File
@@ -1,5 +1,5 @@
import { type Component, createSignal, createMemo, createEffect, For, Show } from "solid-js"; import { type Component, createSignal, createMemo, createEffect, onCleanup, For, Show } from "solid-js";
import { store, renameTagDefinition, updateNote, removeNote, restoreNote } from "@/store"; import { store, renameTagDefinition, updateNote, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments } from "@/store";
import { pb } from "@/lib/pocketbase"; import { pb } from "@/lib/pocketbase";
import { type Note } from "@/store"; import { type Note } from "@/store";
import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight } from "lucide-solid"; import { Trash2, Lock, Unlock, Search, Link, X, ChevronRight } from "lucide-solid";
@@ -21,6 +21,38 @@ export const NotepadView: Component<{
const currentUserId = pb.authStore.model?.id; const currentUserId = pb.authStore.model?.id;
// Sync activeNoteId for slash commands and run cleanup on unmount/change
createEffect(() => {
// By reading the prop inside the effect block, SolidJS tracks it reactively.
const currentId = props.selectedNoteId;
setActiveNoteId(currentId);
// This will run when the effect re-runs (currentId changes) or the component unmounts
onCleanup(() => {
setActiveNoteId(null);
// 1. Immediately flush any pending debounce updates for the outgoing note
if (contentUpdateTimeout) {
clearTimeout(contentUpdateTimeout);
if (pendingContentUpdate) {
// Fire update manually to PocketBase & Store synchronously (fire-and-forget)
updateNote(pendingContentUpdate.id, { content: pendingContentUpdate.html }).catch(console.error);
}
}
// 2. Clear pending state
contentUpdateTimeout = undefined;
pendingContentUpdate = null;
// 3. Trigger cleanup after a tiny delay so the updateNote above processes first
if (currentId) {
setTimeout(() => {
cleanupNoteAttachments(currentId).catch(console.error);
}, 100);
}
});
});
// Derived states // Derived states
const activeNote = createMemo(() => { const activeNote = createMemo(() => {
const id = props.selectedNoteId?.trim(); const id = props.selectedNoteId?.trim();
@@ -32,10 +64,15 @@ export const NotepadView: Component<{
}; };
let contentUpdateTimeout: number | undefined; let contentUpdateTimeout: number | undefined;
let pendingContentUpdate: { id: string, html: string } | null = null;
const handleUpdateContent = (id: string, html: string) => { const handleUpdateContent = (id: string, html: string) => {
clearTimeout(contentUpdateTimeout); clearTimeout(contentUpdateTimeout);
pendingContentUpdate = { id, html };
contentUpdateTimeout = window.setTimeout(async () => { contentUpdateTimeout = window.setTimeout(async () => {
await updateNote(id, { content: html }); await updateNote(id, { content: html });
pendingContentUpdate = null;
}, 500); }, 500);
}; };