Images adding in notes and cleanup of orphaned images from notes
This commit is contained in:
@@ -10,7 +10,7 @@ import {
|
||||
Trash2
|
||||
} from "lucide-solid";
|
||||
import { cn, convertImageToWebpFile } from "@/lib/utils";
|
||||
import { activeTaskId, uploadTaskAttachment } from "@/store";
|
||||
import { activeTaskId, uploadTaskAttachment, activeNoteId, uploadNoteAttachment } from "@/store";
|
||||
import { toast } from "solid-sonner";
|
||||
|
||||
export interface CommandItem {
|
||||
@@ -123,9 +123,12 @@ export const getSuggestionItems = ({ query }: { query: string }): CommandItem[]
|
||||
icon: ImageIcon,
|
||||
command: ({ editor, range }: { editor: any, range: any }) => {
|
||||
editor.chain().focus().deleteRange(range).run();
|
||||
|
||||
const taskId = activeTaskId();
|
||||
if (!taskId) {
|
||||
toast.error("Cannot upload image outside of a task.");
|
||||
const noteId = activeNoteId();
|
||||
|
||||
if (!taskId && !noteId) {
|
||||
toast.error("Cannot upload image outside of a task or note.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,7 +141,11 @@ export const getSuggestionItems = ({ query }: { query: string }): CommandItem[]
|
||||
toast.promise(
|
||||
(async () => {
|
||||
const webpFile = await convertImageToWebpFile(originalFile);
|
||||
if (taskId) {
|
||||
return uploadTaskAttachment(taskId, webpFile);
|
||||
} else {
|
||||
return uploadNoteAttachment(noteId!, webpFile);
|
||||
}
|
||||
})(),
|
||||
{
|
||||
loading: "Processing & uploading image...",
|
||||
|
||||
@@ -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
|
||||
type TaskContext = 'mine' | { userId: string, name: string } | { bucketId: string, name: string };
|
||||
export const [activeTaskId, setActiveTaskId] = createSignal<string | null>(null);
|
||||
export const [activeNoteId, setActiveNoteId] = createSignal<string | null>(null);
|
||||
export const [currentTaskContext, setCurrentTaskContext] = createSignal<TaskContext>('mine');
|
||||
|
||||
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 => {
|
||||
return {
|
||||
id: r.id,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type Component, createSignal, createMemo, createEffect, For, Show } from "solid-js";
|
||||
import { store, renameTagDefinition, updateNote, removeNote, restoreNote } from "@/store";
|
||||
import { type Component, createSignal, createMemo, createEffect, onCleanup, For, Show } from "solid-js";
|
||||
import { store, renameTagDefinition, updateNote, removeNote, restoreNote, setActiveNoteId, cleanupNoteAttachments } from "@/store";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { type Note } from "@/store";
|
||||
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;
|
||||
|
||||
// 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
|
||||
const activeNote = createMemo(() => {
|
||||
const id = props.selectedNoteId?.trim();
|
||||
@@ -32,10 +64,15 @@ export const NotepadView: Component<{
|
||||
};
|
||||
|
||||
let contentUpdateTimeout: number | undefined;
|
||||
let pendingContentUpdate: { id: string, html: string } | null = null;
|
||||
|
||||
const handleUpdateContent = (id: string, html: string) => {
|
||||
clearTimeout(contentUpdateTimeout);
|
||||
pendingContentUpdate = { id, html };
|
||||
|
||||
contentUpdateTimeout = window.setTimeout(async () => {
|
||||
await updateNote(id, { content: html });
|
||||
pendingContentUpdate = null;
|
||||
}, 500);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user