refactor(ux): consolidate BMAD skills, update design system, and clean up Prisma generated client
This commit is contained in:
@@ -8,6 +8,7 @@ import { getAIProvider } from '@/lib/ai/factory'
|
||||
import { parseNote as parseNoteUtil, cosineSimilarity, validateEmbedding, calculateRRFK, detectQueryType, getSearchWeights } from '@/lib/utils'
|
||||
import { getSystemConfig, getConfigNumber, getConfigBoolean, SEARCH_DEFAULTS } from '@/lib/config'
|
||||
import { contextualAutoTagService } from '@/lib/ai/services/contextual-auto-tag.service'
|
||||
import { cleanupNoteImages, parseImageUrls, deleteImageFileSafely } from '@/lib/image-cleanup'
|
||||
|
||||
/**
|
||||
* Champs sélectionnés pour les listes de notes (sans embedding pour économiser ~6KB/note).
|
||||
@@ -20,6 +21,7 @@ const NOTE_LIST_SELECT = {
|
||||
color: true,
|
||||
isPinned: true,
|
||||
isArchived: true,
|
||||
trashedAt: true,
|
||||
type: true,
|
||||
dismissedFromRecent: true,
|
||||
checkItems: true,
|
||||
@@ -219,6 +221,7 @@ export async function getNotes(includeArchived = false) {
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
trashedAt: null,
|
||||
...(includeArchived ? {} : { isArchived: false }),
|
||||
},
|
||||
select: NOTE_LIST_SELECT,
|
||||
@@ -245,6 +248,7 @@ export async function getNotesWithReminders() {
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
trashedAt: null,
|
||||
isArchived: false,
|
||||
reminder: { not: null }
|
||||
},
|
||||
@@ -286,7 +290,8 @@ export async function getArchivedNotes() {
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
isArchived: true
|
||||
isArchived: true,
|
||||
trashedAt: null
|
||||
},
|
||||
select: NOTE_LIST_SELECT,
|
||||
orderBy: { updatedAt: 'desc' }
|
||||
@@ -321,6 +326,7 @@ export async function searchNotes(query: string, useSemantic: boolean = false, n
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
isArchived: false,
|
||||
trashedAt: null,
|
||||
OR: [
|
||||
{ title: { contains: query } },
|
||||
{ content: { contains: query } },
|
||||
@@ -349,6 +355,7 @@ async function semanticSearch(query: string, userId: string, notebookId?: string
|
||||
where: {
|
||||
userId: userId,
|
||||
isArchived: false,
|
||||
trashedAt: null,
|
||||
...(notebookId !== undefined ? { notebookId } : {})
|
||||
},
|
||||
include: { noteEmbedding: true }
|
||||
@@ -650,17 +657,16 @@ export async function updateNote(id: string, data: {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a note
|
||||
// Soft-delete a note (move to trash)
|
||||
export async function deleteNote(id: string) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) throw new Error('Unauthorized');
|
||||
|
||||
try {
|
||||
await prisma.note.delete({ where: { id, userId: session.user.id } })
|
||||
|
||||
// Sync labels with empty array to trigger cleanup of any orphans
|
||||
// The syncLabels function will scan all remaining notes and clean up unused labels
|
||||
await syncLabels(session.user.id, [])
|
||||
await prisma.note.update({
|
||||
where: { id, userId: session.user.id },
|
||||
data: { trashedAt: new Date() }
|
||||
})
|
||||
|
||||
revalidatePath('/')
|
||||
return { success: true }
|
||||
@@ -670,6 +676,192 @@ export async function deleteNote(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Trash actions
|
||||
export async function trashNote(id: string) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) throw new Error('Unauthorized');
|
||||
|
||||
try {
|
||||
await prisma.note.update({
|
||||
where: { id, userId: session.user.id },
|
||||
data: { trashedAt: new Date() }
|
||||
})
|
||||
revalidatePath('/')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Error trashing note:', error)
|
||||
throw new Error('Failed to trash note')
|
||||
}
|
||||
}
|
||||
|
||||
export async function restoreNote(id: string) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) throw new Error('Unauthorized');
|
||||
|
||||
try {
|
||||
await prisma.note.update({
|
||||
where: { id, userId: session.user.id },
|
||||
data: { trashedAt: null }
|
||||
})
|
||||
revalidatePath('/')
|
||||
revalidatePath('/trash')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Error restoring note:', error)
|
||||
throw new Error('Failed to restore note')
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTrashedNotes() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return [];
|
||||
|
||||
try {
|
||||
const notes = await prisma.note.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
trashedAt: { not: null }
|
||||
},
|
||||
select: NOTE_LIST_SELECT,
|
||||
orderBy: { trashedAt: 'desc' }
|
||||
})
|
||||
|
||||
return notes.map(parseNote)
|
||||
} catch (error) {
|
||||
console.error('Error fetching trashed notes:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export async function permanentDeleteNote(id: string) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) throw new Error('Unauthorized');
|
||||
|
||||
try {
|
||||
// Fetch images before deleting so we can clean up files
|
||||
const note = await prisma.note.findUnique({
|
||||
where: { id, userId: session.user.id },
|
||||
select: { images: true }
|
||||
})
|
||||
const imageUrls = parseImageUrls(note?.images ?? null)
|
||||
|
||||
await prisma.note.delete({ where: { id, userId: session.user.id } })
|
||||
|
||||
// Clean up orphaned image files (safe: skips if referenced by other notes)
|
||||
if (imageUrls.length > 0) {
|
||||
await cleanupNoteImages(id, imageUrls)
|
||||
}
|
||||
|
||||
await syncLabels(session.user.id, [])
|
||||
revalidatePath('/trash')
|
||||
revalidatePath('/')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Error permanently deleting note:', error)
|
||||
throw new Error('Failed to permanently delete note')
|
||||
}
|
||||
}
|
||||
|
||||
export async function emptyTrash() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) throw new Error('Unauthorized');
|
||||
|
||||
try {
|
||||
// Fetch trashed notes with images before deleting
|
||||
const trashedNotes = await prisma.note.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
trashedAt: { not: null }
|
||||
},
|
||||
select: { id: true, images: true }
|
||||
})
|
||||
|
||||
await prisma.note.deleteMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
trashedAt: { not: null }
|
||||
}
|
||||
})
|
||||
|
||||
// Clean up image files for all deleted notes
|
||||
for (const note of trashedNotes) {
|
||||
const imageUrls = parseImageUrls(note.images)
|
||||
if (imageUrls.length > 0) {
|
||||
await cleanupNoteImages(note.id, imageUrls)
|
||||
}
|
||||
}
|
||||
|
||||
await syncLabels(session.user.id, [])
|
||||
revalidatePath('/trash')
|
||||
revalidatePath('/')
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Error emptying trash:', error)
|
||||
throw new Error('Failed to empty trash')
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeImageFromNote(noteId: string, imageIndex: number) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) throw new Error('Unauthorized')
|
||||
|
||||
try {
|
||||
const note = await prisma.note.findUnique({
|
||||
where: { id: noteId, userId: session.user.id },
|
||||
select: { images: true },
|
||||
})
|
||||
if (!note) throw new Error('Note not found')
|
||||
|
||||
const imageUrls = parseImageUrls(note.images)
|
||||
if (imageIndex < 0 || imageIndex >= imageUrls.length) throw new Error('Invalid image index')
|
||||
|
||||
const removedUrl = imageUrls[imageIndex]
|
||||
const newImages = imageUrls.filter((_, i) => i !== imageIndex)
|
||||
|
||||
await prisma.note.update({
|
||||
where: { id: noteId },
|
||||
data: { images: newImages.length > 0 ? JSON.stringify(newImages) : null },
|
||||
})
|
||||
|
||||
// Clean up file if no other note references it
|
||||
await deleteImageFileSafely(removedUrl, noteId)
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Error removing image:', error)
|
||||
throw new Error('Failed to remove image')
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupOrphanedImages(imageUrls: string[], noteId: string) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) return
|
||||
|
||||
try {
|
||||
for (const url of imageUrls) {
|
||||
await deleteImageFileSafely(url, noteId)
|
||||
}
|
||||
} catch {
|
||||
// Silent — best-effort cleanup
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTrashCount() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return 0;
|
||||
|
||||
try {
|
||||
return await prisma.note.count({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
trashedAt: { not: null }
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle functions
|
||||
export async function togglePin(id: string, isPinned: boolean) { return updateNote(id, { isPinned }) }
|
||||
export async function toggleArchive(id: string, isArchived: boolean) { return updateNote(id, { isArchived }) }
|
||||
@@ -710,7 +902,7 @@ export async function reorderNotes(draggedId: string, targetId: string) {
|
||||
const targetNote = await prisma.note.findUnique({ where: { id: targetId, userId: session.user.id } })
|
||||
if (!draggedNote || !targetNote) throw new Error('Notes not found')
|
||||
const allNotes = await prisma.note.findMany({
|
||||
where: { userId: session.user.id, isPinned: draggedNote.isPinned, isArchived: false },
|
||||
where: { userId: session.user.id, isPinned: draggedNote.isPinned, isArchived: false, trashedAt: null },
|
||||
orderBy: { order: 'asc' }
|
||||
})
|
||||
const reorderedNotes = allNotes.filter((n: any) => n.id !== draggedId)
|
||||
@@ -865,11 +1057,12 @@ export async function syncAllEmbeddings() {
|
||||
const userId = session.user.id;
|
||||
let updatedCount = 0;
|
||||
try {
|
||||
const notesToSync = await prisma.note.findMany({
|
||||
where: {
|
||||
userId,
|
||||
const notesToSync = await prisma.note.findMany({
|
||||
where: {
|
||||
userId,
|
||||
trashedAt: null,
|
||||
noteEmbedding: { is: null }
|
||||
}
|
||||
}
|
||||
})
|
||||
const provider = getAIProvider(await getSystemConfig());
|
||||
for (const note of notesToSync) {
|
||||
@@ -905,6 +1098,7 @@ export async function getAllNotes(includeArchived = false) {
|
||||
prisma.note.findMany({
|
||||
where: {
|
||||
userId,
|
||||
trashedAt: null,
|
||||
...(includeArchived ? {} : { isArchived: false }),
|
||||
},
|
||||
select: NOTE_LIST_SELECT,
|
||||
@@ -923,6 +1117,7 @@ export async function getAllNotes(includeArchived = false) {
|
||||
const sharedNotes = acceptedShares
|
||||
.map(share => share.note)
|
||||
.filter(note => includeArchived || !note.isArchived)
|
||||
.map(note => ({ ...note, _isShared: true }))
|
||||
|
||||
return [...ownNotes.map(parseNote), ...sharedNotes.map(parseNote)]
|
||||
} catch (error) {
|
||||
@@ -944,6 +1139,7 @@ export async function getPinnedNotes(notebookId?: string) {
|
||||
userId: userId,
|
||||
isPinned: true,
|
||||
isArchived: false,
|
||||
trashedAt: null,
|
||||
...(notebookId !== undefined ? { notebookId } : {})
|
||||
},
|
||||
orderBy: [
|
||||
@@ -977,6 +1173,7 @@ export async function getRecentNotes(limit: number = 3) {
|
||||
userId: userId,
|
||||
contentUpdatedAt: { gte: sevenDaysAgo },
|
||||
isArchived: false,
|
||||
trashedAt: null,
|
||||
dismissedFromRecent: false // Filter out dismissed notes
|
||||
},
|
||||
orderBy: { contentUpdatedAt: 'desc' },
|
||||
@@ -1118,8 +1315,20 @@ export async function getNoteCollaborators(noteId: string) {
|
||||
throw new Error('Note not found')
|
||||
}
|
||||
|
||||
// Owner can always see collaborators
|
||||
// Shared users can also see collaborators if they have accepted access
|
||||
if (note.userId !== session.user.id) {
|
||||
throw new Error('You do not have access to this note')
|
||||
const share = await prisma.noteShare.findUnique({
|
||||
where: {
|
||||
noteId_userId: {
|
||||
noteId,
|
||||
userId: session.user.id
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!share || share.status !== 'accepted') {
|
||||
throw new Error('You do not have access to this note')
|
||||
}
|
||||
}
|
||||
|
||||
// Get all users who have been shared this note (any status)
|
||||
|
||||
Reference in New Issue
Block a user