feat: note history modal with restore, diff comparison, and dynamic UI updates
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m14s

- Complete rewrite of note-history-modal: version list with inline restore/delete,
  split diff view with synced scrolling, rich preview for markdown/richtext/checklist
- Fix restore not updating editor: use key={id-updatedAt} on NoteInlineEditor to
  force remount on restore, and add sync useEffect to reset local state on prop changes
- Add sync useEffect in NoteEditor for external note updates
- Add historyEnabled to NOTE_LIST_SELECT (no more 'Activer' on every modal open)
- Replace browser confirm() with shadcn AlertDialog for delete confirmation
- Add i18n keys: currentVersion, compareVersions, diffTitle, diffSelectHint, deleteVersionDesc
- Install diff + @types/diff for visual line diffing
- Fix MasonryGrid render-phase sync to preserve local sizes
- Update notes-tabs-view merge logic for restored note propagation
- Remove debug console.log from restoreNoteVersion
This commit is contained in:
2026-05-02 16:51:12 +02:00
parent bd4dd0e9eb
commit 3818eb8237
13 changed files with 1012 additions and 302 deletions

View File

@@ -535,7 +535,7 @@ export const NoteCard = memo(function NoteCard({
)}
{/* Fusion Badge */}
{optimisticNote.aiProvider === 'fusion' && (
{note.aiProvider === 'fusion' && optimisticNote.autoGenerated !== null && (
<div className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 border border-purple-200 dark:border-purple-800 flex items-center gap-1 group/badge relative mb-2 w-fit">
<Link2 className="h-2.5 w-2.5" />
{t('memoryEcho.fused')}
@@ -550,28 +550,28 @@ export const NoteCard = memo(function NoteCard({
)}
{/* Title */}
{optimisticNote.title && (
{note.title && (
<h3 dir="auto" className="text-lg font-heading font-semibold mb-2 pr-20 text-foreground leading-tight tracking-tight flex items-center gap-2">
{(() => {
const TypeIcon = NOTE_TYPE_ICONS[optimisticNote.type] || AlignLeft
const TypeIcon = NOTE_TYPE_ICONS[note.type] || AlignLeft
return <TypeIcon className="h-4 w-4 shrink-0 text-muted-foreground/50" />
})()}
<span className="min-w-0 truncate">{optimisticNote.title}</span>
<span className="min-w-0 truncate">{note.title}</span>
</h3>
)}
{/* Search Match Type Badge */}
{optimisticNote.matchType && (
{note.matchType && (
<Badge
variant={optimisticNote.matchType === 'exact' ? 'default' : 'secondary'}
variant={note.matchType === 'exact' ? 'default' : 'secondary'}
className={cn(
'mb-2 text-xs',
optimisticNote.matchType === 'exact'
note.matchType === 'exact'
? 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-800'
: 'bg-primary/10 text-primary border-primary/20 dark:bg-primary/20 dark:text-primary-foreground'
)}
>
{t(`semanticSearch.${optimisticNote.matchType === 'exact' ? 'exactMatch' : 'related'}`)}
{t(`semanticSearch.${note.matchType === 'exact' ? 'exactMatch' : 'related'}`)}
</Badge>
)}
@@ -597,12 +597,12 @@ export const NoteCard = memo(function NoteCard({
)}
{/* Images Component */}
<NoteImages images={optimisticNote.images || []} title={optimisticNote.title} />
<NoteImages images={note.images || []} title={note.title} />
{/* Link Previews */}
{Array.isArray(optimisticNote.links) && optimisticNote.links.length > 0 && (
{Array.isArray(note.links) && note.links.length > 0 && (
<div className="flex flex-col gap-2 mb-2">
{optimisticNote.links.map((link, idx) => (
{note.links.map((link: any, idx: number) => (
<a
key={idx}
href={link.url}
@@ -627,26 +627,26 @@ export const NoteCard = memo(function NoteCard({
)}
{/* Content */}
{optimisticNote.type === 'checklist' ? (
{note.type === 'checklist' ? (
<NoteChecklist
items={localCheckItems || optimisticNote.checkItems || []}
items={localCheckItems || optimisticNote.checkItems || note.checkItems || []}
onToggleItem={handleCheckItem}
/>
) : optimisticNote.type === 'richtext' ? (
<div className="text-sm text-foreground line-clamp-10 rt-preview" dangerouslySetInnerHTML={{ __html: optimisticNote.content || '' }} />
) : note.type === 'richtext' ? (
<div className="text-sm text-foreground line-clamp-10 rt-preview" dangerouslySetInnerHTML={{ __html: note.content || '' }} />
) : (
<div className="text-sm text-foreground line-clamp-10">
<MarkdownContent
content={optimisticNote.content}
content={note.content}
className="prose-h1:text-xl prose-h1:font-semibold prose-h1:leading-snug prose-h1:mt-1 prose-h1:mb-2 prose-h2:text-lg prose-h2:font-medium prose-h3:text-base prose-p:text-sm prose-p:leading-relaxed"
/>
</div>
)}
{/* Labels - using shared LabelBadge component */}
{optimisticNote.notebookId && Array.isArray(optimisticNote.labels) && optimisticNote.labels.length > 0 && (
{note.notebookId && Array.isArray(note.labels) && note.labels.length > 0 && (
<div className="flex flex-wrap gap-1 mt-3">
{optimisticNote.labels.map((label) => (
{note.labels.map((label: string) => (
<LabelBadge key={label} label={label} />
))}
</div>
@@ -691,7 +691,7 @@ export const NoteCard = memo(function NoteCard({
onRestore={handleRestore}
onPermanentDelete={handlePermanentDelete}
onOpenHistory={() => onOpenHistory?.(note)}
historyEnabled={noteHistoryEnabled}
historyEnabled={!!note.historyEnabled}
noteId={note.id}
currentReminder={reminderDate}
onUpdateReminder={handleUpdateReminder}