From 2cf6638d9ea1325fbd61df01948b272d3027ffa6 Mon Sep 17 00:00:00 2001 From: Hermes Date: Sat, 1 Aug 2026 13:18:08 +0000 Subject: [PATCH] fix: isolate note editor in memoized pane to prevent focus stealing on parent re-render --- apps/web/src/routes/_app/notes.tsx | 133 ++++++----- fix_pane2.py | 354 +++++++++++++++++++++++++++++ 2 files changed, 424 insertions(+), 63 deletions(-) create mode 100644 fix_pane2.py diff --git a/apps/web/src/routes/_app/notes.tsx b/apps/web/src/routes/_app/notes.tsx index 80991a8..9a7aff8 100644 --- a/apps/web/src/routes/_app/notes.tsx +++ b/apps/web/src/routes/_app/notes.tsx @@ -77,12 +77,78 @@ const NoteTitleInput = memo(function NoteTitleInput({ noteId, initialTitle }: { ); }); +// Memoized right pane - only re-renders when note changes, not on parent re-renders +const NoteEditorPane = memo(function NoteEditorPane({ note, onDelete }: { note: Note; onDelete: (id: string) => void }) { + const [showBacklinks, setShowBacklinks] = useState(false); + const [showVersions, setShowVersions] = useState(false); + const queryClient = useQueryClient(); + + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: string; data: any }) => api.patch("/notes/" + id, data), + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["notes"] }); }, + }); + + return ( + <> +
+ +
+ + + + + + + + + Delete Note + Are you sure you want to delete "{note.title}"? + + + Cancel + onDelete(note.id)} className="bg-destructive text-destructive-foreground">Delete + + + +
+
+
+ { updateMutation.mutate({ id: note.id, data: { content: html } }); }} /> +
+ {/* Backlinks section */} + {showBacklinks && note.backlinks && note.backlinks.length > 0 && ( +
+

Linked from

+
+ {note.backlinks.map((bl) => ( +
+ {bl.noteTitle} +
+ ))} +
+
+ )} + {/* Version history */} + {showVersions && ( +
+

Version History

+

Version history available via API.

+
+ )} + + ); +}); + function NotesPage() { const queryClient = useQueryClient(); const [search, setSearch] = useState(""); const [selectedNoteId, setSelectedNoteId] = useState(null); - const [showBacklinks, setShowBacklinks] = useState(false); - const [showVersions, setShowVersions] = useState(false); const selectedNoteRef = useRef(null); useRealtime({ enabled: true }); @@ -103,11 +169,6 @@ function NotesPage() { }, }); - const updateMutation = useMutation({ - mutationFn: ({ id, data }: { id: string; data: any }) => api.patch("/notes/" + id, data), - onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["notes"] }); }, - }); - const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete("/notes/" + id), onSuccess: () => { @@ -125,8 +186,6 @@ function NotesPage() { selectedNoteRef.current = note; } setSelectedNoteId(note.id); - setShowBacklinks(false); - setShowVersions(false); }; const selectedNote = selectedNoteRef.current; @@ -176,62 +235,10 @@ function NotesPage() { - {/* Right pane - editor */} + {/* Right pane - editor (memoized, won't re-render on parent state changes) */}
{selectedNote ? ( - <> -
- -
- - - - - - - - - Delete Note - Are you sure you want to delete "{selectedNote.title}"? - - - Cancel - deleteMutation.mutate(selectedNote.id)} className="bg-destructive text-destructive-foreground">Delete - - - -
-
-
- { if (selectedNoteRef.current) { updateMutation.mutate({ id: selectedNoteRef.current.id, data: { content: html } }); } }} /> -
- {/* Backlinks section */} - {showBacklinks && selectedNote.backlinks && selectedNote.backlinks.length > 0 && ( -
-

Linked from

-
- {selectedNote.backlinks.map((bl) => ( -
- {bl.noteTitle} -
- ))} -
-
- )} - {/* Version history */} - {showVersions && ( -
-

Version History

-

Version history available via API.

-
- )} - + deleteMutation.mutate(id)} /> ) : (
diff --git a/fix_pane2.py b/fix_pane2.py new file mode 100644 index 0000000..0f5a309 --- /dev/null +++ b/fix_pane2.py @@ -0,0 +1,354 @@ +import sys + +with open(sys.argv[1], 'r') as f: + content = f.read() + +# Replace the entire NotesPage function and add NoteEditorPane +old = '''function NotesPage() { + const queryClient = useQueryClient(); + const [search, setSearch] = useState(""); + const [selectedNoteId, setSelectedNoteId] = useState(null); + const [showBacklinks, setShowBacklinks] = useState(false); + const [showVersions, setShowVersions] = useState(false); + const selectedNoteRef = useRef(null); + + useRealtime({ enabled: true }); + + const { data: notesData, isLoading } = useApiQuery>( + ["notes", search], + "/notes?limit=200" + (search ? "&search=" + encodeURIComponent(search) : "") + ); + + const notes = notesData?.items || []; + + const createMutation = useMutation({ + mutationFn: () => api.post("/notes", { title: "Untitled", content: "" }), + onSuccess: (note) => { + queryClient.invalidateQueries({ queryKey: ["notes"] }); + selectedNoteRef.current = note; + setSelectedNoteId(note.id); + }, + }); + + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: string; data: any }) => api.patch("/notes/" + id, data), + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["notes"] }); }, + }); + + const deleteMutation = useMutation({ + mutationFn: (id: string) => api.delete("/notes/" + id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["notes"] }); + selectedNoteRef.current = null; + setSelectedNoteId(null); + }, + }); + + const selectNote = async (note: Note) => { + try { + const detail = await api.get("/notes/" + note.id); + selectedNoteRef.current = detail; + } catch { + selectedNoteRef.current = note; + } + setSelectedNoteId(note.id); + setShowBacklinks(false); + setShowVersions(false); + }; + + const selectedNote = selectedNoteRef.current; + + return ( +
+ {/* Left pane - note list */} +
+
+
+ + setSearch(e.target.value)} className="pl-8" tabIndex={-1} /> +
+
+
+ +
+ + {isLoading ? ( +
Loading...
+ ) : notes.length === 0 ? ( +
No notes yet.
+ ) : ( +
+ {notes.map((note) => ( + + ))} +
+ )} +
+
+ + {/* Right pane - editor */} +
+ {selectedNote ? ( + <> +
+ +
+ + + + + + + + + Delete Note + Are you sure you want to delete "{selectedNote.title}"? + + + Cancel + deleteMutation.mutate(selectedNote.id)} className="bg-destructive text-destructive-foreground">Delete + + + +
+
+
+ { if (selectedNoteRef.current) { updateMutation.mutate({ id: selectedNoteRef.current.id, data: { content: html } }); } }} /> +
+ {/* Backlinks section */} + {showBacklinks && selectedNote.backlinks && selectedNote.backlinks.length > 0 && ( +
+

Linked from

+
+ {selectedNote.backlinks.map((bl) => ( +
+ {bl.noteTitle} +
+ ))} +
+
+ )} + {/* Version history */} + {showVersions && ( +
+

Version History

+

Version history available via API.

+
+ )} + + ) : ( +
+
+ +

Select a note or create a new one

+
+
+ )} +
+
+ ); +}''' + +new = '''// Memoized right pane - only re-renders when note changes, not on parent re-renders +const NoteEditorPane = memo(function NoteEditorPane({ note, onDelete }: { note: Note; onDelete: (id: string) => void }) { + const [showBacklinks, setShowBacklinks] = useState(false); + const [showVersions, setShowVersions] = useState(false); + const queryClient = useQueryClient(); + + const updateMutation = useMutation({ + mutationFn: ({ id, data }: { id: string; data: any }) => api.patch("/notes/" + id, data), + onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["notes"] }); }, + }); + + return ( + <> +
+ +
+ + + + + + + + + Delete Note + Are you sure you want to delete "{note.title}"? + + + Cancel + onDelete(note.id)} className="bg-destructive text-destructive-foreground">Delete + + + +
+
+
+ { updateMutation.mutate({ id: note.id, data: { content: html } }); }} /> +
+ {/* Backlinks section */} + {showBacklinks && note.backlinks && note.backlinks.length > 0 && ( +
+

Linked from

+
+ {note.backlinks.map((bl) => ( +
+ {bl.noteTitle} +
+ ))} +
+
+ )} + {/* Version history */} + {showVersions && ( +
+

Version History

+

Version history available via API.

+
+ )} + + ); +}); + +function NotesPage() { + const queryClient = useQueryClient(); + const [search, setSearch] = useState(""); + const [selectedNoteId, setSelectedNoteId] = useState(null); + const selectedNoteRef = useRef(null); + + useRealtime({ enabled: true }); + + const { data: notesData, isLoading } = useApiQuery>( + ["notes", search], + "/notes?limit=200" + (search ? "&search=" + encodeURIComponent(search) : "") + ); + + const notes = notesData?.items || []; + + const createMutation = useMutation({ + mutationFn: () => api.post("/notes", { title: "Untitled", content: "" }), + onSuccess: (note) => { + queryClient.invalidateQueries({ queryKey: ["notes"] }); + selectedNoteRef.current = note; + setSelectedNoteId(note.id); + }, + }); + + const deleteMutation = useMutation({ + mutationFn: (id: string) => api.delete("/notes/" + id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["notes"] }); + selectedNoteRef.current = null; + setSelectedNoteId(null); + }, + }); + + const selectNote = async (note: Note) => { + try { + const detail = await api.get("/notes/" + note.id); + selectedNoteRef.current = detail; + } catch { + selectedNoteRef.current = note; + } + setSelectedNoteId(note.id); + }; + + const selectedNote = selectedNoteRef.current; + + return ( +
+ {/* Left pane - note list */} +
+
+
+ + setSearch(e.target.value)} className="pl-8" tabIndex={-1} /> +
+
+
+ +
+ + {isLoading ? ( +
Loading...
+ ) : notes.length === 0 ? ( +
No notes yet.
+ ) : ( +
+ {notes.map((note) => ( + + ))} +
+ )} +
+
+ + {/* Right pane - editor (memoized, won't re-render on parent state changes) */} +
+ {selectedNote ? ( + deleteMutation.mutate(id)} /> + ) : ( +
+
+ +

Select a note or create a new one

+
+
+ )} +
+
+ ); +}''' + +content = content.replace(old, new, 1) + +with open(sys.argv[1], 'w') as f: + f.write(content) +print('Done')