fix: remove duplicate useMemo inside handleGenerateTitles (hooks violation = broken save+title), add Save btn + three-dot delete + Ctrl+S to fullPage toolbar

This commit is contained in:
Antigravity
2026-05-07 23:52:21 +00:00
parent 29e65038b7
commit a58610003d

View File

@@ -25,8 +25,8 @@ import {
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { NoteTypeSelector } from '@/components/note-type-selector' import { NoteTypeSelector } from '@/components/note-type-selector'
import { RichTextEditor } from '@/components/rich-text-editor' import { RichTextEditor } from '@/components/rich-text-editor'
import { X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles, Maximize2, Copy, Wand2, LogOut, ArrowLeft, ChevronRight, Info, Check, Loader2 } from 'lucide-react' import { X, Plus, Palette, Image as ImageIcon, Bell, Eye, Link as LinkIcon, Sparkles, Maximize2, Copy, Wand2, LogOut, ArrowLeft, ChevronRight, Info, Check, Loader2, Save, MoreHorizontal, Trash2 } from 'lucide-react'
import { updateNote, createNote, cleanupOrphanedImages, leaveSharedNote } from '@/app/actions/notes' import { updateNote, createNote, cleanupOrphanedImages, leaveSharedNote, deleteNote } from '@/app/actions/notes'
import { format } from 'date-fns' import { format } from 'date-fns'
import { useTitleSuggestions } from '@/hooks/use-title-suggestions' import { useTitleSuggestions } from '@/hooks/use-title-suggestions'
import { MarkdownSlashCommands } from './markdown-slash-commands' import { MarkdownSlashCommands } from './markdown-slash-commands'
@@ -329,12 +329,6 @@ export function NoteEditor({ note, readOnly = false, onClose, fullPage = false }
.join(' ') .join(' ')
.trim() .trim()
const allImages = useMemo(() => {
const extracted = noteType === 'richtext' ? extractImagesFromHTML(content) : [];
return Array.from(new Set([...images, ...extracted]));
}, [images, content, noteType]);
const wordCount = fullContentForAI.split(/\s+/).filter(word => word.length > 0).length const wordCount = fullContentForAI.split(/\s+/).filter(word => word.length > 0).length
@@ -680,7 +674,54 @@ export function NoteEditor({ note, readOnly = false, onClose, fullPage = false }
} }
} }
// ── fullPage mode: editorial layout (fidèle au prototype) ── // Save in place (fullPage) — without closing
const handleSaveInPlace = async () => {
setIsSaving(true)
try {
await updateNote(note.id, {
title: title.trim() || null,
content: noteType !== 'checklist' ? content : '',
checkItems: noteType === 'checklist' ? checkItems : null,
labels,
images,
links,
color,
reminder: currentReminder,
isMarkdown: noteType === 'markdown',
type: noteType,
size,
})
if (removedImageUrls.length > 0) {
cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
}
await refreshLabels()
triggerRefresh()
setIsDirty(false)
toast.success('Note sauvegardée !')
} catch (error) {
console.error('Failed to save note:', error)
toast.error('Erreur lors de la sauvegarde.')
} finally {
setIsSaving(false)
}
}
// Ctrl+S / Cmd+S shortcut — save in place in fullPage mode
// Note: this useEffect must be outside the if(fullPage) branch (no conditional hooks)
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (!fullPage) return
const handler = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault()
handleSaveInPlace()
}
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fullPage, isSaving])
if (fullPage) { if (fullPage) {
const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null const notebookName = notebooks.find(nb => nb.id === note.notebookId)?.name || null
@@ -766,6 +807,50 @@ export function NoteEditor({ note, readOnly = false, onClose, fullPage = false }
<Info size={16} /> <Info size={16} />
<span>Document Info</span> <span>Document Info</span>
</button> </button>
{/* Save button */}
{!readOnly && (
<button
onClick={handleSaveInPlace}
disabled={isSaving || !isDirty}
className={cn(
'flex items-center gap-2 px-3 py-1.5 rounded-full border transition-all duration-300 text-xs font-medium',
isDirty
? 'bg-foreground text-background border-foreground hover:opacity-80'
: 'border-black/20 dark:border-white/20 text-foreground/40 cursor-not-allowed'
)}
>
{isSaving ? <Loader2 size={14} className="animate-spin" /> : <Save size={14} />}
<span>{isSaving ? 'Saving…' : 'Save'}</span>
</button>
)}
{/* Three-dot options menu */}
{!readOnly && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="p-1.5 rounded-full border border-black/20 dark:border-white/20 text-foreground hover:bg-black/5 dark:hover:bg-white/5 transition-all">
<MoreHorizontal size={16} />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem
onClick={async () => {
try {
await deleteNote(note.id)
triggerRefresh()
toast.success('Note supprimée.')
onClose()
} catch { toast.error('Impossible de supprimer.') }
}}
className="text-red-600 dark:text-red-400 focus:text-red-600"
>
<Trash2 className="h-4 w-4 mr-2" />
Supprimer la note
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div> </div>
</div> </div>