feat: icon-only toolbar, versioning fixes, history modal, PanelRight repositioning

- Toolbar: remove text labels from all icon buttons (AI, Save, Preview, Convert)
  all buttons now icon-only with title tooltip for accessibility
- Toolbar: reposition PanelRight (info panel toggle) to far right after three-dot menu
- Versioning: decouple getNoteHistory/restoreNoteVersion from global userAISettings.noteHistory
  now checks note.historyEnabled directly — unblocks manual per-note history
- Versioning: add 'Sauvegarder cette version' button in Versions tab of info panel
  calls commitNoteHistory with visual feedback (spinner → success state)
- note-document-info-panel: import commitNoteHistory, add isSavingVersion state
- notes.ts: fix double guard that silently blocked all history operations
This commit is contained in:
Antigravity
2026-05-09 07:28:03 +00:00
parent 574c8b3166
commit 97b08e5d0b
65 changed files with 2991 additions and 2296 deletions

View File

@@ -25,10 +25,11 @@ interface NoteEditorProviderProps {
note: Note
readOnly?: boolean
fullPage?: boolean
onNoteSaved?: (savedNote: Note) => void
children: ReactNode
}
export function NoteEditorProvider({ note, readOnly = false, fullPage = false, children }: NoteEditorProviderProps) {
export function NoteEditorProvider({ note, readOnly = false, fullPage = false, onNoteSaved, children }: NoteEditorProviderProps) {
const { data: session } = useSession()
const { t } = useLanguage()
const queryClient = useQueryClient()
@@ -133,6 +134,13 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
enabled: fullPage && !title && !dismissedTitleSuggestions,
})
// Wire autoTitleSuggestions into state so NoteTitleBlock can display them
useEffect(() => {
if (autoTitleSuggestions.length > 0) {
setTitleSuggestions(autoTitleSuggestions)
}
}, [autoTitleSuggestions])
// Track previous content for copilot action undo
const [previousContentForCopilot, setPreviousContentForCopilot] = useState<string | null>(null)
@@ -169,7 +177,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
for (const file of Array.from(files)) {
try {
const url = await uploadImageFile(file)
setImages(prev => [...prev, url])
setImages(prev => prev.includes(url) ? prev : [...prev, url])
} catch (error) {
console.error('Upload error:', error)
toast.error(t('notes.uploadFailed', { filename: file.name }))
@@ -190,7 +198,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
if (!file) continue
try {
const url = await uploadImageFile(file)
setImages(prev => [...prev, url])
setImages(prev => prev.includes(url) ? prev : [...prev, url])
} catch {
toast.error(t('notes.uploadFailed', { filename: 'pasted image' }))
}
@@ -293,7 +301,14 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
const data = await response.json()
setTitleSuggestions(data.suggestions || [])
toast.success(t('ai.titlesGenerated', { count: data.suggestions.length }))
// Auto-apply first title for dialog mode (fullPage shows suggestions UI instead)
if (!fullPage && data.suggestions?.[0]?.title) {
setTitle(data.suggestions[0].title)
setDismissedTitleSuggestions(true)
toast.success(t('ai.titlesGenerated', { count: data.suggestions.length }))
} else if (data.suggestions?.length) {
toast.success(t('ai.titlesGenerated', { count: data.suggestions.length }))
}
} catch (error: any) {
console.error('Error generating titles:', error)
toast.error(error.message || t('ai.titleGenerationFailed'))
@@ -521,9 +536,11 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
}
const handleSave = async () => {
console.log('[SAVE] handleSave called, note.id:', note.id)
setIsSaving(true)
try {
await updateNote(note.id, {
console.log('[SAVE] Calling updateNote...')
const result = await updateNote(note.id, {
title: title.trim() || null,
content: noteType !== 'checklist' ? content : '',
checkItems: noteType === 'checklist' ? checkItems : null,
@@ -536,20 +553,25 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
type: noteType,
size,
})
console.log('[SAVE] updateNote succeeded, result title:', result?.title, 'result content len:', result?.content?.length)
console.log('[SAVE] prevNoteRef BEFORE sync:', JSON.stringify(prevNoteRef.current.content)?.substring(0, 80))
// Keep local note ref in sync with saved data so useEffect detects changes correctly
prevNoteRef.current = { ...prevNoteRef.current, ...result }
console.log('[SAVE] prevNoteRef AFTER sync:', JSON.stringify(prevNoteRef.current.content)?.substring(0, 80))
if (removedImageUrls.length > 0) {
cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
}
await refreshLabels()
// Notify parent with the freshly-saved note so it can update its local state immediately
onNoteSaved?.(result)
// Invalidate note and notes list cache
queryClient.invalidateQueries({ queryKey: queryKeys.note(note.id) })
queryClient.invalidateQueries({ queryKey: queryKeys.notes(note.notebookId) })
triggerRefresh()
// Note: onClose is handled by the composition component
toast.success(t('notes.saved') || 'Note sauvegardée !')
} catch (error) {
console.error('Failed to save note:', error)
console.error('[SAVE] updateNote failed:', error)
toast.error(t('notes.saveFailed') || 'Erreur lors de la sauvegarde.')
} finally {
setIsSaving(false)
}
@@ -633,9 +655,11 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
// Save in place (fullPage) — without closing
const handleSaveInPlace = async () => {
console.log('[SAVE] handleSaveInPlace called, note.id:', note.id, 'content length:', content.length, 'title:', title.substring(0, 50))
setIsSaving(true)
try {
await updateNote(note.id, {
console.log('[SAVE] Calling updateNote with note.id:', note.id, '| content len:', content.length, '| title:', title.substring(0, 30))
const updatePayload = {
title: title.trim() || null,
content: noteType !== 'checklist' ? content : '',
checkItems: noteType === 'checklist' ? checkItems : null,
@@ -647,11 +671,20 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
isMarkdown: noteType === 'markdown',
type: noteType,
size,
})
}
console.log('[SAVE] payload.content:', JSON.stringify(updatePayload.content)?.substring(0, 100))
const result = await updateNote(note.id, updatePayload)
console.log('[SAVE] updateNote succeeded, result.id:', result?.id, '| result.content len:', result?.content?.length, '| result.title:', result?.title)
console.log('[SAVE] prevNoteRef BEFORE sync:', JSON.stringify(prevNoteRef.current.content)?.substring(0, 50))
// Sync local note reference with saved data so prop/state stay aligned after save
prevNoteRef.current = { ...prevNoteRef.current, ...result }
console.log('[SAVE] prevNoteRef AFTER sync:', JSON.stringify(prevNoteRef.current.content)?.substring(0, 50))
if (removedImageUrls.length > 0) {
cleanupOrphanedImages(removedImageUrls, note.id).catch(() => {})
}
await refreshLabels()
// Notify parent with the freshly-saved note so it can update its local state immediately
onNoteSaved?.(result)
// Invalidate note and notes list cache
queryClient.invalidateQueries({ queryKey: queryKeys.note(note.id) })
queryClient.invalidateQueries({ queryKey: queryKeys.notes(note.notebookId) })
@@ -659,7 +692,7 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
setIsDirty(false)
toast.success('Note sauvegardée !')
} catch (error) {
console.error('Failed to save note:', error)
console.error('[SAVE] updateNote failed:', error)
toast.error('Erreur lors de la sauvegarde.')
} finally {
setIsSaving(false)
@@ -725,8 +758,11 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
isAnalyzingSuggestions, isMarkdown, allImages, colorClasses
])
// Build actions object
const actions: NoteEditorActions = useMemo(() => ({
// Build actions object — NOT memoized to avoid stale closures.
// handleSave / handleSaveInPlace close over content, title, labels, etc.
// which change on every keystroke. Memoizing with [] would freeze those
// values at the first render, causing the wrong content to be saved.
const actions: NoteEditorActions = {
setTitle: (t) => { setTitle(t); setIsDirty(true); setDismissedTitleSuggestions(true) },
setDismissedTitleSuggestions,
setContent: (c) => { setContent(c); setIsDirty(true) },
@@ -775,9 +811,10 @@ export function NoteEditorProvider({ note, readOnly = false, fullPage = false, c
setInfoOpen,
setIsProcessingAI,
setIsGeneratingTitles,
setIsAnalyzingSuggestions: (a) => { /* handled by useAutoTagging */ },
setIsAnalyzingSuggestions: (_a) => { /* handled by useAutoTagging */ },
setPreviousContentForCopilot,
}), [])
}
const value: NoteEditorContextValue = useMemo(() => ({
note,