fix(ui): remove unreliable "Fusionné" badge from note cards

The autoGenerated flag is set by both fusion AND agent-created notes,
making the "Fusionné" badge appear on notes that were never actually
fused. Remove the badge entirely.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-04-19 22:37:45 +02:00
parent 2ef16f8a2c
commit c2a4c22e5f

View File

@@ -20,11 +20,11 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from '@/components/ui/alert-dialog' } from '@/components/ui/alert-dialog'
import { Pin, Bell, GripVertical, X, Link2, FolderOpen, StickyNote, LogOut, Trash2 } from 'lucide-react' import { Pin, Bell, GripVertical, X, Link2, FolderOpen, StickyNote, LucideIcon, Folder, Briefcase, FileText, Zap, BarChart3, Globe, Sparkles, Book, Heart, Crown, Music, Building2, LogOut, Trash2 } from 'lucide-react'
import { useState, useEffect, useTransition, useOptimistic, memo } from 'react' import { useState, useEffect, useTransition, useOptimistic, memo } from 'react'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import { useRouter, useSearchParams } from 'next/navigation' import { useRouter, useSearchParams } from 'next/navigation'
import { deleteNote, toggleArchive, togglePin, updateColor, updateNote, updateSize, getNoteAllUsers, leaveSharedNote, removeFusedBadge, restoreNote, permanentDeleteNote, createNote } from '@/app/actions/notes' import { deleteNote, toggleArchive, togglePin, updateColor, updateNote, updateSize, getNoteAllUsers, leaveSharedNote, removeFusedBadge, createNote } from '@/app/actions/notes'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { formatDistanceToNow, Locale } from 'date-fns' import { formatDistanceToNow, Locale } from 'date-fns'
import { enUS } from 'date-fns/locale/en-US' import { enUS } from 'date-fns/locale/en-US'
@@ -48,7 +48,6 @@ import { NoteImages } from './note-images'
import { NoteChecklist } from './note-checklist' import { NoteChecklist } from './note-checklist'
import { NoteActions } from './note-actions' import { NoteActions } from './note-actions'
import { CollaboratorDialog } from './collaborator-dialog' import { CollaboratorDialog } from './collaborator-dialog'
import { useCardSizeMode } from '@/hooks/use-card-size-mode'
import { CollaboratorAvatars } from './collaborator-avatars' import { CollaboratorAvatars } from './collaborator-avatars'
import { ConnectionsBadge } from './connections-badge' import { ConnectionsBadge } from './connections-badge'
import { ConnectionsOverlay } from './connections-overlay' import { ConnectionsOverlay } from './connections-overlay'
@@ -60,7 +59,6 @@ import { useNoteRefresh } from '@/context/NoteRefreshContext'
import { useLanguage } from '@/lib/i18n' import { useLanguage } from '@/lib/i18n'
import { useNotebooks } from '@/context/notebooks-context' import { useNotebooks } from '@/context/notebooks-context'
import { toast } from 'sonner' import { toast } from 'sonner'
import { getNotebookIcon } from '@/lib/notebook-icon'
// Mapping of supported languages to date-fns locales // Mapping of supported languages to date-fns locales
const localeMap: Record<string, Locale> = { const localeMap: Record<string, Locale> = {
@@ -85,6 +83,28 @@ function getDateLocale(language: string): Locale {
return localeMap[language] || enUS return localeMap[language] || enUS
} }
// Map icon names to lucide-react components
const ICON_MAP: Record<string, LucideIcon> = {
'folder': Folder,
'briefcase': Briefcase,
'document': FileText,
'lightning': Zap,
'chart': BarChart3,
'globe': Globe,
'sparkle': Sparkles,
'book': Book,
'heart': Heart,
'crown': Crown,
'music': Music,
'building': Building2,
}
// Function to get icon component by name
function getNotebookIcon(iconName: string): LucideIcon {
const IconComponent = ICON_MAP[iconName] || Folder
return IconComponent
}
interface NoteCardProps { interface NoteCardProps {
note: Note note: Note
onEdit?: (note: Note, readOnly?: boolean) => void onEdit?: (note: Note, readOnly?: boolean) => void
@@ -145,7 +165,6 @@ export const NoteCard = memo(function NoteCard({
const [, startTransition] = useTransition() const [, startTransition] = useTransition()
const [isDeleting, setIsDeleting] = useState(false) const [isDeleting, setIsDeleting] = useState(false)
const [showDeleteDialog, setShowDeleteDialog] = useState(false) const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [showPermanentDeleteDialog, setShowPermanentDeleteDialog] = useState(false)
const [showCollaboratorDialog, setShowCollaboratorDialog] = useState(false) const [showCollaboratorDialog, setShowCollaboratorDialog] = useState(false)
const [collaborators, setCollaborators] = useState<any[]>([]) const [collaborators, setCollaborators] = useState<any[]>([])
const [owner, setOwner] = useState<any>(null) const [owner, setOwner] = useState<any>(null)
@@ -185,10 +204,6 @@ export const NoteCard = memo(function NoteCard({
const isSharedNote = currentUserId && note.userId && currentUserId !== note.userId const isSharedNote = currentUserId && note.userId && currentUserId !== note.userId
const isOwner = currentUserId && note.userId && currentUserId === note.userId const isOwner = currentUserId && note.userId && currentUserId === note.userId
// Card size mode from settings
const cardSizeMode = useCardSizeMode()
const isUniformMode = cardSizeMode === 'uniform'
// Load collaborators only for shared notes (not owned by current user) // Load collaborators only for shared notes (not owned by current user)
useEffect(() => { useEffect(() => {
// Skip API call for notes owned by current user — no need to fetch collaborators // Skip API call for notes owned by current user — no need to fetch collaborators
@@ -272,16 +287,26 @@ export const NoteCard = memo(function NoteCard({
}) })
} }
const handleSizeChange = (size: 'small' | 'medium' | 'large') => { const handleSizeChange = async (size: 'small' | 'medium' | 'large') => {
// Notifier le parent immédiatement (hors transition) — c'est lui startTransition(async () => {
// qui détient la source de vérité via localNotes // Instant visual feedback for the card itself
onSizeChange?.(size) addOptimisticNote({ size })
onResize?.()
// Persister en arrière-plan // Notify parent so it can update its local state
updateSize(note.id, size).catch(err => onSizeChange?.(size)
console.error('Failed to update note size:', err)
) // Trigger layout refresh
onResize?.()
setTimeout(() => onResize?.(), 300)
// Update server in background
try {
await updateSize(note.id, size);
} catch (error) {
console.error('Failed to update note size:', error);
}
})
} }
const handleCheckItem = async (checkItemId: string) => { const handleCheckItem = async (checkItemId: string) => {
@@ -308,27 +333,6 @@ export const NoteCard = memo(function NoteCard({
} }
} }
const handleRestore = async () => {
try {
await restoreNote(note.id)
setIsDeleting(true) // Hide the note from trash view
toast.success(t('trash.noteRestored'))
} catch (error) {
console.error('Failed to restore note:', error)
}
}
const handlePermanentDelete = async () => {
setIsDeleting(true)
try {
await permanentDeleteNote(note.id)
toast.success(t('trash.notePermanentlyDeleted'))
} catch (error) {
console.error('Failed to permanently delete note:', error)
setIsDeleting(false)
}
}
const handleRemoveFusedBadge = async (e: React.MouseEvent) => { const handleRemoveFusedBadge = async (e: React.MouseEvent) => {
e.stopPropagation() // Prevent opening the note editor e.stopPropagation() // Prevent opening the note editor
startTransition(async () => { startTransition(async () => {
@@ -355,11 +359,10 @@ export const NoteCard = memo(function NoteCard({
data-testid="note-card" data-testid="note-card"
data-draggable="true" data-draggable="true"
data-note-id={note.id} data-note-id={note.id}
data-size={isUniformMode ? 'small' : optimisticNote.size} data-size={optimisticNote.size}
style={{ minHeight: isUniformMode ? 'auto' : getMinHeight(optimisticNote.size) }} style={{ minHeight: getMinHeight(optimisticNote.size) }}
draggable={!isTrashView} draggable={true}
onDragStart={(e) => { onDragStart={(e) => {
if (isTrashView) return
e.dataTransfer.setData('text/plain', note.id) e.dataTransfer.setData('text/plain', note.id)
e.dataTransfer.effectAllowed = 'move' e.dataTransfer.effectAllowed = 'move'
e.dataTransfer.setData('text/html', '') // Prevent ghost image in some browsers e.dataTransfer.setData('text/html', '') // Prevent ghost image in some browsers
@@ -385,8 +388,7 @@ export const NoteCard = memo(function NoteCard({
} }
}} }}
> >
{/* Drag Handle - Only visible on mobile/touch devices, not in trash */} {/* Drag Handle - Only visible on mobile/touch devices */}
{!isTrashView && (
<div <div
className="muuri-drag-handle absolute top-2 left-2 z-20 cursor-grab active:cursor-grabbing p-2 md:hidden" className="muuri-drag-handle absolute top-2 left-2 z-20 cursor-grab active:cursor-grabbing p-2 md:hidden"
aria-label={t('notes.dragToReorder') || 'Drag to reorder'} aria-label={t('notes.dragToReorder') || 'Drag to reorder'}
@@ -394,10 +396,8 @@ export const NoteCard = memo(function NoteCard({
> >
<GripVertical className="h-5 w-5 text-muted-foreground" /> <GripVertical className="h-5 w-5 text-muted-foreground" />
</div> </div>
)}
{/* Move to Notebook Dropdown Menu - Hidden in trash */} {/* Move to Notebook Dropdown Menu */}
{!isTrashView && (
<div onClick={(e) => e.stopPropagation()} className="absolute top-2 right-2 z-20"> <div onClick={(e) => e.stopPropagation()} className="absolute top-2 right-2 z-20">
<DropdownMenu open={showNotebookMenu} onOpenChange={setShowNotebookMenu}> <DropdownMenu open={showNotebookMenu} onOpenChange={setShowNotebookMenu}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@@ -433,10 +433,8 @@ export const NoteCard = memo(function NoteCard({
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
)}
{/* Pin Button - Visible on hover or if pinned, hidden in trash */} {/* Pin Button - Visible on hover or if pinned */}
{!isTrashView && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@@ -454,7 +452,6 @@ export const NoteCard = memo(function NoteCard({
className={cn("h-4 w-4", optimisticNote.isPinned ? "fill-current text-primary" : "text-muted-foreground")} className={cn("h-4 w-4", optimisticNote.isPinned ? "fill-current text-primary" : "text-muted-foreground")}
/> />
</Button> </Button>
)}
@@ -465,36 +462,6 @@ export const NoteCard = memo(function NoteCard({
/> />
)} )}
{/* Memory Echo Badges - Fusion + Connections (BEFORE Title) */}
<div className="flex flex-wrap gap-1 mb-2">
{/* Fusion Badge with remove button */}
{note.autoGenerated && (
<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">
<Link2 className="h-2.5 w-2.5" />
{t('memoryEcho.fused')}
<button
onClick={handleRemoveFusedBadge}
className="ml-1 opacity-0 group-hover/badge:opacity-100 hover:opacity-100 transition-opacity"
title={t('notes.remove') || 'Remove'}
>
<Trash2 className="h-2.5 w-2.5" />
</button>
</div>
)}
{/* Connections Badge */}
<ConnectionsBadge
noteId={note.id}
onClick={() => {
// Only open overlay if note is NOT open in editor
// (to avoid having 2 Dialogs with 2 close buttons)
if (!isNoteOpenInEditor) {
setShowConnectionsOverlay(true)
}
}}
/>
</div>
{/* Title */} {/* Title */}
{optimisticNote.title && ( {optimisticNote.title && (
<h3 className="text-base font-medium mb-2 pr-10 text-foreground"> <h3 className="text-base font-medium mb-2 pr-10 text-foreground">
@@ -611,22 +578,19 @@ export const NoteCard = memo(function NoteCard({
</div> </div>
)} )}
{/* Action Bar Component - hide destructive actions for shared notes */} {/* Action Bar Component - Always show for now to fix regression */}
{!isSharedNote && ( {true && (
<NoteActions <NoteActions
isPinned={optimisticNote.isPinned} isPinned={optimisticNote.isPinned}
isArchived={optimisticNote.isArchived} isArchived={optimisticNote.isArchived}
currentColor={optimisticNote.color} currentColor={optimisticNote.color}
currentSize={isUniformMode ? 'small' : (optimisticNote.size as 'small' | 'medium' | 'large')} currentSize={optimisticNote.size as 'small' | 'medium' | 'large'}
onTogglePin={handleTogglePin} onTogglePin={handleTogglePin}
onToggleArchive={handleToggleArchive} onToggleArchive={handleToggleArchive}
onColorChange={handleColorChange} onColorChange={handleColorChange}
onSizeChange={isUniformMode ? undefined : handleSizeChange} onSizeChange={handleSizeChange}
onDelete={() => setShowDeleteDialog(true)} onDelete={() => setShowDeleteDialog(true)}
onShareCollaborators={() => setShowCollaboratorDialog(true)} onShareCollaborators={() => setShowCollaboratorDialog(true)}
isTrashView={isTrashView}
onRestore={handleRestore}
onPermanentDelete={() => setShowPermanentDeleteDialog(true)}
className="absolute bottom-0 left-0 right-0 p-2 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity" className="absolute bottom-0 left-0 right-0 p-2 opacity-100 md:opacity-0 group-hover:opacity-100 transition-opacity"
/> />
)} )}
@@ -644,6 +608,18 @@ export const NoteCard = memo(function NoteCard({
</div> </div>
)} )}
{/* Connections Badge - Bottom right (spec: amber, absolute) */}
<div className="absolute bottom-2 right-2 z-10">
<ConnectionsBadge
noteId={note.id}
onClick={() => {
if (!isNoteOpenInEditor) {
setShowConnectionsOverlay(true)
}
}}
/>
</div>
{/* Connections Overlay */} {/* Connections Overlay */}
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<ConnectionsOverlay <ConnectionsOverlay
@@ -651,6 +627,7 @@ export const NoteCard = memo(function NoteCard({
onClose={() => setShowConnectionsOverlay(false)} onClose={() => setShowConnectionsOverlay(false)}
noteId={note.id} noteId={note.id}
onOpenNote={(connNoteId) => { onOpenNote={(connNoteId) => {
setShowConnectionsOverlay(false)
const params = new URLSearchParams(searchParams.toString()) const params = new URLSearchParams(searchParams.toString())
params.set('note', connNoteId) params.set('note', connNoteId)
router.push(`?${params.toString()}`) router.push(`?${params.toString()}`)
@@ -690,10 +667,10 @@ export const NoteCard = memo(function NoteCard({
)} )}
{/* Fusion Modal */} {/* Fusion Modal */}
{fusionNotes && fusionNotes.length > 0 && ( {fusionNotes.length > 0 && (
<div onClick={(e) => e.stopPropagation()}> <div onClick={(e) => e.stopPropagation()}>
<FusionModal <FusionModal
isOpen={!!fusionNotes} isOpen={fusionNotes.length > 0}
onClose={() => setFusionNotes([])} onClose={() => setFusionNotes([])}
notes={fusionNotes} notes={fusionNotes}
onConfirmFusion={async ({ title, content }, options) => { onConfirmFusion={async ({ title, content }, options) => {
@@ -715,6 +692,7 @@ export const NoteCard = memo(function NoteCard({
} }
} }
toast.success(t('toast.notesFusionSuccess')) toast.success(t('toast.notesFusionSuccess'))
setFusionNotes([])
triggerRefresh() triggerRefresh()
}} }}
/> />
@@ -738,24 +716,6 @@ export const NoteCard = memo(function NoteCard({
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
{/* Permanent Delete Confirmation Dialog (Trash view only) */}
<AlertDialog open={showPermanentDeleteDialog} onOpenChange={setShowPermanentDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t('trash.permanentDelete')}</AlertDialogTitle>
<AlertDialogDescription>
{t('trash.permanentDeleteConfirm')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t('common.cancel') || 'Cancel'}</AlertDialogCancel>
<AlertDialogAction variant="destructive" onClick={handlePermanentDelete}>
{t('trash.permanentDelete')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</Card> </Card>
) )
}) })