Attempt to fix note resizing with React keys and Muuri sync

This commit is contained in:
2026-01-24 19:52:13 +01:00
parent d59ec592eb
commit 8e35780717
48 changed files with 3369 additions and 279 deletions

View File

@@ -20,12 +20,13 @@ interface MasonryItemProps {
note: Note;
onEdit: (note: Note, readOnly?: boolean) => void;
onResize: () => void;
onNoteSizeChange: (noteId: string, newSize: 'small' | 'medium' | 'large') => void;
onDragStart?: (noteId: string) => void;
onDragEnd?: () => void;
isDragging?: boolean;
}
const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onDragStart, onDragEnd, isDragging }: MasonryItemProps) {
const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onNoteSizeChange, onDragStart, onDragEnd, isDragging }: MasonryItemProps) {
const resizeRef = useResizeObserver(onResize);
return (
@@ -43,13 +44,12 @@ const MasonryItem = memo(function MasonryItem({ note, onEdit, onResize, onDragSt
onDragStart={onDragStart}
onDragEnd={onDragEnd}
isDragging={isDragging}
onResize={onResize}
onSizeChange={(newSize) => onNoteSizeChange(note.id, newSize)}
/>
</div>
</div>
);
}, (prev, next) => {
// Custom comparison to avoid re-render on function prop changes if note data is same
return prev.note.id === next.note.id && prev.isDragging === next.isDragging;
});
export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
@@ -57,6 +57,22 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const [editingNote, setEditingNote] = useState<{ note: Note; readOnly?: boolean } | null>(null);
const { startDrag, endDrag, draggedNoteId } = useNotebookDrag();
// Local state for notes with dynamic size updates
// This allows size changes to propagate immediately without waiting for server
const [localNotes, setLocalNotes] = useState<Note[]>(notes);
// Sync localNotes when parent notes prop changes
useEffect(() => {
setLocalNotes(notes);
}, [notes]);
// Callback for when a note's size changes - update local state immediately
const handleNoteSizeChange = useCallback((noteId: string, newSize: 'small' | 'medium' | 'large') => {
setLocalNotes(prevNotes =>
prevNotes.map(n => n.id === noteId ? { ...n, size: newSize } : n)
);
}, []);
const handleEdit = useCallback((note: Note, readOnly?: boolean) => {
if (onEdit) {
onEdit(note, readOnly);
@@ -70,14 +86,14 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const pinnedMuuri = useRef<any>(null);
const othersMuuri = useRef<any>(null);
// Memoize filtered notes (order comes from array)
// Memoize filtered notes from localNotes (which includes dynamic size updates)
const pinnedNotes = useMemo(
() => notes.filter(n => n.isPinned),
[notes]
() => localNotes.filter(n => n.isPinned),
[localNotes]
);
const othersNotes = useMemo(
() => notes.filter(n => !n.isPinned),
[notes]
() => localNotes.filter(n => !n.isPinned),
[localNotes]
);
const handleDragEnd = useCallback(async (grid: any) => {
@@ -152,7 +168,7 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const columns = calculateColumns(containerWidth);
const itemWidth = calculateItemWidth(containerWidth, columns);
console.log(`[Masonry] Container width: ${containerWidth}px, Columns: ${columns}, Item width: ${itemWidth}px`);
const layoutOptions = {
dragEnabled: true,
@@ -279,6 +295,13 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
requestAnimationFrame(() => {
syncGridItems(pinnedMuuri.current, pinnedGridRef, pinnedNotes);
syncGridItems(othersMuuri.current, othersGridRef, othersNotes);
// CRITICAL: Force a second layout after CSS transitions (padding/height changes) complete
// NoteCard has a 200ms transition. We wait 300ms to be safe.
setTimeout(() => {
if (pinnedMuuri.current) pinnedMuuri.current.refreshItems().layout();
if (othersMuuri.current) othersMuuri.current.refreshItems().layout();
}, 300);
});
}, [pinnedNotes, othersNotes]); // Re-run when notes change
@@ -295,7 +318,7 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
const containerWidth = entries[0]?.contentRect.width || window.innerWidth - 32;
const columns = calculateColumns(containerWidth);
console.log(`[Masonry Resize] Width: ${containerWidth}px, Columns: ${columns}`);
// Apply dimensions to both grids
applyItemDimensions(pinnedMuuri.current, containerWidth);
@@ -331,10 +354,11 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
<div ref={pinnedGridRef} className="relative min-h-[100px]">
{pinnedNotes.map(note => (
<MasonryItem
key={note.id}
key={`${note.id}-${note.size}`}
note={note}
onEdit={handleEdit}
onResize={refreshLayout}
onNoteSizeChange={handleNoteSizeChange}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
@@ -352,10 +376,11 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
<div ref={othersGridRef} className="relative min-h-[100px]">
{othersNotes.map(note => (
<MasonryItem
key={note.id}
key={`${note.id}-${note.size}`}
note={note}
onEdit={handleEdit}
onResize={refreshLayout}
onNoteSizeChange={handleNoteSizeChange}
onDragStart={startDrag}
onDragEnd={endDrag}
isDragging={draggedNoteId === note.id}
@@ -373,39 +398,6 @@ export function MasonryGrid({ notes, onEdit }: MasonryGridProps) {
/>
)}
<style jsx global>{`
.masonry-container {
width: 100%;
}
.masonry-item {
display: block;
position: absolute;
z-index: 1;
box-sizing: border-box;
}
.masonry-item.muuri-item-dragging {
z-index: 3;
opacity: 0.8;
}
.masonry-item.muuri-item-releasing {
z-index: 2;
}
.masonry-item.muuri-item-hidden {
z-index: 0;
}
.masonry-item-content {
position: relative;
width: 100%;
height: 100%;
box-sizing: border-box;
}
/* Ensure proper box-sizing for all elements in the grid */
.masonry-item *,
.masonry-item-content * {
box-sizing: border-box;
}
`}</style>
</div>
);
}