fix: resolve view mode scrollbar loss and card deletion flashing loop

This commit is contained in:
chrisf committed 2026-10-22 22:08:21 -06:00
1 parent ea34960b52
commit c355e2dfe3
2 files changed
+80 -28

No files matched your search

+74 -24
View File
@@ -693,16 +693,54 @@ export default function EvercloneWorkspace() {
}
};
const handleCachedThumbnail = useCallback((nid: string, dataUrl: string) => {
setNotes((prev) =>
prev.map((item) =>
item.id === nid ? { ...item, thumbnailUrl: dataUrl } : item
)
);
}, []);
const handleDeleteOrTrashNote = async (permanent = false) => {
if (!activeNote) return;
if (permanent && !window.confirm('Permanently delete this note? This cannot be undone.')) {
return;
}
await fetch(`/api/notes/${activeNote.id}?permanent=${permanent ? 'true' : 'false'}`, {
method: 'DELETE',
});
await fetchSidebar();
await fetchNotesList();
if (saveTimerRef.current) {
clearTimeout(saveTimerRef.current);
saveTimerRef.current = null;
}
const deletedId = activeNote.id;
// Determine the next note to select (either the next one in the list or the previous one)
const currentIndex = notes.findIndex((n) => n.id === deletedId);
let nextNoteId = '';
if (currentIndex !== -1) {
if (currentIndex + 1 < notes.length) {
nextNoteId = notes[currentIndex + 1].id;
} else if (currentIndex > 0) {
nextNoteId = notes[currentIndex - 1].id;
}
}
// Optimistically update list to prevent flashing and ghost states
setNotes((prev) => prev.filter((n) => n.id !== deletedId));
setSelectedNoteId(nextNoteId);
if (!nextNoteId) {
setActiveNote(null);
}
try {
await fetch(`/api/notes/${deletedId}?permanent=${permanent ? 'true' : 'false'}`, {
method: 'DELETE',
});
await fetchSidebar();
await fetchNotesList(nextNoteId || undefined);
} catch (e) {
console.error('Failed to delete note:', e);
await fetchNotesList();
}
};
const handleEmptyTrash = async () => {
@@ -937,9 +975,31 @@ export default function EvercloneWorkspace() {
getScrollElement: () => listParentRef.current,
estimateSize: () =>
listMode === 'compact' ? 52 : listMode === 'cards' ? 240 : 112,
getItemKey: (index: number) => {
if (listMode === 'cards') {
const start = index * 2;
const n1 = notes[start]?.id || index;
const n2 = notes[start + 1]?.id || '';
return `cards-${n1}-${n2}`;
}
return `${listMode}-${notes[index]?.id || index}`;
},
overscan: 8,
});
// When switching between snippets, cards, and compact views:
// Reset scroll position and remeasure virtualizer so scrollbar never disappears or desyncs
const prevListModeRef = useRef(listMode);
useEffect(() => {
if (prevListModeRef.current !== listMode) {
prevListModeRef.current = listMode;
if (listParentRef.current) {
listParentRef.current.scrollTop = 0;
}
rowVirtualizer.measure();
}
}, [listMode, rowVirtualizer]);
// Infinite scroll trigger: when scrolling near bottom, fetch next page of notes
const virtualItems = rowVirtualizer.getVirtualItems();
useEffect(() => {
@@ -2295,7 +2355,7 @@ export default function EvercloneWorkspace() {
</div>
{/* Note List Body: Card Grid vs Virtualized Snippet / Compact List */}
<div ref={listParentRef} className="flex-1 overflow-y-auto">
<div ref={listParentRef} className="flex-1 overflow-y-scroll">
{loadingList && notes.length === 0 ? (
<div className="p-8 text-center text-xs text-gray-400">Loading notes…</div>
) : notes.length === 0 ? (
@@ -2348,10 +2408,11 @@ export default function EvercloneWorkspace() {
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const startIndex = virtualRow.index * 2;
const rowNotes = notes.slice(startIndex, startIndex + 2);
if (rowNotes.length === 0) return null;
return (
<div
key={`card-row-${virtualRow.index}`}
key={`card-row-${virtualRow.index}-${rowNotes.map((r) => r.id).join('-')}`}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
style={{
@@ -2386,7 +2447,7 @@ export default function EvercloneWorkspace() {
e.dataTransfer.setData('text/everclone-note-id', n.id);
}}
onClick={() => openNoteInEditor(n.id)}
className={`group/card cursor-pointer rounded-xl border transition flex flex-col overflow-hidden shadow-xs ${
className={`group/card cursor-pointer rounded-xl border transition-colors duration-150 flex flex-col overflow-hidden shadow-xs ${
isSelected
? 'bg-emerald-50/60 border-evernote-green ring-2 ring-evernote-green/30'
: 'bg-white border-gray-200/90 hover:border-gray-300 hover:shadow-md'
@@ -2402,7 +2463,7 @@ export default function EvercloneWorkspace() {
/>
{firstPdf && (
<span className="absolute bottom-1.5 left-2 px-1.5 py-0.5 rounded bg-red-600/90 text-white text-[9px] font-bold uppercase tracking-wider shadow">
PDF
PDF
</span>
)}
<button
@@ -2431,13 +2492,7 @@ export default function EvercloneWorkspace() {
setThumbnailInitialUrl(dataUrl);
setThumbnailEditorOpen(true);
}}
onCachedThumbnail={(nid, dataUrl) => {
setNotes((prev) =>
prev.map((item) =>
item.id === nid ? { ...item, thumbnailUrl: dataUrl } : item
)
);
}}
onCachedThumbnail={handleCachedThumbnail}
/>
)
)}
@@ -2532,6 +2587,7 @@ export default function EvercloneWorkspace() {
>
{rowVirtualizer.getVirtualItems().map((virtualRow) => {
const n = notes[virtualRow.index];
if (!n) return null;
const isSelected = n.id === selectedNoteId;
const firstImg = n.attachments.find((a) =>
a.mimeType.startsWith('image/')
@@ -2563,7 +2619,7 @@ export default function EvercloneWorkspace() {
width: '100%',
transform: `translateY(${virtualRow.start}px)`,
}}
className={`cursor-pointer border-b border-gray-200/80 transition px-4 ${
className={`cursor-pointer border-b border-gray-200/80 transition-colors duration-150 px-4 ${
listMode === 'compact' ? 'py-2' : 'py-3.5'
} ${
isSelected
@@ -2698,13 +2754,7 @@ export default function EvercloneWorkspace() {
setThumbnailInitialUrl(dataUrl);
setThumbnailEditorOpen(true);
}}
onCachedThumbnail={(nid, dataUrl) => {
setNotes((prev) =>
prev.map((item) =>
item.id === nid ? { ...item, thumbnailUrl: dataUrl } : item
)
);
}}
onCachedThumbnail={handleCachedThumbnail}
/>
)
)}
+6 -4
View File
@@ -1,6 +1,6 @@
'use client';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useRef } from 'react';
import {
FileText,
ChevronDown,
@@ -32,6 +32,8 @@ export function PdfAutoThumbnail({
}) {
const [dataUrl, setDataUrl] = useState<string | null>(null);
const pdfUrl = `/api/attachments/${pdfAttachmentId}`;
const onCachedRef = useRef(onCachedThumbnail);
onCachedRef.current = onCachedThumbnail;
useEffect(() => {
let cancelled = false;
@@ -39,8 +41,8 @@ export function PdfAutoThumbnail({
const rendered = await renderPdfPageToDataUrl(pdfUrl, 1, 360);
if (!cancelled && rendered) {
setDataUrl(rendered);
if (onCachedThumbnail) {
onCachedThumbnail(noteId, rendered);
if (onCachedRef.current) {
onCachedRef.current(noteId, rendered);
}
// Persist the generated Page-1 thumbnail on the note so future list loads are instant
try {
@@ -57,7 +59,7 @@ export function PdfAutoThumbnail({
return () => {
cancelled = true;
};
}, [noteId, pdfUrl, onCachedThumbnail]);
}, [noteId, pdfUrl]);
if (mode === 'cards') {
return (