Images adding in notes and cleanup of orphaned images from notes
This commit is contained in:
@@ -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...",
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user