/** * T089-T092: Note editor with split-pane and conflict handling */ import { useState, useEffect } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Save, X, AlertCircle } from 'lucide-react'; import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import type { Note } from '@/types/note'; import { createWikilinkComponent } from '@/lib/markdown.tsx'; import { updateNote, APIException } from '@/services/api'; interface NoteEditorProps { note: Note; onSave: (updatedNote: Note) => void; onCancel: () => void; onWikilinkClick: (linkText: string) => void; } export function NoteEditor({ note, onSave, onCancel, onWikilinkClick }: NoteEditorProps) { const [body, setBody] = useState(note.body); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); const [hasChanges, setHasChanges] = useState(false); // Track if content has changed useEffect(() => { setHasChanges(body !== note.body); }, [body, note.body]); // Create custom markdown components with wikilink handler const markdownComponents = createWikilinkComponent(onWikilinkClick); const handleSave = async () => { setIsSaving(true); setError(null); try { // T090: Send update with version for optimistic concurrency const updatedNote = await updateNote(note.note_path, { body, if_version: note.version, }); onSave(updatedNote); } catch (err) { // T092: Handle 409 Conflict if (err instanceof APIException && err.status === 409) { setError( 'This note changed since you opened it. Please reload before saving to avoid losing changes.' ); } else if (err instanceof APIException) { setError(err.error); } else { setError('Failed to save note'); } console.error('Error saving note:', err); } finally { setIsSaving(false); } }; // T091: Cancel handler const handleCancel = () => { if (hasChanges) { const confirmed = window.confirm( 'You have unsaved changes. Are you sure you want to cancel?' ); if (!confirmed) return; } onCancel(); }; // Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Cmd/Ctrl + S to save if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); if (hasChanges && !isSaving) { handleSave(); } } // Escape to cancel if (e.key === 'Escape') { e.preventDefault(); handleCancel(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [hasChanges, isSaving, body]); return (
{/* Header */}

{note.title}

{note.note_path} | Version {note.version} {hasChanges && ' | Unsaved changes'}

{/* Error Alert */} {error && ( {error} )}
{/* Split Pane Editor */}
{/* Left: Markdown Source */}

Markdown Source

{body.length} chars {body.split('\n').length} lines