fix: resolve view mode scrollbar loss and card deletion flashing loop
This commit is contained in:
1 parent
ea34960b52
commit
c355e2dfe3
2 files changed
+80
-28
No files matched your search
@@ -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}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in new issue
Block a user