feat: smart note history with manual/auto modes, delete entries, i18n fixes
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m16s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m16s
- Add noteHistoryMode setting (manual default / auto) with DB migration - Manual mode: commit button in editor toolbar creates snapshots on demand - Auto mode: smart snapshots with 20-char diff threshold + 5min cooldown, structural changes (color, pin, archive, labels) bypass cooldown - Add delete individual history entries from history modal - Fix sidebar: Notes nav no longer active on notebook pages - Fix sidebar icon: replace filled Lightbulb with outlined FileText - Fix title suggestions: change from amber to sky blue color scheme - Fix hydration mismatch: add suppressHydrationWarning on locale dates - Complete i18n: add history, sort, and AI chat translations for all 16 languages - Translate French AI assistant section (40+ keys) from English to French - Update README with new features and stack info Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -18,8 +18,10 @@ import { useLanguage } from '@/lib/i18n'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
updateNote,
|
||||
toggleArchive,
|
||||
deleteNote,
|
||||
createNote,
|
||||
commitNoteHistory,
|
||||
} from '@/app/actions/notes'
|
||||
import { fetchLinkMetadata } from '@/app/actions/scrape'
|
||||
import {
|
||||
@@ -39,6 +41,8 @@ import {
|
||||
Loader2,
|
||||
Check,
|
||||
RotateCcw,
|
||||
History,
|
||||
GitCommitHorizontal,
|
||||
} from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { MarkdownContent } from '@/components/markdown-content'
|
||||
@@ -62,6 +66,9 @@ interface NoteInlineEditorProps {
|
||||
onDelete?: (noteId: string) => void
|
||||
onArchive?: (noteId: string) => void
|
||||
onChange?: (noteId: string, fields: Partial<Note>) => void
|
||||
onOpenHistory?: (note: Note) => void
|
||||
noteHistoryEnabled?: boolean
|
||||
noteHistoryMode?: 'manual' | 'auto'
|
||||
colorKey: NoteColor
|
||||
/** If true and the note is a Markdown note, open directly in preview mode */
|
||||
defaultPreviewMode?: boolean
|
||||
@@ -90,6 +97,9 @@ export function NoteInlineEditor({
|
||||
onDelete,
|
||||
onArchive,
|
||||
onChange,
|
||||
onOpenHistory,
|
||||
noteHistoryEnabled = false,
|
||||
noteHistoryMode = 'manual',
|
||||
colorKey,
|
||||
defaultPreviewMode = false,
|
||||
}: NoteInlineEditorProps) {
|
||||
@@ -313,7 +323,8 @@ export function NoteInlineEditor({
|
||||
startTransition(async () => {
|
||||
onArchive?.(note.id)
|
||||
try {
|
||||
await updateNote(note.id, { isArchived: !note.isArchived }, { skipRevalidation: true })
|
||||
await toggleArchive(note.id, !note.isArchived)
|
||||
triggerRefresh()
|
||||
} catch {
|
||||
// Cannot easily revert since onArchive removes from list
|
||||
toast.error(t('general.error'))
|
||||
@@ -479,7 +490,13 @@ export function NoteInlineEditor({
|
||||
|
||||
<Button variant="ghost" size="icon"
|
||||
className={cn('h-8 w-8', isMarkdown && 'text-primary bg-primary/10')}
|
||||
onClick={() => { setIsMarkdown(!isMarkdown); if (isMarkdown) setShowMarkdownPreview(false); scheduleSave() }}
|
||||
onClick={() => {
|
||||
const nextIsMarkdown = !isMarkdown
|
||||
setIsMarkdown(nextIsMarkdown)
|
||||
onChange?.(note.id, { isMarkdown: nextIsMarkdown })
|
||||
if (!nextIsMarkdown) setShowMarkdownPreview(false)
|
||||
scheduleSave()
|
||||
}}
|
||||
title="Markdown">
|
||||
<FileText className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -515,6 +532,26 @@ export function NoteInlineEditor({
|
||||
|
||||
{/* Right group: meta actions + save indicator */}
|
||||
<div className="flex items-center gap-1">
|
||||
{noteHistoryEnabled && noteHistoryMode === 'manual' && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 gap-1.5 text-xs text-primary/70 hover:text-primary"
|
||||
title={t('notes.commitVersion')}
|
||||
onClick={() => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await commitNoteHistory(note.id)
|
||||
toast.success(t('notes.versionSaved'))
|
||||
} catch {
|
||||
toast.error(t('general.error'))
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
<GitCommitHorizontal className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
)}
|
||||
<span className="mr-1 flex items-center gap-1 text-[11px] text-muted-foreground/50 select-none">
|
||||
{isSaving ? (
|
||||
<><Loader2 className="h-3 w-3 animate-spin" /> {t('notes.saving')}</>
|
||||
@@ -557,6 +594,16 @@ export function NoteInlineEditor({
|
||||
? <><ArchiveRestore className="h-4 w-4 mr-2" />{t('notes.unarchive')}</>
|
||||
: <><Archive className="h-4 w-4 mr-2" />{t('notes.archive')}</>}
|
||||
</DropdownMenuItem>
|
||||
{onOpenHistory && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => onOpenHistory(note)}
|
||||
>
|
||||
<History className="h-4 w-4 mr-2" />
|
||||
{noteHistoryEnabled
|
||||
? (t('notes.history') || 'Historique')
|
||||
: (t('notes.enableHistory') || "Activer l'historique")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-red-600 dark:text-red-400" onClick={handleDelete}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />{t('notes.delete')}
|
||||
@@ -781,9 +828,9 @@ export function NoteInlineEditor({
|
||||
{/* ── Footer ───────────────────────────────────────────────────────────── */}
|
||||
<div className="shrink-0 border-t border-border/20 px-8 py-2">
|
||||
<div className="flex items-center gap-3 text-[11px] text-muted-foreground/40">
|
||||
<span>{t('notes.modified') } {formatDistanceToNow(new Date(note.updatedAt), { addSuffix: true, locale: dateLocale })}</span>
|
||||
<span suppressHydrationWarning>{t('notes.modified') } {formatDistanceToNow(new Date(note.updatedAt), { addSuffix: true, locale: dateLocale })}</span>
|
||||
<span>·</span>
|
||||
<span>{t('notes.created') || 'Créée'} {formatDistanceToNow(new Date(note.createdAt), { addSuffix: true, locale: dateLocale })}</span>
|
||||
<span suppressHydrationWarning>{t('notes.created') || 'Créée'} {formatDistanceToNow(new Date(note.createdAt), { addSuffix: true, locale: dateLocale })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user