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:
@@ -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>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
Reference in New Issue
Block a user