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:
@@ -11,7 +11,7 @@ import { NotesEditorialView } from '@/components/notes-editorial-view'
|
||||
import { MemoryEchoNotification } from '@/components/memory-echo-notification'
|
||||
import { NotebookSuggestionToast } from '@/components/notebook-suggestion-toast'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Plus, ArrowUpDown } from 'lucide-react'
|
||||
import { Plus, ArrowUpDown, Search, Share2 } from 'lucide-react'
|
||||
import { useNoteRefresh } from '@/context/NoteRefreshContext'
|
||||
import { useRefresh } from '@/lib/use-refresh'
|
||||
import { useReminderCheck } from '@/hooks/use-reminder-check'
|
||||
@@ -71,6 +71,9 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
const [isCreating, startCreating] = useTransition()
|
||||
const [sortOrder, setSortOrder] = useState<SortOrder>('newest')
|
||||
const [showSortMenu, setShowSortMenu] = useState(false)
|
||||
const [showInlineSearch, setShowInlineSearch] = useState(false)
|
||||
const [inlineSearchQuery, setInlineSearchQuery] = useState('')
|
||||
const inlineSearchRef = useRef<HTMLInputElement>(null)
|
||||
const notesRef = useRef(notes)
|
||||
notesRef.current = notes
|
||||
const { refreshKey, triggerRefresh } = useNoteRefresh()
|
||||
@@ -147,10 +150,12 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
}
|
||||
}, [searchParams, labels, router])
|
||||
|
||||
const handleOpenNote = (noteId: string) => {
|
||||
const note = notes.find(n => n.id === noteId)
|
||||
if (note) setEditingNote({ note, readOnly: false })
|
||||
}
|
||||
// Always fetch fresh from server — avoids stale state after a save regardless of
|
||||
// whether the notes list has re-fetched yet.
|
||||
const handleOpenNoteFresh = useCallback(async (noteId: string, readOnly = false) => {
|
||||
const note = await getNoteById(noteId)
|
||||
if (note) setEditingNote({ note, readOnly })
|
||||
}, [])
|
||||
|
||||
const handleAddNote = () => {
|
||||
startCreating(async () => {
|
||||
@@ -205,13 +210,12 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
|
||||
let cancelled = false
|
||||
const run = async () => {
|
||||
const existing = notesRef.current.find(n => n.id === openNoteId)
|
||||
const note = existing ?? (await getNoteById(openNoteId))
|
||||
// Always fetch fresh data from DB to avoid showing stale content after a save.
|
||||
// notesRef.current can be stale if the notes list hasn't re-fetched yet when the
|
||||
// user closes and re-opens the note quickly after saving.
|
||||
const note = await getNoteById(openNoteId)
|
||||
if (cancelled || !note) return
|
||||
setEditingNote(prev => {
|
||||
if (prev?.note.id === note.id && prev.readOnly === false) return prev
|
||||
return { note, readOnly: false }
|
||||
})
|
||||
setEditingNote({ note, readOnly: false })
|
||||
}
|
||||
run()
|
||||
return () => {
|
||||
@@ -347,6 +351,16 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
refreshNotes(searchParams.get('notebook') || null)
|
||||
}, [refreshNotes, router, searchParams])
|
||||
|
||||
// Called by NoteEditor when a save succeeds — update local state immediately
|
||||
// so the user sees fresh data if they reopen the note before getAllNotes() completes
|
||||
const handleNoteSaved = useCallback((savedNote: Note) => {
|
||||
setNotes(prev => prev.map(n => n.id === savedNote.id ? { ...n, ...savedNote } : n))
|
||||
setPinnedNotes(prev => prev.map(n => n.id === savedNote.id ? { ...n, ...savedNote } : n))
|
||||
setEditingNote(prev => prev?.note.id === savedNote.id ? { ...prev, note: savedNote } : prev)
|
||||
// Refresh sidebar note titles so the new title appears immediately
|
||||
refreshNotes(savedNote.notebookId || null)
|
||||
}, [refreshNotes])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -359,13 +373,14 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
note={editingNote.note}
|
||||
readOnly={editingNote.readOnly}
|
||||
onClose={handleEditorClose}
|
||||
onNoteSaved={handleNoteSaved}
|
||||
fullPage
|
||||
/>
|
||||
) : (
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
<div className={cn(
|
||||
'px-12 pt-12 pb-8 flex flex-col gap-6',
|
||||
isEditorialMode ? 'sticky top-0 bg-background/90 backdrop-blur-md z-30 border-b border-foreground/5' : ''
|
||||
isEditorialMode ? 'sticky top-0 bg-background/90 backdrop-blur-md z-30' : ''
|
||||
)}>
|
||||
<div className="flex justify-between items-start">
|
||||
<h1 className="font-memento-serif text-4xl font-medium tracking-tight text-foreground leading-tight pr-12">
|
||||
@@ -374,53 +389,85 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-b border-foreground/5 pb-4">
|
||||
<button
|
||||
onClick={handleAddNote}
|
||||
disabled={isCreating}
|
||||
className="flex items-center gap-2 text-[13px] text-foreground font-medium hover:opacity-70 transition-opacity"
|
||||
>
|
||||
<Plus size={16} />
|
||||
<span>{t('notes.newNote') || 'Add Note'}</span>
|
||||
</button>
|
||||
|
||||
{/* Sort order */}
|
||||
<div className="relative">
|
||||
<div className="flex items-center gap-6">
|
||||
<button
|
||||
onClick={() => setShowSortMenu(s => !s)}
|
||||
className="flex items-center gap-1.5 text-[13px] text-muted-foreground hover:text-foreground font-medium transition-opacity"
|
||||
title={t('sidebar.sortOrder') || 'Sort order'}
|
||||
onClick={handleAddNote}
|
||||
disabled={isCreating}
|
||||
className="flex items-center gap-2 text-[13px] text-foreground font-medium hover:opacity-70 transition-opacity"
|
||||
>
|
||||
<ArrowUpDown size={14} />
|
||||
<span className="hidden sm:inline text-[11px] uppercase tracking-wider font-bold">
|
||||
{sortOrder === 'newest' ? 'Plus récentes' : sortOrder === 'oldest' ? 'Plus anciennes' : 'A → Z'}
|
||||
</span>
|
||||
<Plus size={16} />
|
||||
<span>{t('notes.newNote') || 'Add Note'}</span>
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{showSortMenu && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9, y: -4 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: -4 }}
|
||||
className="absolute right-0 top-full mt-2 bg-card border border-border rounded-xl shadow-lg z-50 py-1 min-w-[140px]"
|
||||
>
|
||||
{(['newest', 'oldest', 'alpha'] as SortOrder[]).map(order => (
|
||||
<button
|
||||
key={order}
|
||||
onClick={() => { setSortOrder(order); setShowSortMenu(false) }}
|
||||
className={cn(
|
||||
'w-full text-left px-4 py-2 text-[12px] transition-colors',
|
||||
sortOrder === order
|
||||
? 'font-bold text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/40'
|
||||
)}
|
||||
>
|
||||
{sortLabels[order]}
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Inline search — toggles an input within the toolbar */}
|
||||
{showInlineSearch ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Search size={14} className="text-muted-foreground shrink-0" />
|
||||
<input
|
||||
ref={inlineSearchRef}
|
||||
autoFocus
|
||||
type="text"
|
||||
value={inlineSearchQuery}
|
||||
onChange={e => {
|
||||
const q = e.target.value
|
||||
setInlineSearchQuery(q)
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
if (q.trim()) {
|
||||
params.set('search', q)
|
||||
} else {
|
||||
params.delete('search')
|
||||
}
|
||||
router.push(`/?${params.toString()}`)
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!inlineSearchQuery) {
|
||||
setShowInlineSearch(false)
|
||||
}
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Escape') {
|
||||
setShowInlineSearch(false)
|
||||
setInlineSearchQuery('')
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
params.delete('search')
|
||||
router.push(`/?${params.toString()}`)
|
||||
}
|
||||
}}
|
||||
placeholder={t('search.placeholder') || 'Rechercher...'}
|
||||
className="w-48 bg-transparent border-b border-foreground/20 focus:border-foreground outline-none text-[13px] text-foreground placeholder:text-muted-foreground/50 py-0.5 transition-colors"
|
||||
/>
|
||||
{inlineSearchQuery && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowInlineSearch(false)
|
||||
setInlineSearchQuery('')
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
params.delete('search')
|
||||
router.push(`/?${params.toString()}`)
|
||||
}}
|
||||
className="text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<span className="text-[11px]">×</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowInlineSearch(true)
|
||||
setTimeout(() => inlineSearchRef.current?.focus(), 50)
|
||||
}}
|
||||
className="flex items-center gap-2 text-[13px] text-foreground font-medium hover:opacity-70 transition-opacity"
|
||||
>
|
||||
<Search size={16} />
|
||||
<span>{t('notes.search') || 'Search'}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button className="flex items-center gap-2 text-[13px] text-foreground font-medium hover:opacity-70 transition-opacity">
|
||||
<Share2 size={16} />
|
||||
<span>{t('notes.share') || 'Share'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -442,13 +489,13 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
) : (
|
||||
<div className="max-w-3xl space-y-16">
|
||||
{sortedPinnedNotes.length > 0 && (
|
||||
<div className="mb-8">
|
||||
<h2 className="text-[10px] font-bold text-muted-foreground tracking-widest uppercase mb-6 px-2">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-[10px] font-bold text-muted-foreground tracking-widest uppercase mb-4 px-2">
|
||||
{t('notes.pinned')}
|
||||
</h2>
|
||||
<NotesEditorialView
|
||||
notes={sortedPinnedNotes}
|
||||
onOpen={(note: Note, readOnly?: boolean) => setEditingNote({ note, readOnly: readOnly ?? false })}
|
||||
onOpen={(note: Note, readOnly?: boolean) => handleOpenNoteFresh(note.id, readOnly ?? false)}
|
||||
notebookName={currentNotebook?.name}
|
||||
onOpenHistory={handleOpenHistory}
|
||||
/>
|
||||
@@ -458,7 +505,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
{sortedNotes.filter((note) => !note.isPinned).length > 0 && (
|
||||
<NotesEditorialView
|
||||
notes={sortedNotes.filter((note) => !note.isPinned)}
|
||||
onOpen={(note, readOnly) => setEditingNote({ note, readOnly })}
|
||||
onOpen={(note, readOnly) => handleOpenNoteFresh(note.id, readOnly ?? false)}
|
||||
notebookName={currentNotebook?.name}
|
||||
onOpenHistory={handleOpenHistory}
|
||||
/>
|
||||
@@ -490,7 +537,7 @@ export function HomeClient({ initialNotes, initialSettings }: HomeClientProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MemoryEchoNotification onOpenNote={handleOpenNote} />
|
||||
<MemoryEchoNotification onOpenNote={(noteId) => handleOpenNoteFresh(noteId)} />
|
||||
|
||||
{notebookSuggestion && (
|
||||
<NotebookSuggestionToast
|
||||
|
||||
Reference in New Issue
Block a user