feat(notes): liens internes, onglet Réseau, living blocks et consentement IA
Rend les liens entre notes visibles et persistants (sync NoteLink au save, auto-save, graphe réseau rafraîchi), ajoute living blocks, Memory Echo, recherche globale, consentement IA explicite et consolide les prototypes design en architectural-grid. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,6 +5,8 @@ import { useQueryClient } from '@tanstack/react-query'
|
||||
import type { Notebook, Label, Note } from '@/lib/types'
|
||||
import { LabelColorName, LABEL_COLORS } from '@/lib/types'
|
||||
import { getHashColor } from '@/lib/utils'
|
||||
import { emitNoteChange } from '@/lib/note-change-sync'
|
||||
import { getNoteById } from '@/app/actions/notes'
|
||||
import { useNoteRefresh } from './NoteRefreshContext'
|
||||
import { toast } from 'sonner'
|
||||
import { queryKeys } from '@/lib/query-keys'
|
||||
@@ -104,7 +106,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isMovingNote, setIsMovingNote] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const { triggerRefresh, triggerNotebooksRefresh, notebooksRefreshKey } = useNoteRefresh()
|
||||
const { triggerNotebooksRefresh, notebooksRefreshKey } = useNoteRefresh()
|
||||
|
||||
// ===== LABEL STATE (merged from LabelContext) =====
|
||||
const [labels, setLabels] = useState<Label[]>([])
|
||||
@@ -187,8 +189,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
// Invalidate notebooks cache — React Query will re-fetch in bg
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
|
||||
triggerNotebooksRefresh()
|
||||
triggerRefresh()
|
||||
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
|
||||
}, [queryClient, triggerNotebooksRefresh])
|
||||
|
||||
const updateNotebook = useCallback(async (notebookId: string, data: UpdateNotebookInput) => {
|
||||
const response = await fetch(`/api/notebooks/${notebookId}`, {
|
||||
@@ -203,8 +204,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
|
||||
triggerNotebooksRefresh()
|
||||
triggerRefresh()
|
||||
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
|
||||
}, [queryClient, triggerNotebooksRefresh])
|
||||
|
||||
const moveNotebookToParent = useCallback(async (notebookId: string, parentId: string | null) => {
|
||||
const response = await fetch(`/api/notebooks/${notebookId}`, {
|
||||
@@ -220,8 +220,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
|
||||
triggerNotebooksRefresh()
|
||||
triggerRefresh()
|
||||
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
|
||||
}, [queryClient, triggerNotebooksRefresh])
|
||||
|
||||
const deleteNotebook = useCallback(async (notebookId: string) => {
|
||||
const response = await fetch(`/api/notebooks/${notebookId}`, {
|
||||
@@ -234,8 +233,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
|
||||
triggerNotebooksRefresh()
|
||||
triggerRefresh()
|
||||
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
|
||||
}, [queryClient, triggerNotebooksRefresh])
|
||||
|
||||
const trashNotebook = useCallback(async (notebookId: string) => {
|
||||
const response = await fetch(`/api/notebooks/${notebookId}`, {
|
||||
@@ -250,8 +248,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
|
||||
triggerNotebooksRefresh()
|
||||
triggerRefresh()
|
||||
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
|
||||
}, [queryClient, triggerNotebooksRefresh])
|
||||
|
||||
const restoreNotebook = useCallback(async (notebookId: string) => {
|
||||
const response = await fetch(`/api/notebooks/${notebookId}`, {
|
||||
@@ -266,8 +263,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
|
||||
triggerNotebooksRefresh()
|
||||
triggerRefresh()
|
||||
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
|
||||
}, [queryClient, triggerNotebooksRefresh])
|
||||
|
||||
const permanentDeleteNotebook = useCallback(async (notebookId: string) => {
|
||||
const response = await fetch(`/api/notebooks/${notebookId}`, {
|
||||
@@ -280,8 +276,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
|
||||
triggerNotebooksRefresh()
|
||||
triggerRefresh()
|
||||
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
|
||||
}, [queryClient, triggerNotebooksRefresh])
|
||||
|
||||
const updateNotebookOrderOptimistic = useCallback(async (notebookIds: string[]) => {
|
||||
const response = await fetch('/api/notebooks/reorder', {
|
||||
@@ -296,8 +291,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
|
||||
triggerNotebooksRefresh()
|
||||
triggerRefresh()
|
||||
}, [queryClient, triggerNotebooksRefresh, triggerRefresh])
|
||||
}, [queryClient, triggerNotebooksRefresh])
|
||||
|
||||
// ===== LABEL ACTIONS (merged from LabelContext) =====
|
||||
const addLabel = useCallback(async (name: string, color?: LabelColorName, labelNotebookId?: string | null) => {
|
||||
@@ -315,13 +309,12 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
setLabels(prev => [...prev, data.data])
|
||||
// Invalidate labels cache
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.labels(finalNotebookId) })
|
||||
triggerRefresh()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add label:', error)
|
||||
throw error
|
||||
}
|
||||
}, [notebookId, queryClient, triggerRefresh])
|
||||
}, [notebookId, queryClient])
|
||||
|
||||
const getLabelColor = useCallback((name: string): LabelColorName => {
|
||||
const label = labels.find(l => l.name.toLowerCase() === name.toLowerCase())
|
||||
@@ -331,8 +324,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
const refreshLabels = useCallback(async () => {
|
||||
await fetchLabels(notebookId)
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.labels(notebookId) })
|
||||
triggerRefresh()
|
||||
}, [fetchLabels, notebookId, queryClient, triggerRefresh])
|
||||
}, [fetchLabels, notebookId, queryClient])
|
||||
|
||||
// ===== ACTIONS: LABELS (keep existing API-based ones for compatibility) =====
|
||||
const createLabel = useCallback(async (data: CreateLabelInput) => {
|
||||
@@ -349,9 +341,8 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
const result = await response.json()
|
||||
// Invalidate labels cache for this notebook
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.labels(data.notebookId) })
|
||||
triggerRefresh()
|
||||
return result
|
||||
}, [queryClient, triggerRefresh])
|
||||
}, [queryClient])
|
||||
|
||||
const updateLabel = useCallback(async (labelId: string, data: UpdateLabelInput) => {
|
||||
const response = await fetch(`/api/labels/${labelId}`, {
|
||||
@@ -372,8 +363,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
}
|
||||
// Invalidate labels cache
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.labels(notebookId) })
|
||||
triggerRefresh()
|
||||
}, [notebookId, queryClient, triggerRefresh])
|
||||
}, [notebookId, queryClient])
|
||||
|
||||
const deleteLabel = useCallback(async (labelId: string) => {
|
||||
const response = await fetch(`/api/labels/${labelId}`, {
|
||||
@@ -387,8 +377,7 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
setLabels(prev => prev.filter(label => label.id !== labelId))
|
||||
// Invalidate labels cache
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.labels(notebookId) })
|
||||
triggerRefresh()
|
||||
}, [notebookId, queryClient, triggerRefresh])
|
||||
}, [notebookId, queryClient])
|
||||
|
||||
// ===== ACTIONS: NOTES =====
|
||||
const moveNoteToNotebookOptimistic = useCallback(async (noteId: string, targetNotebookId: string | null) => {
|
||||
@@ -404,18 +393,20 @@ export function NotebooksProvider({ children, initialNotebooks = [] }: Notebooks
|
||||
throw new Error('Failed to move note')
|
||||
}
|
||||
|
||||
// Invalidate notebooks and notes cache
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notebooks() })
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notes(null) }) // notes list
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.notes(null) })
|
||||
triggerNotebooksRefresh()
|
||||
triggerRefresh()
|
||||
const movedNote = await getNoteById(noteId)
|
||||
if (movedNote) {
|
||||
emitNoteChange({ type: 'updated', note: movedNote })
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to move note. Please try again.')
|
||||
throw error
|
||||
} finally {
|
||||
setIsMovingNote(false)
|
||||
}
|
||||
}, [queryClient, triggerRefresh, triggerNotebooksRefresh])
|
||||
}, [queryClient, triggerNotebooksRefresh])
|
||||
|
||||
// ===== ACTIONS: AI (STUBS) =====
|
||||
const suggestNotebookForNote = useCallback(async (_noteContent: string) => {
|
||||
|
||||
44
memento-note/context/search-modal-context.tsx
Normal file
44
memento-note/context/search-modal-context.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
|
||||
import { SearchModal } from '@/components/search-modal'
|
||||
|
||||
interface SearchModalContextValue {
|
||||
open: () => void
|
||||
close: () => void
|
||||
}
|
||||
|
||||
const SearchModalContext = createContext<SearchModalContextValue>({
|
||||
open: () => {},
|
||||
close: () => {},
|
||||
})
|
||||
|
||||
export function useSearchModal() {
|
||||
return useContext(SearchModalContext)
|
||||
}
|
||||
|
||||
export function SearchModalProvider({ children }: { children: ReactNode }) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const open = () => setIsOpen(true)
|
||||
const close = () => setIsOpen(false)
|
||||
|
||||
// Global keyboard shortcut: Ctrl+K or Cmd+K
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||
e.preventDefault()
|
||||
setIsOpen(prev => !prev)
|
||||
}
|
||||
}
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<SearchModalContext.Provider value={{ open, close }}>
|
||||
{children}
|
||||
<SearchModal isOpen={isOpen} onClose={close} />
|
||||
</SearchModalContext.Provider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user