feat: hierarchical notebook system - trash, selectors, breadcrumb, sidebar tree
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 2m9s

- Schema: soft delete with trashedAt on Notebook model
- API: PATCH/GET notebooks support trashedAt filtering with cascade
- Sidebar: recursive tree rendering with collapse/expand, visual guides, hover actions
- HierarchicalNotebookSelector: portal-based dropdown with search, breadcrumbs, dropUp support
- AI chat: context selector with Toutes mes notes + notebook selector
- Agent detail: flat selects replaced with HierarchicalNotebookSelector
- Breadcrumb: notebook path display on home page
- Trash view: card grid with countdown, restore/permanent delete
- CSS: design tokens (ink, paper, blueprint, concrete, etc.)
- Types: parentId, trashedAt added to Notebook interface
This commit is contained in:
Antigravity
2026-05-10 10:52:26 +00:00
parent 539c72cf6d
commit 916fb78dfb
20 changed files with 1319 additions and 391 deletions

View File

@@ -510,35 +510,49 @@ export const NoteCard = memo(function NoteCard({
<StickyNote className="h-4 w-4 mr-2" />
{t('notebookSuggestion.generalNotes')}
</DropdownMenuItem>
{notebooks.filter(nb => !nb.parentId).map((notebook: any) => {
const NotebookIcon = getNotebookIcon(notebook.icon || 'folder')
const children = notebooks.filter((c: any) => c.parentId === notebook.id)
if (children.length > 0) {
return (
<DropdownMenuSub key={notebook.id}>
<DropdownMenuSubTrigger className="gap-2">
<NotebookIcon className="h-4 w-4" />
{notebook.name}
<ChevronRight className="h-3 w-3 ml-auto opacity-50" />
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={() => handleMoveToNotebook(notebook.id)}>
<NotebookIcon className="h-4 w-4 mr-2" />
{notebook.name}
</DropdownMenuItem>
{children.map((child: any) => {
const ChildIcon = getNotebookIcon(child.icon || 'folder')
return (
<DropdownMenuItem key={child.id} onClick={() => handleMoveToNotebook(child.id)}>
<ChildIcon className="h-4 w-4 mr-2" />
{child.name}
</DropdownMenuItem>
)
})}
</DropdownMenuSubContent>
</DropdownMenuSub>
)
}
{notebooks.filter(nb => !nb.parentId && !nb.trashedAt).map((notebook: any) => {
const NotebookIcon = getNotebookIcon(notebook.icon || 'folder')
const allDescendants = (parentId: string): any[] => {
const kids = notebooks.filter((c: any) => c.parentId === parentId && !c.trashedAt)
return kids.flatMap((k: any) => [k, ...allDescendants(k.id)])
}
const descendants = allDescendants(notebook.id)
if (descendants.length > 0) {
return (
<DropdownMenuSub key={notebook.id}>
<DropdownMenuSubTrigger className="gap-2">
<NotebookIcon className="h-4 w-4" />
{notebook.name}
<ChevronRight className="h-3 w-3 ml-auto opacity-50" />
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuItem onClick={() => handleMoveToNotebook(notebook.id)}>
<NotebookIcon className="h-4 w-4 mr-2" />
{notebook.name}
</DropdownMenuItem>
{descendants.map((child: any) => {
const ChildIcon = getNotebookIcon(child.icon || 'folder')
const depth = (() => {
let d = 0
let current = child
while (current.parentId && current.parentId !== notebook.id) {
d++
current = notebooks.find((nb: any) => nb.id === current.parentId)
if (!current) break
}
return d
})()
return (
<DropdownMenuItem key={child.id} onClick={() => handleMoveToNotebook(child.id)}>
<NotebookIcon className="h-4 w-4 mr-2" />
<span className="ml-{depth * 2}">{child.name}</span>
</DropdownMenuItem>
)
})}
</DropdownMenuSubContent>
</DropdownMenuSub>
)
}
return (
<DropdownMenuItem key={notebook.id} onClick={() => handleMoveToNotebook(notebook.id)}>
<NotebookIcon className="h-4 w-4 mr-2" />